Skip to content

Commit cf06447

Browse files
committed
feat(tools): add kubernetes_list_clusters tool
This commit introduces a new tool to list Kubernetes clusters using a PromQL query. The tool supports filtering by cluster name and limiting the number of results. It uses the Sysdig client to execute the PromQL query.
1 parent fa412fa commit cf06447

File tree

5 files changed

+188
-0
lines changed

5 files changed

+188
-0
lines changed

AGENTS.md

Lines changed: 1 addition & 0 deletions
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.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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**: None
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).

cmd/server/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func setupHandler(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *mcp
9494
tools.NewToolGetEventProcessTree(sysdigClient),
9595
tools.NewToolRunSysql(sysdigClient),
9696
tools.NewToolGenerateSysql(sysdigClient),
97+
tools.NewKubernetesListClusters(sysdigClient),
9798
)
9899
return handler
99100
}
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+
}

0 commit comments

Comments
 (0)