feat(kubernetes): reusable Kubernetes clients

Improve cache performance
This commit is contained in:
Marc Nuri
2025-02-20 06:33:42 +01:00
parent 40ff50e04d
commit d3754585ec
10 changed files with 131 additions and 159 deletions

View File

@@ -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)
}
},

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"),

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}