Skip to content

Commit 3ea23f3

Browse files
committed
feat(kubernetes): resources_get can get any resource in the cluster
1 parent b91f948 commit 3ea23f3

File tree

2 files changed

+148
-11
lines changed

2 files changed

+148
-11
lines changed

pkg/mcp/resources.go

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ func (s *Sever) initResources() {
2525
mcp.Description("Optional Namespace to retrieve the namespaced resources from (ignored in case of cluster scoped resources). If not provided, will list resources from all namespaces"),
2626
),
2727
), resourcesList)
28+
s.server.AddTool(mcp.NewTool(
29+
"resources_get",
30+
mcp.WithDescription("Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name"),
31+
mcp.WithString("apiVersion",
32+
mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
33+
mcp.Required(),
34+
),
35+
mcp.WithString("kind",
36+
mcp.Description("kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)"),
37+
mcp.Required(),
38+
),
39+
mcp.WithString("namespace",
40+
mcp.Description("Optional Namespace to retrieve the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will get resource from configured namespace"),
41+
),
42+
mcp.WithString("name",
43+
mcp.Description("Name of the resource"),
44+
mcp.Required(),
45+
),
46+
), resourcesGet)
2847
s.server.AddTool(mcp.NewTool(
2948
"resources_create_or_update",
3049
mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource"),
@@ -38,27 +57,43 @@ func (s *Sever) initResources() {
3857
func resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
3958
k, err := kubernetes.NewKubernetes()
4059
if err != nil {
41-
return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil
60+
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
4261
}
43-
apiVersion := ctr.Params.Arguments["apiVersion"]
44-
if apiVersion == nil {
45-
return NewTextResult("", errors.New("failed to list resources, missing argument apiVersion")), nil
62+
namespace := ctr.Params.Arguments["namespace"]
63+
if namespace == nil {
64+
namespace = ""
4665
}
47-
kind := ctr.Params.Arguments["kind"]
48-
if kind == nil {
49-
return NewTextResult("", errors.New("failed to list resources, missing argument kind")), nil
66+
gvk, err := parseGroupVersionKind(ctr.Params.Arguments)
67+
if err != nil {
68+
return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil
69+
}
70+
ret, err := k.ResourcesList(ctx, gvk, namespace.(string))
71+
if err != nil {
72+
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
73+
}
74+
return NewTextResult(ret, err), nil
75+
}
76+
77+
func resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
78+
k, err := kubernetes.NewKubernetes()
79+
if err != nil {
80+
return NewTextResult("", fmt.Errorf("failed to get resource: %v", err)), nil
5081
}
5182
namespace := ctr.Params.Arguments["namespace"]
5283
if namespace == nil {
5384
namespace = ""
5485
}
55-
gv, err := schema.ParseGroupVersion(apiVersion.(string))
86+
gvk, err := parseGroupVersionKind(ctr.Params.Arguments)
5687
if err != nil {
57-
return NewTextResult("", errors.New("failed to list resources, invalid argument apiVersion")), nil
88+
return NewTextResult("", fmt.Errorf("failed to get resource, %s", err)), nil
89+
}
90+
name := ctr.Params.Arguments["name"]
91+
if name == nil {
92+
return NewTextResult("", errors.New("failed to get resource, missing argument name")), nil
5893
}
59-
ret, err := k.ResourcesList(ctx, &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind.(string)}, namespace.(string))
94+
ret, err := k.ResourcesGet(ctx, gvk, namespace.(string), name.(string))
6095
if err != nil {
61-
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
96+
return NewTextResult("", fmt.Errorf("failed to get resource: %v", err)), nil
6297
}
6398
return NewTextResult(ret, err), nil
6499
}
@@ -78,3 +113,19 @@ func resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp
78113
}
79114
return NewTextResult(ret, err), nil
80115
}
116+
117+
func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) {
118+
apiVersion := arguments["apiVersion"]
119+
if apiVersion == nil {
120+
return nil, errors.New("missing argument apiVersion")
121+
}
122+
kind := arguments["kind"]
123+
if kind == nil {
124+
return nil, errors.New("missing argument kind")
125+
}
126+
gv, err := schema.ParseGroupVersion(apiVersion.(string))
127+
if err != nil {
128+
return nil, errors.New("invalid argument apiVersion")
129+
}
130+
return &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind.(string)}, nil
131+
}

pkg/mcp/resources_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,92 @@ func TestResourcesList(t *testing.T) {
8585
})
8686
}
8787

88+
func TestResourcesGet(t *testing.T) {
89+
testCase(t, func(c *mcpContext) {
90+
c.withEnvTest()
91+
t.Run("resources_get with missing apiVersion returns error", func(t *testing.T) {
92+
toolResult, _ := c.callTool("resources_get", map[string]interface{}{})
93+
if !toolResult.IsError {
94+
t.Fatalf("call tool should fail")
95+
return
96+
}
97+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, missing argument apiVersion" {
98+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
99+
return
100+
}
101+
})
102+
t.Run("resources_get with missing kind returns error", func(t *testing.T) {
103+
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "v1"})
104+
if !toolResult.IsError {
105+
t.Fatalf("call tool should fail")
106+
return
107+
}
108+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, missing argument kind" {
109+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
110+
return
111+
}
112+
})
113+
t.Run("resources_get with invalid apiVersion returns error", func(t *testing.T) {
114+
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "invalid/api/version", "kind": "Pod", "name": "a-pod"})
115+
if !toolResult.IsError {
116+
t.Fatalf("call tool should fail")
117+
return
118+
}
119+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, invalid argument apiVersion" {
120+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
121+
return
122+
}
123+
})
124+
t.Run("resources_get with nonexistent apiVersion returns error", func(t *testing.T) {
125+
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "custom.non.existent.example.com/v1", "kind": "Custom", "name": "a-custom"})
126+
if !toolResult.IsError {
127+
t.Fatalf("call tool should fail")
128+
return
129+
}
130+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != `failed to get resource: no matches for kind "Custom" in version "custom.non.existent.example.com/v1"` {
131+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
132+
return
133+
}
134+
})
135+
t.Run("resources_get with missing name returns error", func(t *testing.T) {
136+
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace"})
137+
if !toolResult.IsError {
138+
t.Fatalf("call tool should fail")
139+
return
140+
}
141+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, missing argument name" {
142+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
143+
return
144+
}
145+
})
146+
namespace, err := c.callTool("resources_get", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace", "name": "default"})
147+
t.Run("resources_get returns namespace", func(t *testing.T) {
148+
if err != nil {
149+
t.Fatalf("call tool failed %v", err)
150+
return
151+
}
152+
if namespace.IsError {
153+
t.Fatalf("call tool failed")
154+
return
155+
}
156+
})
157+
var decodedNamespace unstructured.Unstructured
158+
err = yaml.Unmarshal([]byte(namespace.Content[0].(map[string]interface{})["text"].(string)), &decodedNamespace)
159+
t.Run("resources_get has yaml content", func(t *testing.T) {
160+
if err != nil {
161+
t.Fatalf("invalid tool result content %v", err)
162+
return
163+
}
164+
})
165+
t.Run("resources_get returns default namespace", func(t *testing.T) {
166+
if decodedNamespace.GetName() != "default" {
167+
t.Fatalf("invalid namespace name, expected default, got %v", decodedNamespace.GetName())
168+
return
169+
}
170+
})
171+
})
172+
}
173+
88174
func TestResourcesCreateOrUpdate(t *testing.T) {
89175
testCase(t, func(c *mcpContext) {
90176
c.withEnvTest()

0 commit comments

Comments
 (0)