Skip to content

Commit a258bf4

Browse files
authored
feat: add kubernetes_list_pod_containers tool (#34)
1 parent 92e7355 commit a258bf4

File tree

6 files changed

+332
-1
lines changed

6 files changed

+332
-1
lines changed

.gemini/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"mcpServers": {
33
"sysdig": {
44
"command": "go",
5-
"args": ["run", "./cmd/server"]
5+
"args": ["run", "./cmd/server"],
6+
"trust": true
67
}
78
}
89
}

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ The handler filters tools dynamically based on `GetMyPermissions` from Sysdig Se
5252
| `kubernetes_list_clusters` | `tool_kubernetes_list_clusters.go` | Lists Kubernetes cluster information. | `promql.exec` | "List all Kubernetes clusters" |
5353
| `kubernetes_list_nodes` | `tool_kubernetes_list_nodes.go` | Lists Kubernetes node information. | `promql.exec` | "List all Kubernetes nodes in the cluster 'production-gke'" |
5454
| `kubernetes_list_workloads` | `tool_kubernetes_list_workloads.go` | Lists Kubernetes workload information. | `promql.exec` | "List all desired workloads in the cluster 'production-gke' and namespace 'default'" |
55+
| `kubernetes_list_pod_containers` | `tool_kubernetes_list_pod_containers.go` | Retrieves information from a particular pod and container. | `promql.exec` | "Show me info for pod 'my-pod' in cluster 'production-gke'" |
5556

5657
Every tool has a companion `_test.go` file that exercises request validation, permission metadata, and Sysdig client calls through mocks.
5758
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
@@ -123,6 +123,11 @@ The server dynamically filters the available tools based on the permissions asso
123123
- **Required Permission**: `promql.exec`
124124
- **Sample Prompt**: "List all desired workloads in the cluster 'production-gke' and namespace 'default'"
125125

126+
- **`kubernetes_list_pod_containers`**
127+
- **Description**: Retrieves information from a particular pod and container.
128+
- **Required Permission**: `promql.exec`
129+
- **Sample Prompt**: "Show me info for pod 'my-pod' in cluster 'production-gke'"
130+
126131
## Requirements
127132

128133
- [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
@@ -98,6 +98,7 @@ func setupHandler(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *mcp
9898
tools.NewKubernetesListClusters(sysdigClient),
9999
tools.NewKubernetesListNodes(sysdigClient),
100100
tools.NewKubernetesListWorkloads(sysdigClient),
101+
tools.NewKubernetesListPodContainers(sysdigClient),
101102
)
102103
return handler
103104
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package tools
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"strings"
9+
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
"github.com/sysdiglabs/sysdig-mcp-server/internal/infra/sysdig"
13+
)
14+
15+
type KubernetesListPodContainers struct {
16+
SysdigClient sysdig.ExtendedClientWithResponsesInterface
17+
}
18+
19+
func NewKubernetesListPodContainers(sysdigClient sysdig.ExtendedClientWithResponsesInterface) *KubernetesListPodContainers {
20+
return &KubernetesListPodContainers{
21+
SysdigClient: sysdigClient,
22+
}
23+
}
24+
25+
func (t *KubernetesListPodContainers) RegisterInServer(s *server.MCPServer) {
26+
tool := mcp.NewTool("kubernetes_list_pod_containers",
27+
mcp.WithDescription("Retrieves information from a particular pod and container."),
28+
mcp.WithString("cluster_name", mcp.Description("The name of the cluster to filter by.")),
29+
mcp.WithString("namespace_name", mcp.Description("The name of the namespace to filter by.")),
30+
mcp.WithString("workload_type", mcp.Description("The type of the workload to filter by.")),
31+
mcp.WithString("workload_name", mcp.Description("The name of the workload to filter by.")),
32+
mcp.WithString("pod_name", mcp.Description("The name of the pod to filter by.")),
33+
mcp.WithString("container_name", mcp.Description("The name of the container to filter by.")),
34+
mcp.WithString("image_pullstring", mcp.Description("The image pullstring to filter by.")),
35+
mcp.WithString("node_name", mcp.Description("The name of the node to filter by.")),
36+
mcp.WithNumber("limit",
37+
mcp.Description("Maximum number of pod containers to return."),
38+
mcp.DefaultNumber(10),
39+
),
40+
mcp.WithOutputSchema[map[string]any](),
41+
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.
42+
)
43+
s.AddTool(tool, t.handle)
44+
}
45+
46+
func (t *KubernetesListPodContainers) handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
47+
clusterName := mcp.ParseString(request, "cluster_name", "")
48+
namespaceName := mcp.ParseString(request, "namespace_name", "")
49+
workloadType := mcp.ParseString(request, "workload_type", "")
50+
workloadName := mcp.ParseString(request, "workload_name", "")
51+
podName := mcp.ParseString(request, "pod_name", "")
52+
containerName := mcp.ParseString(request, "container_name", "")
53+
imagePullstring := mcp.ParseString(request, "image_pullstring", "")
54+
nodeName := mcp.ParseString(request, "node_name", "")
55+
limit := mcp.ParseInt(request, "limit", 10)
56+
57+
query := buildKubePodContainerInfoQuery(clusterName, namespaceName, workloadType, workloadName, podName, containerName, imagePullstring, nodeName)
58+
59+
limitQuery := sysdig.LimitQuery(limit)
60+
params := &sysdig.GetQueryV1Params{
61+
Query: query,
62+
Limit: &limitQuery,
63+
}
64+
65+
httpResp, err := t.SysdigClient.GetQueryV1(ctx, params)
66+
if err != nil {
67+
return mcp.NewToolResultErrorFromErr("failed to get pod container list", err), nil
68+
}
69+
70+
if httpResp.StatusCode != 200 {
71+
bodyBytes, _ := io.ReadAll(httpResp.Body)
72+
return mcp.NewToolResultErrorf("failed to get pod container list: status code %d, body: %s", httpResp.StatusCode, string(bodyBytes)), nil
73+
}
74+
75+
var queryResponse sysdig.QueryResponseV1
76+
if err := json.NewDecoder(httpResp.Body).Decode(&queryResponse); err != nil {
77+
return mcp.NewToolResultErrorFromErr("failed to decode response", err), nil
78+
}
79+
80+
return mcp.NewToolResultJSON(queryResponse)
81+
}
82+
83+
func buildKubePodContainerInfoQuery(clusterName, namespaceName, workloadType, workloadName, podName, containerName, imagePullstring, nodeName string) string {
84+
filters := []string{}
85+
if clusterName != "" {
86+
filters = append(filters, fmt.Sprintf("kube_cluster_name=\"%s\"", clusterName))
87+
}
88+
if namespaceName != "" {
89+
filters = append(filters, fmt.Sprintf("kube_namespace_name=\"%s\"", namespaceName))
90+
}
91+
if workloadType != "" {
92+
filters = append(filters, fmt.Sprintf("kube_workload_type=\"%s\"", workloadType))
93+
}
94+
if workloadName != "" {
95+
filters = append(filters, fmt.Sprintf("kube_workload_name=\"%s\"", workloadName))
96+
}
97+
if podName != "" {
98+
filters = append(filters, fmt.Sprintf("kube_pod_name=\"%s\"", podName))
99+
}
100+
if containerName != "" {
101+
filters = append(filters, fmt.Sprintf("kube_pod_container_name=\"%s\"", containerName))
102+
}
103+
if imagePullstring != "" {
104+
filters = append(filters, fmt.Sprintf("image=\"%s\"", imagePullstring))
105+
}
106+
if nodeName != "" {
107+
filters = append(filters, fmt.Sprintf("kube_node_name=\"%s\"", nodeName))
108+
}
109+
110+
if len(filters) == 0 {
111+
return "kube_pod_container_info"
112+
}
113+
114+
return fmt.Sprintf("kube_pod_container_info{%s}", strings.Join(filters, ","))
115+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
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("KubernetesListPodContainers Tool", func() {
20+
var (
21+
tool *tools.KubernetesListPodContainers
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.NewKubernetesListPodContainers(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_pod_containers")).NotTo(BeNil())
37+
})
38+
39+
When("listing pod containers", 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_pod_containers",
56+
mcp.CallToolRequest{
57+
Params: mcp.CallToolParams{
58+
Name: "kubernetes_list_pod_containers",
59+
Arguments: map[string]any{},
60+
},
61+
},
62+
sysdig.GetQueryV1Params{
63+
Query: `kube_pod_container_info`,
64+
Limit: asPtr(sysdig.LimitQuery(10)),
65+
},
66+
),
67+
Entry(nil,
68+
"kubernetes_list_pod_containers",
69+
mcp.CallToolRequest{
70+
Params: mcp.CallToolParams{
71+
Name: "kubernetes_list_pod_containers",
72+
Arguments: map[string]any{"limit": "20"},
73+
},
74+
},
75+
sysdig.GetQueryV1Params{
76+
Query: `kube_pod_container_info`,
77+
Limit: asPtr(sysdig.LimitQuery(20)),
78+
},
79+
),
80+
Entry(nil,
81+
"kubernetes_list_pod_containers",
82+
mcp.CallToolRequest{
83+
Params: mcp.CallToolParams{
84+
Name: "kubernetes_list_pod_containers",
85+
Arguments: map[string]any{"cluster_name": "my_cluster"},
86+
},
87+
},
88+
sysdig.GetQueryV1Params{
89+
Query: `kube_pod_container_info{kube_cluster_name="my_cluster"}`,
90+
Limit: asPtr(sysdig.LimitQuery(10)),
91+
},
92+
),
93+
Entry(nil,
94+
"kubernetes_list_pod_containers",
95+
mcp.CallToolRequest{
96+
Params: mcp.CallToolParams{
97+
Name: "kubernetes_list_pod_containers",
98+
Arguments: map[string]any{"namespace_name": "my_namespace"},
99+
},
100+
},
101+
sysdig.GetQueryV1Params{
102+
Query: `kube_pod_container_info{kube_namespace_name="my_namespace"}`,
103+
Limit: asPtr(sysdig.LimitQuery(10)),
104+
},
105+
),
106+
Entry(nil,
107+
"kubernetes_list_pod_containers",
108+
mcp.CallToolRequest{
109+
Params: mcp.CallToolParams{
110+
Name: "kubernetes_list_pod_containers",
111+
Arguments: map[string]any{"workload_type": "my_workload_type"},
112+
},
113+
},
114+
sysdig.GetQueryV1Params{
115+
Query: `kube_pod_container_info{kube_workload_type="my_workload_type"}`,
116+
Limit: asPtr(sysdig.LimitQuery(10)),
117+
},
118+
),
119+
Entry(nil,
120+
"kubernetes_list_pod_containers",
121+
mcp.CallToolRequest{
122+
Params: mcp.CallToolParams{
123+
Name: "kubernetes_list_pod_containers",
124+
Arguments: map[string]any{"workload_name": "my_workload_name"},
125+
},
126+
},
127+
sysdig.GetQueryV1Params{
128+
Query: `kube_pod_container_info{kube_workload_name="my_workload_name"}`,
129+
Limit: asPtr(sysdig.LimitQuery(10)),
130+
},
131+
),
132+
Entry(nil,
133+
"kubernetes_list_pod_containers",
134+
mcp.CallToolRequest{
135+
Params: mcp.CallToolParams{
136+
Name: "kubernetes_list_pod_containers",
137+
Arguments: map[string]any{"pod_name": "my_pod_name"},
138+
},
139+
},
140+
sysdig.GetQueryV1Params{
141+
Query: `kube_pod_container_info{kube_pod_name="my_pod_name"}`,
142+
Limit: asPtr(sysdig.LimitQuery(10)),
143+
},
144+
),
145+
Entry(nil,
146+
"kubernetes_list_pod_containers",
147+
mcp.CallToolRequest{
148+
Params: mcp.CallToolParams{
149+
Name: "kubernetes_list_pod_containers",
150+
Arguments: map[string]any{"container_name": "my_container_name"},
151+
},
152+
},
153+
sysdig.GetQueryV1Params{
154+
Query: `kube_pod_container_info{kube_pod_container_name="my_container_name"}`,
155+
Limit: asPtr(sysdig.LimitQuery(10)),
156+
},
157+
),
158+
Entry(nil,
159+
"kubernetes_list_pod_containers",
160+
mcp.CallToolRequest{
161+
Params: mcp.CallToolParams{
162+
Name: "kubernetes_list_pod_containers",
163+
Arguments: map[string]any{"image_pullstring": "my_image"},
164+
},
165+
},
166+
sysdig.GetQueryV1Params{
167+
Query: `kube_pod_container_info{image="my_image"}`,
168+
Limit: asPtr(sysdig.LimitQuery(10)),
169+
},
170+
),
171+
Entry(nil,
172+
"kubernetes_list_pod_containers",
173+
mcp.CallToolRequest{
174+
Params: mcp.CallToolParams{
175+
Name: "kubernetes_list_pod_containers",
176+
Arguments: map[string]any{"node_name": "my_node_name"},
177+
},
178+
},
179+
sysdig.GetQueryV1Params{
180+
Query: `kube_pod_container_info{kube_node_name="my_node_name"}`,
181+
Limit: asPtr(sysdig.LimitQuery(10)),
182+
},
183+
),
184+
Entry(nil,
185+
"kubernetes_list_pod_containers",
186+
mcp.CallToolRequest{
187+
Params: mcp.CallToolParams{
188+
Name: "kubernetes_list_pod_containers",
189+
Arguments: map[string]any{
190+
"cluster_name": "my_cluster",
191+
"namespace_name": "my_namespace",
192+
"workload_type": "my_workload_type",
193+
"workload_name": "my_workload_name",
194+
"pod_name": "my_pod_name",
195+
"container_name": "my_container_name",
196+
"image_pullstring": "my_image",
197+
"node_name": "my_node_name",
198+
},
199+
},
200+
},
201+
sysdig.GetQueryV1Params{
202+
Query: `kube_pod_container_info{kube_cluster_name="my_cluster",kube_namespace_name="my_namespace",kube_workload_type="my_workload_type",kube_workload_name="my_workload_name",kube_pod_name="my_pod_name",kube_pod_container_name="my_container_name",image="my_image",kube_node_name="my_node_name"}`,
203+
Limit: asPtr(sysdig.LimitQuery(10)),
204+
},
205+
),
206+
)
207+
})
208+
})

0 commit comments

Comments
 (0)