mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
feat(kubernetes): resources_list can list any resource in the cluster
This commit is contained in:
2
go.mod
2
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/spf13/viper v1.19.0
|
||||
golang.org/x/net v0.33.0
|
||||
k8s.io/api v0.32.1
|
||||
k8s.io/apiextensions-apiserver v0.32.0
|
||||
k8s.io/apimachinery v0.32.1
|
||||
k8s.io/client-go v0.32.1
|
||||
sigs.k8s.io/controller-runtime v0.20.1
|
||||
@@ -63,7 +64,6 @@ require (
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.32.0 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
|
||||
@@ -22,7 +22,6 @@ const (
|
||||
AppKubernetesPartOf = "app.kubernetes.io/part-of"
|
||||
)
|
||||
|
||||
// TODO: WIP
|
||||
func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string) (string, error) {
|
||||
client, err := dynamic.NewForConfig(k.cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -6,9 +6,25 @@ import (
|
||||
"fmt"
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func (s *Sever) initResources() {
|
||||
s.server.AddTool(mcp.NewTool(
|
||||
"resources_list",
|
||||
mcp.WithDescription("List Kubernetes resources in the current cluster by providing their apiVersion and kind and optionally the namespace"),
|
||||
mcp.WithString("apiVersion",
|
||||
mcp.Description("apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString("kind",
|
||||
mcp.Description("kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)"),
|
||||
mcp.Required(),
|
||||
),
|
||||
mcp.WithString("namespace",
|
||||
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_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"),
|
||||
@@ -19,6 +35,34 @@ func (s *Sever) initResources() {
|
||||
), resourcesCreateOrUpdate)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
namespace := ctr.Params.Arguments["namespace"]
|
||||
if namespace == nil {
|
||||
namespace = ""
|
||||
}
|
||||
gv, err := schema.ParseGroupVersion(apiVersion.(string))
|
||||
if err != nil {
|
||||
return NewTextResult("", errors.New("failed to list resources, invalid argument apiVersion")), nil
|
||||
}
|
||||
ret, err := k.ResourcesList(ctx, &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind.(string)}, namespace.(string))
|
||||
if err != nil {
|
||||
return NewTextResult("", fmt.Errorf("failed to list resources: %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 {
|
||||
|
||||
@@ -3,11 +3,88 @@ package mcp
|
||||
import (
|
||||
v1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"sigs.k8s.io/yaml"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourcesList(t *testing.T) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
c.withEnvTest()
|
||||
t.Run("resources_list with missing apiVersion returns error", func(t *testing.T) {
|
||||
toolResult, _ := c.callTool("resources_list", map[string]interface{}{})
|
||||
if !toolResult.IsError {
|
||||
t.Fatalf("call tool should fail")
|
||||
return
|
||||
}
|
||||
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to list resources, missing argument apiVersion" {
|
||||
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("resources_list with missing kind returns error", func(t *testing.T) {
|
||||
toolResult, _ := c.callTool("resources_list", 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 list resources, missing argument kind" {
|
||||
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("resources_list with invalid apiVersion returns error", func(t *testing.T) {
|
||||
toolResult, _ := c.callTool("resources_list", map[string]interface{}{"apiVersion": "invalid/api/version", "kind": "Pod"})
|
||||
if !toolResult.IsError {
|
||||
t.Fatalf("call tool should fail")
|
||||
return
|
||||
}
|
||||
if toolResult.Content[0].(map[string]interface{})["text"].(string) != "failed to list resources, invalid argument apiVersion" {
|
||||
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("resources_list with nonexistent apiVersion returns error", func(t *testing.T) {
|
||||
toolResult, _ := c.callTool("resources_list", map[string]interface{}{"apiVersion": "custom.non.existent.example.com/v1", "kind": "Custom"})
|
||||
if !toolResult.IsError {
|
||||
t.Fatalf("call tool should fail")
|
||||
return
|
||||
}
|
||||
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"` {
|
||||
t.Fatalf("invalid error message, got %v", toolResult.Content[0].(map[string]interface{})["text"].(string))
|
||||
return
|
||||
}
|
||||
})
|
||||
namespaces, err := c.callTool("resources_list", map[string]interface{}{"apiVersion": "v1", "kind": "Namespace"})
|
||||
t.Run("resources_list returns namespaces", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
return
|
||||
}
|
||||
if namespaces.IsError {
|
||||
t.Fatalf("call tool failed")
|
||||
return
|
||||
}
|
||||
})
|
||||
var decodedNamespaces []unstructured.Unstructured
|
||||
err = yaml.Unmarshal([]byte(namespaces.Content[0].(map[string]interface{})["text"].(string)), &decodedNamespaces)
|
||||
t.Run("resources_list has yaml content", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("invalid tool result content %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
t.Run("resources_list returns more than 2 items", func(t *testing.T) {
|
||||
if len(decodedNamespaces) < 3 {
|
||||
t.Fatalf("invalid namespace count, expected >2, got %v", len(decodedNamespaces))
|
||||
return
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourcesCreateOrUpdate(t *testing.T) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
c.withEnvTest()
|
||||
|
||||
Reference in New Issue
Block a user