Skip to content

Commit 089cfe0

Browse files
authored
feat: add kubernetes_list_clusters tool (#31)
1 parent fa412fa commit 089cfe0

File tree

12 files changed

+211
-22
lines changed

12 files changed

+211
-22
lines changed

.envrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ watch_file *.nix
33
dotenv_if_exists .env # You can create a .env file with your env vars for this project. You can also use .secrets if you are using act. See the line below.
44
dotenv_if_exists .secrets # Used by [act](https://nektosact.com/) to load secrets into the pipelines
55
strict_env
6-
env_vars_required SYSDIG_MCP_API_HOST SYSDIG_MCP_API_SECURE_TOKEN
6+
env_vars_required SYSDIG_MCP_API_HOST SYSDIG_MCP_API_TOKEN

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
run: just check
3131
env:
3232
SYSDIG_MCP_API_HOST: ${{ vars.SYSDIG_MCP_API_HOST }}
33-
SYSDIG_MCP_API_SECURE_TOKEN: ${{ secrets.SYSDIG_MCP_API_SECURE_TOKEN }}
33+
SYSDIG_MCP_API_TOKEN: ${{ secrets.SYSDIG_MCP_API_SECURE_TOKEN }}
3434
build:
3535
name: Build
3636
runs-on: ubuntu-latest

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ The handler filters tools dynamically based on `GetMyPermissions` from Sysdig Se
4949
| `get_event_process_tree` | `tool_get_event_process_tree.go` | Retrieve the process tree for an event when available. | `policy-events.read` | “Show the process tree behind event `abc123`.” |
5050
| `run_sysql` | `tool_run_sysql.go` | Execute caller-supplied Sysdig SysQL queries safely. | `sage.exec`, `risks.read` | “Run the following SysQL…”. |
5151
| `generate_sysql` | `tool_generate_sysql.go` | Convert natural language to SysQL via Sysdig Sage. | `sage.exec` (does not work with Service Accounts) | “Create a SysQL to list S3 buckets.” |
52+
| `kubernetes_list_clusters` | `tool_kubernetes_list_clusters.go` | Lists Kubernetes cluster information. | None | "List all Kubernetes clusters" |
5253

5354
Every tool has a companion `_test.go` file that exercises request validation, permission metadata, and Sysdig client calls through mocks.
5455
Note that if you add more tools you need to also update this file to reflect that.
@@ -70,7 +71,7 @@ Note that if you add more tools you need to also update this file to reflect tha
7071

7172
## Troubleshooting & Tips
7273

73-
- **Missing config:** `SYSDIG_MCP_API_HOST` and `SYSDIG_MCP_API_SECURE_TOKEN` are mandatory in `stdio`. Validation fails early in `internal/config/config.go`.
74+
- **Missing config:** `SYSDIG_MCP_API_HOST` and `SYSDIG_MCP_API_TOKEN` are mandatory in `stdio`. Validation fails early in `internal/config/config.go`.
7475
- **Token scope:** If a tool does not appear, verify the token’s permissions under **Settings > Users & Teams > Roles**. `generate_sysql` currently requires a regular user token, not a Service Account.
7576
- **Remote auth:** When using `streamable-http` or `sse`, pass `Authorization: Bearer <token>` and optionally `X-Sysdig-Host`. These values override env vars via the request context middleware.
7677
- **Environment drift:** Always run inside `nix develop`; lint/test expect binaries like `gofumpt`, `golangci-lint`, and `ginkgo` provided by the flake.

README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,12 @@ Get up and running with the Sysdig MCP Server quickly using our pre-built Docker
6565
"-e",
6666
"SYSDIG_MCP_TRANSPORT",
6767
"-e",
68-
"SYSDIG_MCP_API_SECURE_TOKEN",
68+
"SYSDIG_MCP_API_TOKEN",
6969
"ghcr.io/sysdiglabs/sysdig-mcp-server:latest"
7070
],
7171
"env": {
7272
"SYSDIG_MCP_API_HOST": "<your_sysdig_host>",
73-
"SYSDIG_MCP_API_SECURE_TOKEN": "<your_sysdig_secure_api_token>",
73+
"SYSDIG_MCP_API_TOKEN": "<your_sysdig_secure_api_token>",
7474
"SYSDIG_MCP_TRANSPORT": "stdio"
7575
}
7676
}
@@ -108,6 +108,11 @@ The server dynamically filters the available tools based on the permissions asso
108108
- **Required Permission**: `sage.exec`, `risks.read`
109109
- **Sample Prompt**: "Run this query: MATCH CloudResource WHERE type = 'aws_s3_bucket' LIMIT 10"
110110

111+
- **`kubernetes_list_clusters`**
112+
- **Description**: Lists the cluster information for all clusters or just the cluster specified.
113+
- **Required Permission**: `promql.exec`
114+
- **Sample Prompt**: "List all kubernetes clusters" or "Show me info for cluster 'production-gke'"
115+
111116
## Requirements
112117

113118
- [Go](https://go.dev/doc/install) 1.25 or higher (if running without Docker).
@@ -117,7 +122,7 @@ The server dynamically filters the available tools based on the permissions asso
117122
The following environment variables are **required** for configuring the Sysdig SDK:
118123

119124
- `SYSDIG_MCP_API_HOST`: The URL of your Sysdig Secure instance (e.g., `https://us2.app.sysdig.com`). **Required when using `stdio` transport.**
120-
- `SYSDIG_MCP_API_SECURE_TOKEN`: Your Sysdig Secure API token. **Required only when using `stdio` transport.**
125+
- `SYSDIG_MCP_API_TOKEN`: Your Sysdig Secure API token. **Required only when using `stdio` transport.**
121126

122127
You can also set the following variables to override the default configuration:
123128

@@ -138,7 +143,7 @@ You can find your API token in the Sysdig Secure UI under **Settings > Sysdig Se
138143
```bash
139144
# Required
140145
SYSDIG_MCP_API_HOST=https://us2.app.sysdig.com
141-
SYSDIG_MCP_API_SECURE_TOKEN=your-api-token-here
146+
SYSDIG_MCP_API_TOKEN=your-api-token-here
142147
143148
# Optional
144149
SYSDIG_MCP_TRANSPORT=stdio
@@ -153,7 +158,7 @@ SYSDIG_MCP_TRANSPORT=streamable-http
153158
154159
# Optional (Host and Token can be provided via HTTP headers)
155160
# SYSDIG_MCP_API_HOST=https://us2.app.sysdig.com
156-
# SYSDIG_MCP_API_SECURE_TOKEN=your-api-token-here
161+
# SYSDIG_MCP_API_TOKEN=your-api-token-here
157162
SYSDIG_MCP_LISTENING_PORT=8080
158163
SYSDIG_MCP_LISTENING_HOST=localhost
159164
SYSDIG_MCP_MOUNT_PATH=/sysdig-mcp-server
@@ -199,13 +204,13 @@ You can run the MCP server using Docker (recommended for production) or directly
199204
The easiest way to run the server is using the pre-built Docker image from GitHub Container Registry (as shown in the [Quickstart Guide](#quickstart-guide)).
200205

201206
```bash
202-
docker run -e SYSDIG_MCP_API_HOST=<your_sysdig_host> -e SYSDIG_MCP_API_SECURE_TOKEN=<your_sysdig_secure_api_token> -e SYSDIG_MCP_TRANSPORT=stdio -p 8080:8080 ghcr.io/sysdiglabs/sysdig-mcp-server:latest
207+
docker run -e SYSDIG_MCP_API_HOST=<your_sysdig_host> -e SYSDIG_MCP_API_TOKEN=<your_sysdig_secure_api_token> -e SYSDIG_MCP_TRANSPORT=stdio -p 8080:8080 ghcr.io/sysdiglabs/sysdig-mcp-server:latest
203208
```
204209

205210
To use the `streamable-http` or `sse` transports (for remote MCP clients), set the `SYSDIG_MCP_TRANSPORT` environment variable accordingly:
206211

207212
```bash
208-
docker run -e SYSDIG_MCP_TRANSPORT=streamable-http -e SYSDIG_MCP_API_HOST=<your_sysdig_host> -e SYSDIG_MCP_API_SECURE_TOKEN=<your_sysdig_secure_api_token> -p 8080:8080 ghcr.io/sysdiglabs/sysdig-mcp-server:latest
213+
docker run -e SYSDIG_MCP_TRANSPORT=streamable-http -e SYSDIG_MCP_API_HOST=<your_sysdig_host> -e SYSDIG_MCP_API_TOKEN=<your_sysdig_secure_api_token> -p 8080:8080 ghcr.io/sysdiglabs/sysdig-mcp-server:latest
209214
```
210215

211216
### Go
@@ -289,12 +294,12 @@ For the Claude Desktop app, you can manually configure the MCP server by editing
289294
"-e",
290295
"SYSDIG_MCP_TRANSPORT",
291296
"-e",
292-
"SYSDIG_MCP_API_SECURE_TOKEN",
297+
"SYSDIG_MCP_API_TOKEN",
293298
"ghcr.io/sysdiglabs/sysdig-mcp-server:latest"
294299
],
295300
"env": {
296301
"SYSDIG_MCP_API_HOST": "<your_sysdig_host>",
297-
"SYSDIG_MCP_API_SECURE_TOKEN": "<your_sysdig_secure_api_token>",
302+
"SYSDIG_MCP_API_TOKEN": "<your_sysdig_secure_api_token>",
298303
"SYSDIG_MCP_TRANSPORT": "stdio"
299304
}
300305
}
@@ -315,7 +320,7 @@ For the Claude Desktop app, you can manually configure the MCP server by editing
315320
],
316321
"env": {
317322
"SYSDIG_MCP_API_HOST": "<your_sysdig_host>",
318-
"SYSDIG_MCP_API_SECURE_TOKEN": "<your_sysdig_secure_api_token>",
323+
"SYSDIG_MCP_API_TOKEN": "<your_sysdig_secure_api_token>",
319324
"SYSDIG_MCP_TRANSPORT": "stdio"
320325
}
321326
}
@@ -357,7 +362,7 @@ For the Claude Desktop app, you can manually configure the MCP server by editing
357362
env_keys:
358363
- SYSDIG_MCP_TRANSPORT
359364
- SYSDIG_MCP_API_HOST
360-
- SYSDIG_MCP_API_SECURE_TOKEN
365+
- SYSDIG_MCP_API_TOKEN
361366
envs:
362367
SYSDIG_MCP_TRANSPORT: stdio
363368
name: sysdig-mcp-server

cmd/server/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ func setupHandler(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *mcp
9494
tools.NewToolGetEventProcessTree(sysdigClient),
9595
tools.NewToolRunSysql(sysdigClient),
9696
tools.NewToolGenerateSysql(sysdigClient),
97+
98+
tools.NewKubernetesListClusters(sysdigClient),
9799
)
98100
return handler
99101
}

internal/config/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ func (c *Config) Validate() error {
2020
return fmt.Errorf("required configuration missing: SYSDIG_MCP_API_HOST")
2121
}
2222
if c.Transport == "stdio" && c.APIToken == "" {
23-
return fmt.Errorf("required configuration missing: SYSDIG_MCP_API_SECURE_TOKEN")
23+
return fmt.Errorf("required configuration missing: SYSDIG_MCP_API_TOKEN")
2424
}
2525
return nil
2626
}
2727

2828
func Load() (*Config, error) {
2929
cfg := &Config{
3030
APIHost: getEnv("SYSDIG_MCP_API_HOST", ""),
31-
APIToken: getEnv("SYSDIG_MCP_API_SECURE_TOKEN", ""),
31+
APIToken: getEnv("SYSDIG_MCP_API_TOKEN", ""),
3232
Transport: getEnv("SYSDIG_MCP_TRANSPORT", "stdio"),
3333
ListeningHost: getEnv("SYSDIG_MCP_LISTENING_HOST", "localhost"),
3434
ListeningPort: getEnv("SYSDIG_MCP_LISTENING_PORT", "8080"),

internal/config/config_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ var _ = Describe("Config", func() {
5858
}
5959
err := cfg.Validate()
6060
Expect(err).To(HaveOccurred())
61-
Expect(err.Error()).To(ContainSubstring("SYSDIG_MCP_API_SECURE_TOKEN"))
61+
Expect(err.Error()).To(ContainSubstring("SYSDIG_MCP_API_TOKEN"))
6262
})
6363
})
6464
})
@@ -71,7 +71,7 @@ var _ = Describe("Config", func() {
7171
Context("with required env vars set for stdio", func() {
7272
BeforeEach(func() {
7373
_ = os.Setenv("SYSDIG_MCP_API_HOST", "host")
74-
_ = os.Setenv("SYSDIG_MCP_API_SECURE_TOKEN", "token")
74+
_ = os.Setenv("SYSDIG_MCP_API_TOKEN", "token")
7575
})
7676

7777
It("should load default values", func() {
@@ -105,7 +105,7 @@ var _ = Describe("Config", func() {
105105
Context("with all env vars set", func() {
106106
BeforeEach(func() {
107107
_ = os.Setenv("SYSDIG_MCP_API_HOST", "env-host")
108-
_ = os.Setenv("SYSDIG_MCP_API_SECURE_TOKEN", "env-token")
108+
_ = os.Setenv("SYSDIG_MCP_API_TOKEN", "env-token")
109109
_ = os.Setenv("SYSDIG_MCP_TRANSPORT", "http")
110110
_ = os.Setenv("SYSDIG_MCP_LISTENING_HOST", "0.0.0.0")
111111
_ = os.Setenv("SYSDIG_MCP_LISTENING_PORT", "9090")
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/mark3labs/mcp-go/server"
11+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig"
12+
)
13+
14+
type KubernetesListClusters struct {
15+
SysdigClient sysdig.ExtendedClientWithResponsesInterface
16+
}
17+
18+
func NewKubernetesListClusters(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *KubernetesListClusters {
19+
return &KubernetesListClusters{
20+
SysdigClient: sysdigClient,
21+
}
22+
}
23+
24+
func (t *KubernetesListClusters) RegisterInServer(s *server.MCPServer) {
25+
tool := mcp.NewTool("kubernetes_list_clusters",
26+
mcp.WithDescription("Lists the cluster information for all clusters or just the cluster specified."),
27+
mcp.WithString("cluster_name", mcp.Description("The name of the cluster to filter by.")),
28+
mcp.WithNumber("limit",
29+
mcp.Description("Maximum number of clusters to return."),
30+
mcp.DefaultNumber(10),
31+
),
32+
mcp.WithOutputSchema[map[string]any](),
33+
WithRequiredPermissions(), // FIXME(fede): Add the required permissions. It should be `promql.exec` but somehow the token does not have that permission even if you are able to execute queries.
34+
)
35+
s.AddTool(tool, t.handle)
36+
}
37+
38+
func (t *KubernetesListClusters) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
39+
clusterName := mcp.ParseString(request, "cluster_name", "")
40+
limit := mcp.ParseInt(request, "limit", 10)
41+
42+
query := "kube_cluster_info"
43+
if clusterName != "" {
44+
query = fmt.Sprintf("kube_cluster_info{cluster=\"%s\"}", clusterName)
45+
}
46+
47+
limitQuery := sysdig.LimitQuery(limit)
48+
params := &sysdig.GetQueryV1Params{
49+
Query: query,
50+
Limit: &limitQuery,
51+
}
52+
53+
httpResp, err := t.SysdigClient.GetQueryV1(ctx, params)
54+
if err != nil {
55+
return mcp.NewToolResultErrorFromErr("failed to get cluster list", err), nil
56+
}
57+
58+
if httpResp.StatusCode != 200 {
59+
bodyBytes, _ := io.ReadAll(httpResp.Body)
60+
return mcp.NewToolResultErrorf("failed to get cluster list: status code %d, body: %s", httpResp.StatusCode, string(bodyBytes)), nil
61+
}
62+
63+
var queryResponse sysdig.QueryResponseV1
64+
if err := json.NewDecoder(httpResp.Body).Decode(&queryResponse); err != nil {
65+
return mcp.NewToolResultErrorFromErr("failed to decode response", err), nil
66+
}
67+
68+
return mcp.NewToolResultJSON(queryResponse)
69+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package tools_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"net/http"
8+
9+
"github.com/mark3labs/mcp-go/mcp"
10+
"github.com/mark3labs/mcp-go/server"
11+
. "github.com/onsi/ginkgo/v2"
12+
. "github.com/onsi/gomega"
13+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/mcp/tools"
14+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig"
15+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig/mocks"
16+
"go.uber.org/mock/gomock"
17+
)
18+
19+
var _ = Describe("KubernetesListClusters Tool", func() {
20+
var (
21+
tool *tools.KubernetesListClusters
22+
mockSysdig *mocks.MockExtendedClientWithResponsesInterface
23+
mcpServer *server.MCPServer
24+
ctrl *gomock.Controller
25+
)
26+
27+
BeforeEach(func() {
28+
ctrl = gomock.NewController(GinkgoT())
29+
mockSysdig = mocks.NewMockExtendedClientWithResponsesInterface(ctrl)
30+
tool = tools.NewKubernetesListClusters(mockSysdig)
31+
mcpServer = server.NewMCPServer("test", "test")
32+
tool.RegisterInServer(mcpServer)
33+
})
34+
35+
It("should register successfully in the server", func() {
36+
Expect(mcpServer.GetTool("kubernetes_list_clusters")).NotTo(BeNil())
37+
})
38+
39+
When("listing all clusters", func() {
40+
DescribeTable("it succeeds", func(ctx context.Context, toolName string, request mcp.CallToolRequest, expectedParamsRequested sysdig.GetQueryV1Params) {
41+
mockSysdig.EXPECT().GetQueryV1(gomock.Any(), &expectedParamsRequested).Return(&http.Response{
42+
StatusCode: http.StatusOK,
43+
Body: io.NopCloser(bytes.NewBufferString(`{"status":"success"}`)),
44+
}, nil)
45+
46+
serverTool := mcpServer.GetTool(toolName)
47+
result, err := serverTool.Handler(ctx, request)
48+
Expect(err).NotTo(HaveOccurred())
49+
50+
resultData, ok := result.Content[0].(mcp.TextContent)
51+
Expect(ok).To(BeTrue())
52+
Expect(resultData.Text).To(MatchJSON(`{"status":"success"}`))
53+
},
54+
Entry(nil,
55+
"kubernetes_list_clusters",
56+
mcp.CallToolRequest{
57+
Params: mcp.CallToolParams{
58+
Name: "kubernetes_list_clusters",
59+
Arguments: map[string]any{},
60+
},
61+
},
62+
sysdig.GetQueryV1Params{
63+
Query: `kube_cluster_info`,
64+
Limit: asPtr(sysdig.LimitQuery(10)),
65+
},
66+
),
67+
Entry(nil,
68+
"kubernetes_list_clusters",
69+
mcp.CallToolRequest{
70+
Params: mcp.CallToolParams{
71+
Name: "kubernetes_list_clusters",
72+
Arguments: map[string]any{"limit": "20"},
73+
},
74+
},
75+
sysdig.GetQueryV1Params{
76+
Query: `kube_cluster_info`,
77+
Limit: asPtr(sysdig.LimitQuery(20)),
78+
},
79+
),
80+
Entry(nil,
81+
"kubernetes_list_clusters",
82+
mcp.CallToolRequest{
83+
Params: mcp.CallToolParams{
84+
Name: "kubernetes_list_clusters",
85+
Arguments: map[string]any{"cluster_name": "my_cluster"},
86+
},
87+
},
88+
sysdig.GetQueryV1Params{
89+
Query: `kube_cluster_info{cluster="my_cluster"}`,
90+
Limit: asPtr(sysdig.LimitQuery(10)),
91+
},
92+
),
93+
Entry(nil,
94+
"kubernetes_list_clusters",
95+
mcp.CallToolRequest{
96+
Params: mcp.CallToolParams{
97+
Name: "kubernetes_list_clusters",
98+
Arguments: map[string]any{"cluster_name": "my_cluster", "limit": "20"},
99+
},
100+
},
101+
sysdig.GetQueryV1Params{
102+
Query: `kube_cluster_info{cluster="my_cluster"}`,
103+
Limit: asPtr(sysdig.LimitQuery(20)),
104+
},
105+
),
106+
)
107+
})
108+
})
109+
110+
func asPtr[T any](arg T) *T {
111+
return &arg
112+
}

internal/infra/sysdig/client_generate_sysql_integration_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ var _ = Describe("Sysdig Generate Sysql Client", func() {
1515

1616
BeforeEach(func() {
1717
sysdigURL := os.Getenv("SYSDIG_MCP_API_HOST")
18-
sysdigToken := os.Getenv("SYSDIG_MCP_API_SECURE_TOKEN")
18+
sysdigToken := os.Getenv("SYSDIG_MCP_API_TOKEN")
1919

2020
var err error
2121
client, err = sysdig.NewSysdigClient(sysdig.WithFixedHostAndToken(sysdigURL, sysdigToken))

0 commit comments

Comments
 (0)