From d3754585ece5237057c87848e550f3ad115e2221 Mon Sep 17 00:00:00 2001 From: Marc Nuri Date: Thu, 20 Feb 2025 06:33:42 +0100 Subject: [PATCH] feat(kubernetes): reusable Kubernetes clients Improve cache performance --- pkg/kubernetes-mcp-server/cmd/root.go | 6 ++- pkg/kubernetes/kubernetes.go | 33 +++++++++++++- pkg/kubernetes/pods.go | 28 +++--------- pkg/kubernetes/resources.go | 54 +++++------------------ pkg/mcp/common_test.go | 33 ++++++++------ pkg/mcp/configuration.go | 2 +- pkg/mcp/mcp.go | 24 +++++++--- pkg/mcp/pods.go | 63 ++++++++------------------- pkg/mcp/pods_test.go | 4 ++ pkg/mcp/resources.go | 43 ++++++------------ 10 files changed, 131 insertions(+), 159 deletions(-) diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go index ac12e9f..2cd733c 100644 --- a/pkg/kubernetes-mcp-server/cmd/root.go +++ b/pkg/kubernetes-mcp-server/cmd/root.go @@ -28,7 +28,11 @@ Kubernetes Model Context Protocol (MCP) server fmt.Println(version.Version) return } - if err := mcp.NewSever().ServeStdio(); err != nil && !errors.Is(err, context.Canceled) { + mcpServer, err := mcp.NewSever() + if err != nil { + panic(err) + } + if err := mcpServer.ServeStdio(); err != nil && !errors.Is(err, context.Canceled) { panic(err) } }, diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index 238054b..1f67524 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -2,6 +2,10 @@ package kubernetes import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/discovery" + "k8s.io/client-go/discovery/cached/memory" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/client-go/tools/clientcmd" @@ -11,7 +15,10 @@ import ( type Kubernetes struct { cfg *rest.Config + clientSet *kubernetes.Clientset + discoveryClient *discovery.DiscoveryClient deferredDiscoveryRESTMapper *restmapper.DeferredDiscoveryRESTMapper + dynamicClient *dynamic.DynamicClient } func NewKubernetes() (*Kubernetes, error) { @@ -19,7 +26,25 @@ func NewKubernetes() (*Kubernetes, error) { if err != nil { return nil, err } - return &Kubernetes{cfg: cfg}, nil + clientSet, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, err + } + discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg) + if err != nil { + return nil, err + } + dynamicClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return nil, err + } + return &Kubernetes{ + cfg: cfg, + clientSet: clientSet, + discoveryClient: discoveryClient, + deferredDiscoveryRESTMapper: restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient)), + dynamicClient: dynamicClient, + }, nil } func marshal(v any) (string, error) { @@ -28,8 +53,14 @@ func marshal(v any) (string, error) { for i := range t { t[i].SetManagedFields(nil) } + case []*unstructured.Unstructured: + for i := range t { + t[i].SetManagedFields(nil) + } case unstructured.Unstructured: t.SetManagedFields(nil) + case *unstructured.Unstructured: + t.SetManagedFields(nil) } ret, err := yaml.Marshal(v) if err != nil { diff --git a/pkg/kubernetes/pods.go b/pkg/kubernetes/pods.go index e994edd..570cd60 100644 --- a/pkg/kubernetes/pods.go +++ b/pkg/kubernetes/pods.go @@ -11,8 +11,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/rand" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" ) func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context) (string, error) { @@ -34,13 +32,8 @@ func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (strin } func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) { - cs, err := kubernetes.NewForConfig(k.cfg) - if err != nil { - return "", err - } - namespace = namespaceOrDefault(namespace) - pod, err := cs.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) + pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return "", err } @@ -53,22 +46,18 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st // Delete managed service if isManaged { - if sl, _ := cs.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{ + if sl, _ := k.clientSet.CoreV1().Services(namespace).List(ctx, metav1.ListOptions{ LabelSelector: managedLabelSelector.String(), }); sl != nil { for _, svc := range sl.Items { - _ = cs.CoreV1().Services(namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{}) + _ = k.clientSet.CoreV1().Services(namespace).Delete(ctx, svc.Name, metav1.DeleteOptions{}) } } } // Delete managed Route if isManaged && k.supportsGroupVersion("route.openshift.io/v1") { - dynamicClient, dErr := dynamic.NewForConfig(k.cfg) - if dErr != nil { - return "", dErr - } - routeResources := dynamicClient. + routeResources := k.dynamicClient. Resource(schema.GroupVersionResource{Group: "route.openshift.io", Version: "v1", Resource: "routes"}). Namespace(namespace) if rl, _ := routeResources.List(ctx, metav1.ListOptions{ @@ -80,16 +69,13 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st } } - return "Pod deleted successfully", cs.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + return "Pod deleted successfully", + k.clientSet.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{}) } func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name string) (string, error) { - cs, err := kubernetes.NewForConfig(k.cfg) - if err != nil { - return "", err - } tailLines := int64(256) - req := cs.CoreV1().Pods(namespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{ + req := k.clientSet.CoreV1().Pods(namespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{ TailLines: &tailLines, }) res := req.Do(ctx) diff --git a/pkg/kubernetes/resources.go b/pkg/kubernetes/resources.go index 76b1fce..c5b58b4 100644 --- a/pkg/kubernetes/resources.go +++ b/pkg/kubernetes/resources.go @@ -7,10 +7,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/yaml" - "k8s.io/client-go/discovery" - memory "k8s.io/client-go/discovery/cached" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/restmapper" "regexp" "strings" ) @@ -23,15 +19,11 @@ const ( ) func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string) (string, error) { - client, err := dynamic.NewForConfig(k.cfg) - if err != nil { - return "", err - } gvr, err := k.resourceFor(gvk) if err != nil { return "", err } - rl, err := client.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) + rl, err := k.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, metav1.ListOptions{}) if err != nil { return "", err } @@ -39,10 +31,6 @@ func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersion } func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (string, error) { - client, err := dynamic.NewForConfig(k.cfg) - if err != nil { - return "", err - } gvr, err := k.resourceFor(gvk) if err != nil { return "", err @@ -51,7 +39,7 @@ func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionK if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { namespace = namespaceOrDefault(namespace) } - rg, err := client.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) + rg, err := k.dynamicClient.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return "", err } @@ -73,10 +61,6 @@ func (k *Kubernetes) ResourcesCreateOrUpdate(ctx context.Context, resource strin } func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error { - client, err := dynamic.NewForConfig(k.cfg) - if err != nil { - return err - } gvr, err := k.resourceFor(gvk) if err != nil { return err @@ -85,14 +69,10 @@ func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersi if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { namespace = namespaceOrDefault(namespace) } - return client.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + return k.dynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) } func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*unstructured.Unstructured) (string, error) { - client, err := dynamic.NewForConfig(k.cfg) - if err != nil { - return "", err - } for i, obj := range resources { gvk := obj.GroupVersionKind() gvr, rErr := k.resourceFor(&gvk) @@ -104,24 +84,21 @@ func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*u if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced { namespace = namespaceOrDefault(namespace) } - resources[i], rErr = client.Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{ + resources[i], rErr = k.dynamicClient.Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{ FieldManager: version.BinaryName, }) if rErr != nil { return "", rErr } + // Clear the cache to ensure the next operation is performed on the latest exposed APIs + if gvk.Kind == "CustomResourceDefinition" { + k.deferredDiscoveryRESTMapper.Reset() + } } return marshal(resources) } func (k *Kubernetes) resourceFor(gvk *schema.GroupVersionKind) (*schema.GroupVersionResource, error) { - if k.deferredDiscoveryRESTMapper == nil { - d, err := discovery.NewDiscoveryClientForConfig(k.cfg) - if err != nil { - return nil, err - } - k.deferredDiscoveryRESTMapper = restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(d)) - } m, err := k.deferredDiscoveryRESTMapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version) if err != nil { return nil, err @@ -130,11 +107,7 @@ func (k *Kubernetes) resourceFor(gvk *schema.GroupVersionKind) (*schema.GroupVer } func (k *Kubernetes) isNamespaced(gvk *schema.GroupVersionKind) (bool, error) { - d, err := discovery.NewDiscoveryClientForConfig(k.cfg) - if err != nil { - return false, err - } - apiResourceList, err := d.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) + apiResourceList, err := k.discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { return false, err } @@ -147,13 +120,8 @@ func (k *Kubernetes) isNamespaced(gvk *schema.GroupVersionKind) (bool, error) { } func (k *Kubernetes) supportsGroupVersion(groupVersion string) bool { - d, err := discovery.NewDiscoveryClientForConfig(k.cfg) - if err != nil { + if _, err := k.discoveryClient.ServerResourcesForGroupVersion(groupVersion); err != nil { return false } - _, err = d.ServerResourcesForGroupVersion(groupVersion) - if err == nil { - return true - } - return false + return true } diff --git a/pkg/mcp/common_test.go b/pkg/mcp/common_test.go index 5661630..d0bc8e2 100644 --- a/pkg/mcp/common_test.go +++ b/pkg/mcp/common_test.go @@ -77,23 +77,29 @@ func TestMain(m *testing.M) { } type mcpContext struct { - ctx context.Context - tempDir string - testServer *httptest.Server - cancel context.CancelFunc - mcpClient *client.SSEMCPClient + ctx context.Context + tempDir string + cancel context.CancelFunc + mcpServer *Server + mcpHttpServer *httptest.Server + mcpClient *client.SSEMCPClient } func (c *mcpContext) beforeEach(t *testing.T) { var err error c.ctx, c.cancel = context.WithCancel(context.Background()) c.tempDir = t.TempDir() - c.withKubeConfig(nil) - c.testServer = server.NewTestServer(NewSever().server) - if c.mcpClient, err = client.NewSSEMCPClient(c.testServer.URL + "/sse"); err != nil { + _ = os.Unsetenv("KUBECONFIG") + if c.mcpServer, err = NewSever(); err != nil { t.Fatal(err) return } + c.mcpHttpServer = server.NewTestServer(c.mcpServer.server) + if c.mcpClient, err = client.NewSSEMCPClient(c.mcpHttpServer.URL + "/sse"); err != nil { + t.Fatal(err) + return + } + c.withKubeConfig(nil) if err = c.mcpClient.Start(c.ctx); err != nil { t.Fatal(err) return @@ -111,7 +117,7 @@ func (c *mcpContext) beforeEach(t *testing.T) { func (c *mcpContext) afterEach() { c.cancel() _ = c.mcpClient.Close() - c.testServer.Close() + c.mcpHttpServer.Close() } func testCase(t *testing.T, test func(c *mcpContext)) { @@ -140,6 +146,9 @@ func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config { kubeConfig := filepath.Join(c.tempDir, "config") _ = clientcmd.WriteToFile(*fakeConfig, kubeConfig) _ = os.Setenv("KUBECONFIG", kubeConfig) + if err := c.mcpServer.reloadKubernetesClient(); err != nil { + panic(err) + } return fakeConfig } @@ -168,11 +177,9 @@ func (c *mcpContext) inOpenShift() func() { }`) } -// newKubernetesClient creates a new Kubernetes client with the current kubeconfig +// newKubernetesClient creates a new Kubernetes client with the envTest kubeconfig func (c *mcpContext) newKubernetesClient() *kubernetes.Clientset { - c.withEnvTest() - cfg, _ := clientcmd.BuildConfigFromFlags("", clientcmd.NewDefaultPathOptions().GetDefaultFilename()) - return kubernetes.NewForConfigOrDie(cfg) + return kubernetes.NewForConfigOrDie(envTestRestConfig) } // newApiExtensionsClient creates a new ApiExtensions client with the envTest kubeconfig diff --git a/pkg/mcp/configuration.go b/pkg/mcp/configuration.go index a8c235a..940a5df 100644 --- a/pkg/mcp/configuration.go +++ b/pkg/mcp/configuration.go @@ -7,7 +7,7 @@ import ( "github.com/mark3labs/mcp-go/mcp" ) -func (s *Sever) initConfiguration() { +func (s *Server) initConfiguration() { s.server.AddTool(mcp.NewTool( "configuration_view", mcp.WithDescription("Get the current Kubernetes configuration content as a kubeconfig YAML"), diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 456938e..3f4d674 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -1,17 +1,19 @@ package mcp import ( + "github.com/manusa/kubernetes-mcp-server/pkg/kubernetes" "github.com/manusa/kubernetes-mcp-server/pkg/version" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) -type Sever struct { +type Server struct { server *server.MCPServer + k *kubernetes.Kubernetes } -func NewSever() *Sever { - s := &Sever{ +func NewSever() (*Server, error) { + s := &Server{ server: server.NewMCPServer( version.BinaryName, version.Version, @@ -20,13 +22,25 @@ func NewSever() *Sever { server.WithLogging(), ), } + if err := s.reloadKubernetesClient(); err != nil { + return nil, err + } s.initConfiguration() s.initPods() s.initResources() - return s + return s, nil } -func (s *Sever) ServeStdio() error { +func (s *Server) reloadKubernetesClient() error { + k, err := kubernetes.NewKubernetes() + if err != nil { + return err + } + s.k = k + return nil +} + +func (s *Server) ServeStdio() error { return server.ServeStdio(s.server) } diff --git a/pkg/mcp/pods.go b/pkg/mcp/pods.go index 6ff2434..6b2b2e5 100644 --- a/pkg/mcp/pods.go +++ b/pkg/mcp/pods.go @@ -4,15 +4,14 @@ import ( "context" "errors" "fmt" - "github.com/manusa/kubernetes-mcp-server/pkg/kubernetes" "github.com/mark3labs/mcp-go/mcp" ) -func (s *Sever) initPods() { +func (s *Server) initPods() { s.server.AddTool(mcp.NewTool( "pods_list", mcp.WithDescription("List all the Kubernetes pods in the current cluster from all namespaces"), - ), podsListInAllNamespaces) + ), s.podsListInAllNamespaces) s.server.AddTool(mcp.NewTool( "pods_list_in_namespace", mcp.WithDescription("List all the Kubernetes pods in the specified namespace in the current cluster"), @@ -20,7 +19,7 @@ func (s *Sever) initPods() { mcp.Description("Namespace to list pods from"), mcp.Required(), ), - ), podsListInNamespace) + ), s.podsListInNamespace) s.server.AddTool(mcp.NewTool( "pods_get", mcp.WithDescription("Get a Kubernetes Pod in the current or provided namespace with the provided name"), @@ -31,7 +30,7 @@ func (s *Sever) initPods() { mcp.Description("Name of the Pod"), mcp.Required(), ), - ), podsGet) + ), s.podsGet) s.server.AddTool(mcp.NewTool( "pods_delete", mcp.WithDescription("Delete a Kubernetes Pod in the current or provided namespace with the provided name"), @@ -42,7 +41,7 @@ func (s *Sever) initPods() { mcp.Description("Name of the Pod to delete"), mcp.Required(), ), - ), podsDelete) + ), s.podsDelete) s.server.AddTool(mcp.NewTool( "pods_log", mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"), @@ -53,7 +52,7 @@ func (s *Sever) initPods() { mcp.Description("Name of the Pod to get the logs from"), mcp.Required(), ), - ), podsLog) + ), s.podsLog) s.server.AddTool(mcp.NewTool( "pods_run", mcp.WithDescription("Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name"), @@ -70,42 +69,30 @@ func (s *Sever) initPods() { mcp.WithNumber("port", mcp.Description("TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)"), ), - ), podsRun) + ), s.podsRun) } -func podsListInAllNamespaces(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil - } - ret, err := k.PodsListInAllNamespaces(ctx) +func (s *Server) podsListInAllNamespaces(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + ret, err := s.k.PodsListInAllNamespaces(ctx) if err != nil { return NewTextResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil } return NewTextResult(ret, err), nil } -func podsListInNamespace(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to list pods in namespace: %v", err)), nil - } +func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { ns := ctr.Params.Arguments["namespace"] if ns == nil { return NewTextResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil } - ret, err := k.PodsListInNamespace(ctx, ns.(string)) + ret, err := s.k.PodsListInNamespace(ctx, ns.(string)) if err != nil { return NewTextResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil } return NewTextResult(ret, err), nil } -func podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to get pod: %v", err)), nil - } +func (s *Server) podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { ns := ctr.Params.Arguments["namespace"] if ns == nil { ns = "" @@ -114,18 +101,14 @@ func podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, if name == nil { return NewTextResult("", errors.New("failed to get pod, missing argument name")), nil } - ret, err := k.PodsGet(ctx, ns.(string), name.(string)) + ret, err := s.k.PodsGet(ctx, ns.(string), name.(string)) if err != nil { return NewTextResult("", fmt.Errorf("failed to get pod %s in namespace %s: %v", name, ns, err)), nil } return NewTextResult(ret, err), nil } -func podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to delete pod: %v", err)), nil - } +func (s *Server) podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { ns := ctr.Params.Arguments["namespace"] if ns == nil { ns = "" @@ -134,18 +117,14 @@ func podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResu if name == nil { return NewTextResult("", errors.New("failed to delete pod, missing argument name")), nil } - ret, err := k.PodsDelete(ctx, ns.(string), name.(string)) + ret, err := s.k.PodsDelete(ctx, ns.(string), name.(string)) if err != nil { return NewTextResult("", fmt.Errorf("failed to delete pod %s in namespace %s: %v", name, ns, err)), nil } return NewTextResult(ret, err), nil } -func podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to get pod log: %v", err)), nil - } +func (s *Server) podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { ns := ctr.Params.Arguments["namespace"] if ns == nil { ns = "" @@ -154,18 +133,14 @@ func podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, if name == nil { return NewTextResult("", errors.New("failed to get pod log, missing argument name")), nil } - ret, err := k.PodsLog(ctx, ns.(string), name.(string)) + ret, err := s.k.PodsLog(ctx, ns.(string), name.(string)) if err != nil { return NewTextResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil } return NewTextResult(ret, err), nil } -func podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to run pod: %v", err)), nil - } +func (s *Server) podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { ns := ctr.Params.Arguments["namespace"] if ns == nil { ns = "" @@ -182,7 +157,7 @@ func podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, if port == nil { port = float64(0) } - ret, err := k.PodsRun(ctx, ns.(string), name.(string), image.(string), int32(port.(float64))) + ret, err := s.k.PodsRun(ctx, ns.(string), name.(string), image.(string), int32(port.(float64))) if err != nil { return NewTextResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil } diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go index 05d977d..2ac2d04 100644 --- a/pkg/mcp/pods_test.go +++ b/pkg/mcp/pods_test.go @@ -20,6 +20,10 @@ func TestPodsListInAllNamespaces(t *testing.T) { t.Fatalf("call tool failed %v", err) return } + if toolResult.IsError { + t.Fatalf("call tool failed") + return + } }) var decoded []unstructured.Unstructured err = yaml.Unmarshal([]byte(toolResult.Content[0].(map[string]interface{})["text"].(string)), &decoded) diff --git a/pkg/mcp/resources.go b/pkg/mcp/resources.go index 0986531..06e8f0f 100644 --- a/pkg/mcp/resources.go +++ b/pkg/mcp/resources.go @@ -4,12 +4,11 @@ import ( "context" "errors" "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() { +func (s *Server) 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"), @@ -24,7 +23,7 @@ func (s *Sever) initResources() { 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.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"), @@ -43,7 +42,7 @@ func (s *Sever) initResources() { mcp.Description("Name of the resource"), mcp.Required(), ), - ), resourcesGet) + ), s.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"), @@ -51,7 +50,7 @@ func (s *Sever) initResources() { mcp.Description("A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec"), mcp.Required(), ), - ), resourcesCreateOrUpdate) + ), s.resourcesCreateOrUpdate) s.server.AddTool(mcp.NewTool( "resources_delete", mcp.WithDescription("Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name"), @@ -70,14 +69,10 @@ func (s *Sever) initResources() { mcp.Description("Name of the resource"), mcp.Required(), ), - ), resourcesDelete) + ), s.resourcesDelete) } -func resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil - } +func (s *Server) resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := ctr.Params.Arguments["namespace"] if namespace == nil { namespace = "" @@ -86,18 +81,14 @@ func resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolR if err != nil { return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil } - ret, err := k.ResourcesList(ctx, gvk, namespace.(string)) + ret, err := s.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 - } +func (s *Server) resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := ctr.Params.Arguments["namespace"] if namespace == nil { namespace = "" @@ -110,34 +101,26 @@ func resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolRe 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)) + ret, err := s.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 { - return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil - } +func (s *Server) resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { resource := ctr.Params.Arguments["resource"] if resource == nil || resource == "" { return NewTextResult("", errors.New("failed to create or update resources, missing argument resource")), nil } - ret, err := k.ResourcesCreateOrUpdate(ctx, resource.(string)) + ret, err := s.k.ResourcesCreateOrUpdate(ctx, resource.(string)) if err != nil { return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil } return NewTextResult(ret, err), nil } -func resourcesDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { - k, err := kubernetes.NewKubernetes() - if err != nil { - return NewTextResult("", fmt.Errorf("failed to delete resource: %v", err)), nil - } +func (s *Server) resourcesDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { namespace := ctr.Params.Arguments["namespace"] if namespace == nil { namespace = "" @@ -150,7 +133,7 @@ func resourcesDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToo if name == nil { return NewTextResult("", errors.New("failed to delete resource, missing argument name")), nil } - err = k.ResourcesDelete(ctx, gvk, namespace.(string), name.(string)) + err = s.k.ResourcesDelete(ctx, gvk, namespace.(string), name.(string)) if err != nil { return NewTextResult("", fmt.Errorf("failed to delete resource: %v", err)), nil }