Skip to content

Commit b91f948

Browse files
committed
feat(kubernetes): resources_list can list any resource in the cluster
1 parent 6ae9247 commit b91f948

File tree

4 files changed

+122
-2
lines changed

4 files changed

+122
-2
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/spf13/viper v1.19.0
1010
golang.org/x/net v0.33.0
1111
k8s.io/api v0.32.1
12+
k8s.io/apiextensions-apiserver v0.32.0
1213
k8s.io/apimachinery v0.32.1
1314
k8s.io/client-go v0.32.1
1415
sigs.k8s.io/controller-runtime v0.20.1
@@ -63,7 +64,6 @@ require (
6364
gopkg.in/inf.v0 v0.9.1 // indirect
6465
gopkg.in/ini.v1 v1.67.0 // indirect
6566
gopkg.in/yaml.v3 v3.0.1 // indirect
66-
k8s.io/apiextensions-apiserver v0.32.0 // indirect
6767
k8s.io/klog/v2 v2.130.1 // indirect
6868
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
6969
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect

pkg/kubernetes/resources.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const (
2222
AppKubernetesPartOf = "app.kubernetes.io/part-of"
2323
)
2424

25-
// TODO: WIP
2625
func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string) (string, error) {
2726
client, err := dynamic.NewForConfig(k.cfg)
2827
if err != nil {

pkg/mcp/resources.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,25 @@ import (
66
"fmt"
77
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
88
"github.com/mark3labs/mcp-go/mcp"
9+
"k8s.io/apimachinery/pkg/runtime/schema"
910
)
1011

1112
func (s *Sever) initResources() {
13+
s.server.AddTool(mcp.NewTool(
14+
"resources_list",
15+
mcp.WithDescription("List Kubernetes resources in the current cluster by providing their apiVersion and kind and optionally the namespace"),
16+
mcp.WithString("apiVersion",
17+
mcp.Description("apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
18+
mcp.Required(),
19+
),
20+
mcp.WithString("kind",
21+
mcp.Description("kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)"),
22+
mcp.Required(),
23+
),
24+
mcp.WithString("namespace",
25+
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"),
26+
),
27+
), resourcesList)
1228
s.server.AddTool(mcp.NewTool(
1329
"resources_create_or_update",
1430
mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource"),
@@ -19,6 +35,34 @@ func (s *Sever) initResources() {
1935
), resourcesCreateOrUpdate)
2036
}
2137

38+
func resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
39+
k, err := kubernetes.NewKubernetes()
40+
if err != nil {
41+
return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil
42+
}
43+
apiVersion := ctr.Params.Arguments["apiVersion"]
44+
if apiVersion == nil {
45+
return NewTextResult("", errors.New("failed to list resources, missing argument apiVersion")), nil
46+
}
47+
kind := ctr.Params.Arguments["kind"]
48+
if kind == nil {
49+
return NewTextResult("", errors.New("failed to list resources, missing argument kind")), nil
50+
}
51+
namespace := ctr.Params.Arguments["namespace"]
52+
if namespace == nil {
53+
namespace = ""
54+
}
55+
gv, err := schema.ParseGroupVersion(apiVersion.(string))
56+
if err != nil {
57+
return NewTextResult("", errors.New("failed to list resources, invalid argument apiVersion")), nil
58+
}
59+
ret, err := k.ResourcesList(ctx, &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind.(string)}, namespace.(string))
60+
if err != nil {
61+
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
62+
}
63+
return NewTextResult(ret, err), nil
64+
}
65+
2266
func resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
2367
k, err := kubernetes.NewKubernetes()
2468
if err != nil {

pkg/mcp/resources_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,88 @@ package mcp
33
import (
44
v1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
55
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
6+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
67
"k8s.io/apimachinery/pkg/runtime/schema"
78
"k8s.io/client-go/dynamic"
9+
"sigs.k8s.io/yaml"
810
"testing"
911
)
1012

13+
func TestResourcesList(t *testing.T) {
14+
testCase(t, func(c *mcpContext) {
15+
c.withEnvTest()
16+
t.Run("resources_list with missing apiVersion returns error", func(t *testing.T) {
17+
toolResult, _ := c.callTool("resources_list", map[string]interface{}{})
18+
if !toolResult.IsError {
19+
t.Fatalf("call tool should fail")
20+
return
21+
}
22+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to list resources, missing argument apiVersion" {
23+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
24+
return
25+
}
26+
})
27+
t.Run("resources_list with missing kind returns error", func(t *testing.T) {
28+
toolResult, _ := c.callTool("resources_list", map[string]interface{}{"apiVersion": "v1"})
29+
if !toolResult.IsError {
30+
t.Fatalf("call tool should fail")
31+
return
32+
}
33+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to list resources, missing argument kind" {
34+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
35+
return
36+
}
37+
})
38+
t.Run("resources_list with invalid apiVersion returns error", func(t *testing.T) {
39+
toolResult, _ := c.callTool("resources_list", map[string]interface{}{"apiVersion": "invalid/api/version", "kind": "Pod"})
40+
if !toolResult.IsError {
41+
t.Fatalf("call tool should fail")
42+
return
43+
}
44+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to list resources, invalid argument apiVersion" {
45+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
46+
return
47+
}
48+
})
49+
t.Run("resources_list with nonexistent apiVersion returns error", func(t *testing.T) {
50+
toolResult, _ := c.callTool("resources_list", map[string]interface{}{"apiVersion": "custom.non.existent.example.com/v1", "kind": "Custom"})
51+
if !toolResult.IsError {
52+
t.Fatalf("call tool should fail")
53+
return
54+
}
55+
if toolResult.Content[0].(map[string]interface{})["text"].(string) != `failed to list resources: no matches for kind "Custom" in version "custom.non.existent.example.com/v1"` {
56+
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
57+
return
58+
}
59+
})
60+
namespaces, err := c.callTool("resources_list", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace"})
61+
t.Run("resources_list returns namespaces", func(t *testing.T) {
62+
if err != nil {
63+
t.Fatalf("call tool failed %v", err)
64+
return
65+
}
66+
if namespaces.IsError {
67+
t.Fatalf("call tool failed")
68+
return
69+
}
70+
})
71+
var decodedNamespaces []unstructured.Unstructured
72+
err = yaml.Unmarshal([]byte(namespaces.Content[0].(map[string]interface{})["text"].(string)), &decodedNamespaces)
73+
t.Run("resources_list has yaml content", func(t *testing.T) {
74+
if err != nil {
75+
t.Fatalf("invalid tool result content %v", err)
76+
return
77+
}
78+
})
79+
t.Run("resources_list returns more than 2 items", func(t *testing.T) {
80+
if len(decodedNamespaces) < 3 {
81+
t.Fatalf("invalid namespace count, expected >2, got %v", len(decodedNamespaces))
82+
return
83+
}
84+
})
85+
})
86+
}
87+
1188
func TestResourcesCreateOrUpdate(t *testing.T) {
1289
testCase(t, func(c *mcpContext) {
1390
c.withEnvTest()

0 commit comments

Comments
 (0)