feat(kubernetes): resources_get can get any resource in the cluster

This commit is contained in:
Marc Nuri
2025-02-17 12:49:27 +01:00
parent b91f948cb4
commit 3ea23f3d61
2 changed files with 149 additions and 12 deletions

View File

@@ -25,6 +25,25 @@ func (s *Sever) initResources() {
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"),
),
), resourcesList)
s.server.AddTool(mcp.NewTool(
"resources_get",
mcp.WithDescription("Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name"),
mcp.WithString("apiVersion",
mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
mcp.Required(),
),
mcp.WithString("kind",
mcp.Description("kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)"),
mcp.Required(),
),
mcp.WithString("namespace",
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"),
),
mcp.WithString("name",
mcp.Description("Name of the resource"),
mcp.Required(),
),
), resourcesGet)
s.server.AddTool(mcp.NewTool(
"resources_create_or_update",
mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource"),
@@ -38,31 +57,47 @@ func (s *Sever) initResources() {
func resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
k, err := kubernetes.NewKubernetes()
if err != nil {
return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil
}
apiVersion := ctr.Params.Arguments["apiVersion"]
if apiVersion == nil {
return NewTextResult("", errors.New("failed to list resources, missing argument apiVersion")), nil
}
kind := ctr.Params.Arguments["kind"]
if kind == nil {
return NewTextResult("", errors.New("failed to list resources, missing argument kind")), nil
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
}
namespace := ctr.Params.Arguments["namespace"]
if namespace == nil {
namespace = ""
}
gv, err := schema.ParseGroupVersion(apiVersion.(string))
gvk, err := parseGroupVersionKind(ctr.Params.Arguments)
if err != nil {
return NewTextResult("", errors.New("failed to list resources, invalid argument apiVersion")), nil
return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil
}
ret, err := k.ResourcesList(ctx, &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind.(string)}, namespace.(string))
ret, err := k.ResourcesList(ctx, gvk, namespace.(string))
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
}
return NewTextResult(ret, err), nil
}
func resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
k, err := kubernetes.NewKubernetes()
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get resource: %v", err)), nil
}
namespace := ctr.Params.Arguments["namespace"]
if namespace == nil {
namespace = ""
}
gvk, err := parseGroupVersionKind(ctr.Params.Arguments)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get resource, %s", err)), nil
}
name := ctr.Params.Arguments["name"]
if name == nil {
return NewTextResult("", errors.New("failed to get resource, missing argument name")), nil
}
ret, err := k.ResourcesGet(ctx, gvk, namespace.(string), name.(string))
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get resource: %v", err)), nil
}
return NewTextResult(ret, err), nil
}
func resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
k, err := kubernetes.NewKubernetes()
if err != nil {
@@ -78,3 +113,19 @@ func resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp
}
return NewTextResult(ret, err), nil
}
func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) {
apiVersion := arguments["apiVersion"]
if apiVersion == nil {
return nil, errors.New("missing argument apiVersion")
}
kind := arguments["kind"]
if kind == nil {
return nil, errors.New("missing argument kind")
}
gv, err := schema.ParseGroupVersion(apiVersion.(string))
if err != nil {
return nil, errors.New("invalid argument apiVersion")
}
return &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind.(string)}, nil
}

View File

@@ -85,6 +85,92 @@ func TestResourcesList(t *testing.T) {
})
}
func TestResourcesGet(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()
t.Run("resources_get with missing apiVersion returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_get", map[string]interface{}{})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, missing argument apiVersion" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_get with missing kind returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "v1"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, missing argument kind" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_get with invalid apiVersion returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "invalid/api/version", "kind": "Pod", "name": "a-pod"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, invalid argument apiVersion" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_get with nonexistent apiVersion returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "custom.non.existent.example.com/v1", "kind": "Custom", "name": "a-custom"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
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"` {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
t.Run("resources_get with missing name returns error", func(t *testing.T) {
toolResult, _ := c.callTool("resources_get", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace"})
if !toolResult.IsError {
t.Fatalf("call tool should fail")
return
}
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to get resource, missing argument name" {
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
return
}
})
namespace, err := c.callTool("resources_get", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace", "name": "default"})
t.Run("resources_get returns namespace", func(t *testing.T) {
if err != nil {
t.Fatalf("call tool failed %v", err)
return
}
if namespace.IsError {
t.Fatalf("call tool failed")
return
}
})
var decodedNamespace unstructured.Unstructured
err = yaml.Unmarshal([]byte(namespace.Content[0].(map[string]interface{})["text"].(string)), &decodedNamespace)
t.Run("resources_get has yaml content", func(t *testing.T) {
if err != nil {
t.Fatalf("invalid tool result content %v", err)
return
}
})
t.Run("resources_get returns default namespace", func(t *testing.T) {
if decodedNamespace.GetName() != "default" {
t.Fatalf("invalid namespace name, expected default, got %v", decodedNamespace.GetName())
return
}
})
})
}
func TestResourcesCreateOrUpdate(t *testing.T) {
testCase(t, func(c *mcpContext) {
c.withEnvTest()