mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
feat(kubernetes): reusable Kubernetes clients
Improve cache performance
This commit is contained in:
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user