feat(mcp): toolset definitions completely agnostic from underlying MCP impl (#322)

Signed-off-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
Marc Nuri
2025-09-12 11:56:22 +02:00
committed by GitHub
parent 2b6c886d95
commit 209e8434d5
21 changed files with 612 additions and 537 deletions

View File

@@ -14,8 +14,6 @@ import (
"testing"
"time"
"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/containers/kubernetes-mcp-server/pkg/output"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/client/transport"
"github.com/mark3labs/mcp-go/mcp"
@@ -32,7 +30,7 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
toolswatch "k8s.io/client-go/tools/watch"
"k8s.io/klog/v2"
"k8s.io/klog/v2/textlogger"
@@ -43,6 +41,11 @@ import (
"sigs.k8s.io/controller-runtime/tools/setup-envtest/store"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/workflows"
"github.com/containers/kubernetes-mcp-server/pkg/api"
"github.com/containers/kubernetes-mcp-server/pkg/config"
"github.com/containers/kubernetes-mcp-server/pkg/output"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/full"
)
// envTest has an expensive setup, so we only want to do it once per entire test run.
@@ -103,7 +106,7 @@ func TestMain(m *testing.M) {
}
type mcpContext struct {
toolset Toolset
toolset api.Toolset
listOutput output.Output
logLevel int
@@ -127,7 +130,7 @@ func (c *mcpContext) beforeEach(t *testing.T) {
c.tempDir = t.TempDir()
c.withKubeConfig(nil)
if c.toolset == nil {
c.toolset = &Full{}
c.toolset = &full.Full{}
}
if c.listOutput == nil {
c.listOutput = output.Yaml
@@ -188,7 +191,7 @@ func (c *mcpContext) afterEach() {
}
func testCase(t *testing.T, test func(c *mcpContext)) {
testCaseWithContext(t, &mcpContext{toolset: &Full{}}, test)
testCaseWithContext(t, &mcpContext{toolset: &full.Full{}}, test)
}
func testCaseWithContext(t *testing.T, mcpCtx *mcpContext, test func(c *mcpContext)) {
@@ -198,23 +201,23 @@ func testCaseWithContext(t *testing.T, mcpCtx *mcpContext, test func(c *mcpConte
}
// withKubeConfig sets up a fake kubeconfig in the temp directory based on the provided rest.Config
func (c *mcpContext) withKubeConfig(rc *rest.Config) *api.Config {
fakeConfig := api.NewConfig()
fakeConfig.Clusters["fake"] = api.NewCluster()
func (c *mcpContext) withKubeConfig(rc *rest.Config) *clientcmdapi.Config {
fakeConfig := clientcmdapi.NewConfig()
fakeConfig.Clusters["fake"] = clientcmdapi.NewCluster()
fakeConfig.Clusters["fake"].Server = "https://127.0.0.1:6443"
fakeConfig.Clusters["additional-cluster"] = api.NewCluster()
fakeConfig.AuthInfos["fake"] = api.NewAuthInfo()
fakeConfig.AuthInfos["additional-auth"] = api.NewAuthInfo()
fakeConfig.Clusters["additional-cluster"] = clientcmdapi.NewCluster()
fakeConfig.AuthInfos["fake"] = clientcmdapi.NewAuthInfo()
fakeConfig.AuthInfos["additional-auth"] = clientcmdapi.NewAuthInfo()
if rc != nil {
fakeConfig.Clusters["fake"].Server = rc.Host
fakeConfig.Clusters["fake"].CertificateAuthorityData = rc.CAData
fakeConfig.AuthInfos["fake"].ClientKeyData = rc.KeyData
fakeConfig.AuthInfos["fake"].ClientCertificateData = rc.CertData
}
fakeConfig.Contexts["fake-context"] = api.NewContext()
fakeConfig.Contexts["fake-context"] = clientcmdapi.NewContext()
fakeConfig.Contexts["fake-context"].Cluster = "fake"
fakeConfig.Contexts["fake-context"].AuthInfo = "fake"
fakeConfig.Contexts["additional-context"] = api.NewContext()
fakeConfig.Contexts["additional-context"] = clientcmdapi.NewContext()
fakeConfig.Contexts["additional-context"].Cluster = "additional-cluster"
fakeConfig.Contexts["additional-context"].AuthInfo = "additional-auth"
fakeConfig.CurrentContext = "fake-context"

View File

@@ -1,58 +0,0 @@
package mcp
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/output"
)
func (s *Server) initConfiguration() []ServerTool {
tools := []ServerTool{
{Tool: Tool{
Name: "configuration_view",
Description: "Get the current Kubernetes configuration content as a kubeconfig YAML",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"minified": {
Type: "boolean",
Description: "Return a minified version of the configuration. " +
"If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. " +
"If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. " +
"(Optional, default true)",
},
},
},
Annotations: ToolAnnotations{
Title: "Configuration: View",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.configurationView},
}
return tools
}
func (s *Server) configurationView(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
minify := true
minified := ctr.GetArguments()["minified"]
if _, ok := minified.(bool); ok {
minify = minified.(bool)
}
ret, err := s.k.ConfigurationView(minify)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get configuration: %v", err)), nil
}
configurationYaml, err := output.MarshalYaml(ret)
if err != nil {
err = fmt.Errorf("failed to get configuration: %v", err)
}
return NewTextResult(configurationYaml, err), nil
}

View File

@@ -1,60 +0,0 @@
package mcp
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/output"
)
func (s *Server) initEvents() []ServerTool {
return []ServerTool{
{Tool: Tool{
Name: "events_list",
Description: "List all the Kubernetes events in the current cluster from all namespaces",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
},
},
},
Annotations: ToolAnnotations{
Title: "Events: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.eventsList},
}
}
func (s *Server) eventsList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace := ctr.GetArguments()["namespace"]
if namespace == nil {
namespace = ""
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
eventMap, err := derived.EventsList(ctx, namespace.(string))
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list events in all namespaces: %v", err)), nil
}
if len(eventMap) == 0 {
return NewTextResult("No events found", nil), nil
}
yamlEvents, err := output.MarshalYaml(eventMap)
if err != nil {
err = fmt.Errorf("failed to list events in all namespaces: %v", err)
}
return NewTextResult(fmt.Sprintf("The following events (YAML format) were found:\n%s", yamlEvents), err), nil
}

View File

@@ -1,168 +0,0 @@
package mcp
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"
)
func (s *Server) initHelm() []ServerTool {
return []ServerTool{
{Tool: Tool{
Name: "helm_install",
Description: "Install a Helm chart in the current or provided namespace",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"chart": {
Type: "string",
Description: "Chart reference to install (for example: stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress)",
},
"values": {
Type: "object",
Description: "Values to pass to the Helm chart (Optional)",
Properties: make(map[string]*jsonschema.Schema),
},
"name": {
Type: "string",
Description: "Name of the Helm release (Optional, random name if not provided)",
},
"namespace": {
Type: "string",
Description: "Namespace to install the Helm chart in (Optional, current namespace if not provided)",
},
},
Required: []string{"chart"},
},
Annotations: ToolAnnotations{
Title: "Helm: Install",
ReadOnlyHint: ptr.To(false),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
OpenWorldHint: ptr.To(true),
},
}, Handler: s.helmInstall},
{Tool: Tool{
Name: "helm_list",
Description: "List all the Helm releases in the current or provided namespace (or in all namespaces if specified)",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Namespace to list Helm releases from (Optional, all namespaces if not provided)",
},
"all_namespaces": {
Type: "boolean",
Description: "If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)",
},
},
},
Annotations: ToolAnnotations{
Title: "Helm: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.helmList},
{Tool: Tool{
Name: "helm_uninstall",
Description: "Uninstall a Helm release in the current or provided namespace",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"name": {
Type: "string",
Description: "Name of the Helm release to uninstall",
},
"namespace": {
Type: "string",
Description: "Namespace to uninstall the Helm release from (Optional, current namespace if not provided)",
},
},
Required: []string{"name"},
},
Annotations: ToolAnnotations{
Title: "Helm: Uninstall",
ReadOnlyHint: ptr.To(false),
DestructiveHint: ptr.To(true),
IdempotentHint: ptr.To(true),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.helmUninstall},
}
}
func (s *Server) helmInstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var chart string
ok := false
if chart, ok = ctr.GetArguments()["chart"].(string); !ok {
return NewTextResult("", fmt.Errorf("failed to install helm chart, missing argument chart")), nil
}
values := map[string]interface{}{}
if v, ok := ctr.GetArguments()["values"].(map[string]interface{}); ok {
values = v
}
name := ""
if v, ok := ctr.GetArguments()["name"].(string); ok {
name = v
}
namespace := ""
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
namespace = v
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.NewHelm().Install(ctx, chart, values, name, namespace)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to install helm chart '%s': %w", chart, err)), nil
}
return NewTextResult(ret, err), nil
}
func (s *Server) helmList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
allNamespaces := false
if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok {
allNamespaces = v
}
namespace := ""
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
namespace = v
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.NewHelm().List(namespace, allNamespaces)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list helm releases in namespace '%s': %w", namespace, err)), nil
}
return NewTextResult(ret, err), nil
}
func (s *Server) helmUninstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
var name string
ok := false
if name, ok = ctr.GetArguments()["name"].(string); !ok {
return NewTextResult("", fmt.Errorf("failed to uninstall helm chart, missing argument name")), nil
}
namespace := ""
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
namespace = v
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.NewHelm().Uninstall(name, namespace)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to uninstall helm chart '%s': %w", name, err)), nil
}
return NewTextResult(ret, err), nil
}

54
pkg/mcp/m3labs.go Normal file
View File

@@ -0,0 +1,54 @@
package mcp
import (
"context"
"encoding/json"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"github.com/containers/kubernetes-mcp-server/pkg/api"
)
func ServerToolToM3LabsServerTool(s *Server, tools []api.ServerTool) ([]server.ServerTool, error) {
m3labTools := make([]server.ServerTool, 0)
for _, tool := range tools {
m3labTool := mcp.Tool{
Name: tool.Tool.Name,
Description: tool.Tool.Description,
Annotations: mcp.ToolAnnotation{
Title: tool.Tool.Annotations.Title,
ReadOnlyHint: tool.Tool.Annotations.ReadOnlyHint,
DestructiveHint: tool.Tool.Annotations.DestructiveHint,
IdempotentHint: tool.Tool.Annotations.IdempotentHint,
OpenWorldHint: tool.Tool.Annotations.OpenWorldHint,
},
}
if tool.Tool.InputSchema != nil {
schema, err := json.Marshal(tool.Tool.InputSchema)
if err != nil {
return nil, fmt.Errorf("failed to marshal tool input schema for tool %s: %v", tool.Tool.Name, err)
}
m3labTool.RawInputSchema = schema
}
m3labHandler := func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
k, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
result, err := tool.Handler(api.ToolHandlerParams{
Context: ctx,
Kubernetes: k,
ToolCallRequest: request,
ListOutput: s.configuration.ListOutput,
})
if err != nil {
return nil, err
}
return NewTextResult(result.Content, result.Error), nil
}
m3labTools = append(m3labTools, server.ServerTool{Tool: m3labTool, Handler: m3labHandler})
}
return m3labTools, nil
}

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"slices"
"github.com/containers/kubernetes-mcp-server/pkg/api"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
authenticationapiv1 "k8s.io/api/authentication/v1"
@@ -24,13 +25,13 @@ type ContextKey string
const TokenScopesContextKey = ContextKey("TokenScopesContextKey")
type Configuration struct {
Toolset Toolset
Toolset api.Toolset
ListOutput output.Output
StaticConfig *config.StaticConfig
}
func (c *Configuration) isToolApplicable(tool ServerTool) bool {
func (c *Configuration) isToolApplicable(tool api.ServerTool) bool {
if c.StaticConfig.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) {
return false
}
@@ -88,15 +89,15 @@ func (s *Server) reloadKubernetesClient() error {
return err
}
s.k = k
applicableTools := make([]ServerTool, 0)
for _, tool := range s.configuration.Toolset.GetTools(s) {
applicableTools := make([]api.ServerTool, 0)
for _, tool := range s.configuration.Toolset.GetTools(s.k) {
if !s.configuration.isToolApplicable(tool) {
continue
}
applicableTools = append(applicableTools, tool)
s.enabledTools = append(s.enabledTools, tool.Tool.Name)
}
m3labsServerTools, err := ServerToolToM3LabsServerTool(applicableTools)
m3labsServerTools, err := ServerToolToM3LabsServerTool(s, applicableTools)
if err != nil {
return fmt.Errorf("failed to convert tools: %v", err)
}

View File

@@ -161,7 +161,7 @@ func TestToolCallLogging(t *testing.T) {
}
})
sensitiveHeaders := []string{
"Authorization",
"Authorization:",
// TODO: Add more sensitive headers as needed
}
t.Run("Does not log sensitive headers", func(t *testing.T) {

3
pkg/mcp/modules.go Normal file
View File

@@ -0,0 +1,3 @@
package mcp
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/full"

View File

@@ -1,75 +0,0 @@
package mcp
import (
"context"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
)
func (s *Server) initNamespaces() []ServerTool {
ret := make([]ServerTool, 0)
ret = append(ret, ServerTool{
Tool: Tool{
Name: "namespaces_list",
Description: "List all the Kubernetes namespaces in the current cluster",
InputSchema: &jsonschema.Schema{
Type: "object",
},
Annotations: ToolAnnotations{
Title: "Namespaces: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.namespacesList,
})
if s.k.IsOpenShift(context.Background()) {
ret = append(ret, ServerTool{
Tool: Tool{
Name: "projects_list",
Description: "List all the OpenShift projects in the current cluster",
InputSchema: &jsonschema.Schema{
Type: "object",
},
Annotations: ToolAnnotations{
Title: "Projects: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.projectsList,
})
}
return ret
}
func (s *Server) namespacesList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.NamespacesList(ctx, kubernetes.ResourceListOptions{AsTable: s.configuration.ListOutput.AsTable()})
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list namespaces: %v", err)), nil
}
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
}
func (s *Server) projectsList(ctx context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.ProjectsList(ctx, kubernetes.ResourceListOptions{AsTable: s.configuration.ListOutput.AsTable()})
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list projects: %v", err)), nil
}
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
}

View File

@@ -1,467 +0,0 @@
package mcp
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/kubectl/pkg/metricsutil"
"k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/output"
)
func (s *Server) initPods() []ServerTool {
return []ServerTool{
{Tool: Tool{
Name: "pods_list",
Description: "List all the Kubernetes pods in the current cluster from all namespaces",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"labelSelector": {
Type: "string",
Description: "Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label",
Pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]",
},
},
},
Annotations: ToolAnnotations{
Title: "Pods: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsListInAllNamespaces},
{Tool: Tool{
Name: "pods_list_in_namespace",
Description: "List all the Kubernetes pods in the specified namespace in the current cluster",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Namespace to list pods from",
},
"labelSelector": {
Type: "string",
Description: "Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label",
Pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]",
},
},
Required: []string{"namespace"},
},
Annotations: ToolAnnotations{
Title: "Pods: List in Namespace",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsListInNamespace},
{Tool: Tool{
Name: "pods_get",
Description: "Get a Kubernetes Pod in the current or provided namespace with the provided name",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Namespace to get the Pod from",
},
"name": {
Type: "string",
Description: "Name of the Pod",
},
},
Required: []string{"name"},
},
Annotations: ToolAnnotations{
Title: "Pods: Get",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsGet},
{Tool: Tool{
Name: "pods_delete",
Description: "Delete a Kubernetes Pod in the current or provided namespace with the provided name",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Namespace to delete the Pod from",
},
"name": {
Type: "string",
Description: "Name of the Pod to delete",
},
},
Required: []string{"name"},
},
Annotations: ToolAnnotations{
Title: "Pods: Delete",
ReadOnlyHint: ptr.To(false),
DestructiveHint: ptr.To(true),
IdempotentHint: ptr.To(true),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsDelete},
{Tool: Tool{
Name: "pods_top",
Description: "List the resource consumption (CPU and memory) as recorded by the Kubernetes Metrics Server for the specified Kubernetes Pods in the all namespaces, the provided namespace, or the current namespace",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"all_namespaces": {
Type: "boolean",
Description: "If true, list the resource consumption for all Pods in all namespaces. If false, list the resource consumption for Pods in the provided namespace or the current namespace",
Default: ToRawMessage(true),
},
"namespace": {
Type: "string",
Description: "Namespace to get the Pods resource consumption from (Optional, current namespace if not provided and all_namespaces is false)",
},
"name": {
Type: "string",
Description: "Name of the Pod to get the resource consumption from (Optional, all Pods in the namespace if not provided)",
},
"label_selector": {
Type: "string",
Description: "Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label (Optional, only applicable when name is not provided)",
Pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]",
},
},
},
Annotations: ToolAnnotations{
Title: "Pods: Top",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(true),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsTop},
{Tool: Tool{
Name: "pods_exec",
Description: "Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Namespace of the Pod where the command will be executed",
},
"name": {
Type: "string",
Description: "Name of the Pod where the command will be executed",
},
"command": {
Type: "array",
Description: "Command to execute in the Pod container. The first item is the command to be run, and the rest are the arguments to that command. Example: [\"ls\", \"-l\", \"/tmp\"]",
Items: &jsonschema.Schema{
Type: "string",
},
},
"container": {
Type: "string",
Description: "Name of the Pod container where the command will be executed (Optional)",
},
},
Required: []string{"name", "command"},
},
Annotations: ToolAnnotations{
Title: "Pods: Exec",
ReadOnlyHint: ptr.To(false),
DestructiveHint: ptr.To(true), // Depending on the Pod's entrypoint, executing certain commands may kill the Pod
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsExec},
{Tool: Tool{
Name: "pods_log",
Description: "Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Namespace to get the Pod logs from",
},
"name": {
Type: "string",
Description: "Name of the Pod to get the logs from",
},
"container": {
Type: "string",
Description: "Name of the Pod container to get the logs from (Optional)",
},
"previous": {
Type: "boolean",
Description: "Return previous terminated container logs (Optional)",
},
},
Required: []string{"name"},
},
Annotations: ToolAnnotations{
Title: "Pods: Log",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsLog},
{Tool: Tool{
Name: "pods_run",
Description: "Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name",
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"namespace": {
Type: "string",
Description: "Namespace to run the Pod in",
},
"name": {
Type: "string",
Description: "Name of the Pod (Optional, random name if not provided)",
},
"image": {
Type: "string",
Description: "Container Image to run in the Pod",
},
"port": {
Type: "number",
Description: "TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)",
},
},
Required: []string{"image"},
},
Annotations: ToolAnnotations{
Title: "Pods: Run",
ReadOnlyHint: ptr.To(false),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.podsRun},
}
}
func (s *Server) podsListInAllNamespaces(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
labelSelector := ctr.GetArguments()["labelSelector"]
resourceListOptions := kubernetes.ResourceListOptions{
AsTable: s.configuration.ListOutput.AsTable(),
}
if labelSelector != nil {
resourceListOptions.LabelSelector = labelSelector.(string)
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.PodsListInAllNamespaces(ctx, resourceListOptions)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil
}
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
}
func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ns := ctr.GetArguments()["namespace"]
if ns == nil {
return NewTextResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil
}
resourceListOptions := kubernetes.ResourceListOptions{
AsTable: s.configuration.ListOutput.AsTable(),
}
labelSelector := ctr.GetArguments()["labelSelector"]
if labelSelector != nil {
resourceListOptions.LabelSelector = labelSelector.(string)
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.PodsListInNamespace(ctx, ns.(string), resourceListOptions)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil
}
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
}
func (s *Server) podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ns := ctr.GetArguments()["namespace"]
if ns == nil {
ns = ""
}
name := ctr.GetArguments()["name"]
if name == nil {
return NewTextResult("", errors.New("failed to get pod, missing argument name")), nil
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.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(output.MarshalYaml(ret)), nil
}
func (s *Server) podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ns := ctr.GetArguments()["namespace"]
if ns == nil {
ns = ""
}
name := ctr.GetArguments()["name"]
if name == nil {
return NewTextResult("", errors.New("failed to delete pod, missing argument name")), nil
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.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 (s *Server) podsTop(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
podsTopOptions := kubernetes.PodsTopOptions{AllNamespaces: true}
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
podsTopOptions.Namespace = v
}
if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok {
podsTopOptions.AllNamespaces = v
}
if v, ok := ctr.GetArguments()["name"].(string); ok {
podsTopOptions.Name = v
}
if v, ok := ctr.GetArguments()["label_selector"].(string); ok {
podsTopOptions.LabelSelector = v
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.PodsTop(ctx, podsTopOptions)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
}
buf := new(bytes.Buffer)
printer := metricsutil.NewTopCmdPrinter(buf)
err = printer.PrintPodMetrics(ret.Items, true, true, false, "", true)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
}
return NewTextResult(buf.String(), nil), nil
}
func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ns := ctr.GetArguments()["namespace"]
if ns == nil {
ns = ""
}
name := ctr.GetArguments()["name"]
if name == nil {
return NewTextResult("", errors.New("failed to exec in pod, missing argument name")), nil
}
container := ctr.GetArguments()["container"]
if container == nil {
container = ""
}
commandArg := ctr.GetArguments()["command"]
command := make([]string, 0)
if _, ok := commandArg.([]interface{}); ok {
for _, cmd := range commandArg.([]interface{}) {
if _, ok := cmd.(string); ok {
command = append(command, cmd.(string))
}
}
} else {
return NewTextResult("", errors.New("failed to exec in pod, invalid command argument")), nil
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.PodsExec(ctx, ns.(string), name.(string), container.(string), command)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to exec in pod %s in namespace %s: %v", name, ns, err)), nil
} else if ret == "" {
ret = fmt.Sprintf("The executed command in pod %s in namespace %s has not produced any output", name, ns)
}
return NewTextResult(ret, err), nil
}
func (s *Server) podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ns := ctr.GetArguments()["namespace"]
if ns == nil {
ns = ""
}
name := ctr.GetArguments()["name"]
if name == nil {
return NewTextResult("", errors.New("failed to get pod log, missing argument name")), nil
}
container := ctr.GetArguments()["container"]
if container == nil {
container = ""
}
previous := ctr.GetArguments()["previous"]
var previousBool bool
if previous != nil {
previousBool = previous.(bool)
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.PodsLog(ctx, ns.(string), name.(string), container.(string), previousBool)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get pod %s log in namespace %s: %v", name, ns, err)), nil
} else if ret == "" {
ret = fmt.Sprintf("The pod %s in namespace %s has not logged any message yet", name, ns)
}
return NewTextResult(ret, err), nil
}
func (s *Server) podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
ns := ctr.GetArguments()["namespace"]
if ns == nil {
ns = ""
}
name := ctr.GetArguments()["name"]
if name == nil {
name = ""
}
image := ctr.GetArguments()["image"]
if image == nil {
return NewTextResult("", errors.New("failed to run pod, missing argument image")), nil
}
port := ctr.GetArguments()["port"]
if port == nil {
port = float64(0)
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
resources, err := derived.PodsRun(ctx, ns.(string), name.(string), image.(string), int32(port.(float64)))
if err != nil {
return NewTextResult("", fmt.Errorf("failed to run pod %s in namespace %s: %v", name, ns, err)), nil
}
marshalledYaml, err := output.MarshalYaml(resources)
if err != nil {
err = fmt.Errorf("failed to run pod: %v", err)
}
return NewTextResult("# The following resources (YAML) have been created or updated successfully\n"+marshalledYaml, err), nil
}

View File

@@ -1,302 +0,0 @@
package mcp
import (
"context"
"errors"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/output"
)
func (s *Server) initResources() []ServerTool {
commonApiVersion := "v1 Pod, v1 Service, v1 Node, apps/v1 Deployment, networking.k8s.io/v1 Ingress"
if s.k.IsOpenShift(context.Background()) {
commonApiVersion += ", route.openshift.io/v1 Route"
}
commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion)
return []ServerTool{
{Tool: Tool{
Name: "resources_list",
Description: "List Kubernetes resources and objects in the current cluster by providing their apiVersion and kind and optionally the namespace and label selector\n" + commonApiVersion,
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"apiVersion": {
Type: "string",
Description: "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)",
},
"kind": {
Type: "string",
Description: "kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)",
},
"namespace": {
Type: "string",
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",
},
"labelSelector": {
Type: "string",
Description: "Optional Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label",
Pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]",
},
},
Required: []string{"apiVersion", "kind"},
},
Annotations: ToolAnnotations{
Title: "Resources: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.resourcesList},
{Tool: Tool{
Name: "resources_get",
Description: "Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n" + commonApiVersion,
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"apiVersion": {
Type: "string",
Description: "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)",
},
"kind": {
Type: "string",
Description: "kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)",
},
"namespace": {
Type: "string",
Description: "Optional Namespace to retrieve the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will get resource from configured namespace",
},
"name": {
Type: "string",
Description: "Name of the resource",
},
},
Required: []string{"apiVersion", "kind", "name"},
},
Annotations: ToolAnnotations{
Title: "Resources: Get",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.resourcesGet},
{Tool: Tool{
Name: "resources_create_or_update",
Description: "Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n" + commonApiVersion,
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"resource": {
Type: "string",
Description: "A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec",
},
},
Required: []string{"resource"},
},
Annotations: ToolAnnotations{
Title: "Resources: Create or Update",
ReadOnlyHint: ptr.To(false),
DestructiveHint: ptr.To(true),
IdempotentHint: ptr.To(true),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.resourcesCreateOrUpdate},
{Tool: Tool{
Name: "resources_delete",
Description: "Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n" + commonApiVersion,
InputSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"apiVersion": {
Type: "string",
Description: "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)",
},
"kind": {
Type: "string",
Description: "kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)",
},
"namespace": {
Type: "string",
Description: "Optional Namespace to delete the namespaced resource from (ignored in case of cluster scoped resources). If not provided, will delete resource from configured namespace",
},
"name": {
Type: "string",
Description: "Name of the resource",
},
},
Required: []string{"apiVersion", "kind", "name"},
},
Annotations: ToolAnnotations{
Title: "Resources: Delete",
ReadOnlyHint: ptr.To(false),
DestructiveHint: ptr.To(true),
IdempotentHint: ptr.To(true),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.resourcesDelete},
}
}
func (s *Server) resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace := ctr.GetArguments()["namespace"]
if namespace == nil {
namespace = ""
}
labelSelector := ctr.GetArguments()["labelSelector"]
resourceListOptions := kubernetes.ResourceListOptions{
AsTable: s.configuration.ListOutput.AsTable(),
}
if labelSelector != nil {
l, ok := labelSelector.(string)
if !ok {
return NewTextResult("", fmt.Errorf("labelSelector is not a string")), nil
}
resourceListOptions.LabelSelector = l
}
gvk, err := parseGroupVersionKind(ctr.GetArguments())
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil
}
ns, ok := namespace.(string)
if !ok {
return NewTextResult("", fmt.Errorf("namespace is not a string")), nil
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.ResourcesList(ctx, gvk, ns, resourceListOptions)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to list resources: %v", err)), nil
}
return NewTextResult(s.configuration.ListOutput.PrintObj(ret)), nil
}
func (s *Server) resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace := ctr.GetArguments()["namespace"]
if namespace == nil {
namespace = ""
}
gvk, err := parseGroupVersionKind(ctr.GetArguments())
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get resource, %s", err)), nil
}
name := ctr.GetArguments()["name"]
if name == nil {
return NewTextResult("", errors.New("failed to get resource, missing argument name")), nil
}
ns, ok := namespace.(string)
if !ok {
return NewTextResult("", fmt.Errorf("namespace is not a string")), nil
}
n, ok := name.(string)
if !ok {
return NewTextResult("", fmt.Errorf("name is not a string")), nil
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
ret, err := derived.ResourcesGet(ctx, gvk, ns, n)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to get resource: %v", err)), nil
}
return NewTextResult(output.MarshalYaml(ret)), nil
}
func (s *Server) resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
resource := ctr.GetArguments()["resource"]
if resource == nil || resource == "" {
return NewTextResult("", errors.New("failed to create or update resources, missing argument resource")), nil
}
r, ok := resource.(string)
if !ok {
return NewTextResult("", fmt.Errorf("resource is not a string")), nil
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
resources, err := derived.ResourcesCreateOrUpdate(ctx, r)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to create or update resources: %v", err)), nil
}
marshalledYaml, err := output.MarshalYaml(resources)
if err != nil {
err = fmt.Errorf("failed to create or update resources:: %v", err)
}
return NewTextResult("# The following resources (YAML) have been created or updated successfully\n"+marshalledYaml, err), nil
}
func (s *Server) resourcesDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
namespace := ctr.GetArguments()["namespace"]
if namespace == nil {
namespace = ""
}
gvk, err := parseGroupVersionKind(ctr.GetArguments())
if err != nil {
return NewTextResult("", fmt.Errorf("failed to delete resource, %s", err)), nil
}
name := ctr.GetArguments()["name"]
if name == nil {
return NewTextResult("", errors.New("failed to delete resource, missing argument name")), nil
}
ns, ok := namespace.(string)
if !ok {
return NewTextResult("", fmt.Errorf("namespace is not a string")), nil
}
n, ok := name.(string)
if !ok {
return NewTextResult("", fmt.Errorf("name is not a string")), nil
}
derived, err := s.k.Derived(ctx)
if err != nil {
return nil, err
}
err = derived.ResourcesDelete(ctx, gvk, ns, n)
if err != nil {
return NewTextResult("", fmt.Errorf("failed to delete resource: %v", err)), nil
}
return NewTextResult("Resource deleted successfully", err), nil
}
func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) {
apiVersion := arguments["apiVersion"]
if apiVersion == nil {
return nil, errors.New("missing argument apiVersion")
}
kind := arguments["kind"]
if kind == nil {
return nil, errors.New("missing argument kind")
}
a, ok := apiVersion.(string)
if !ok {
return nil, fmt.Errorf("name is not a string")
}
gv, err := schema.ParseGroupVersion(a)
if err != nil {
return nil, errors.New("invalid argument apiVersion")
}
return &schema.GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind.(string)}, nil
}

View File

@@ -1,151 +0,0 @@
package mcp
import (
"context"
"encoding/json"
"fmt"
"slices"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type Toolset interface {
GetName() string
GetDescription() string
GetTools(s *Server) []ServerTool
}
type ServerTool struct {
Tool Tool
Handler ToolHandlerFunc
}
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
type Tool struct {
// The name of the tool.
// Intended for programmatic or logical use, but used as a display name in past
// specs or fallback (if title isn't present).
Name string `json:"name"`
// A human-readable description of the tool.
//
// This can be used by clients to improve the LLM's understanding of available
// tools. It can be thought of like a "hint" to the model.
Description string `json:"description,omitempty"`
// Additional tool information.
Annotations ToolAnnotations `json:"annotations"`
// A JSON Schema object defining the expected parameters for the tool.
InputSchema *jsonschema.Schema
}
type ToolAnnotations struct {
// Human-readable title for the tool
Title string `json:"title,omitempty"`
// If true, the tool does not modify its environment.
ReadOnlyHint *bool `json:"readOnlyHint,omitempty"`
// If true, the tool may perform destructive updates to its environment. If
// false, the tool performs only additive updates.
//
// (This property is meaningful only when ReadOnlyHint == false.)
DestructiveHint *bool `json:"destructiveHint,omitempty"`
// If true, calling the tool repeatedly with the same arguments will have no
// additional effect on its environment.
//
// (This property is meaningful only when ReadOnlyHint == false.)
IdempotentHint *bool `json:"idempotentHint,omitempty"`
// If true, this tool may interact with an "open world" of external entities. If
// false, the tool's domain of interaction is closed. For example, the world of
// a web search tool is open, whereas that of a memory tool is not.
OpenWorldHint *bool `json:"openWorldHint,omitempty"`
}
var toolsets []Toolset
func Register(toolset Toolset) {
toolsets = append(toolsets, toolset)
}
func Toolsets() []Toolset {
return toolsets
}
func ToolsetNames() []string {
names := make([]string, 0)
for _, toolset := range Toolsets() {
names = append(names, toolset.GetName())
}
return names
}
func ToolsetFromString(name string) Toolset {
for _, toolset := range Toolsets() {
if toolset.GetName() == name {
return toolset
}
}
return nil
}
func ToRawMessage(v any) json.RawMessage {
if v == nil {
return nil
}
b, err := json.Marshal(v)
if err != nil {
return nil
}
return b
}
func ServerToolToM3LabsServerTool(tools []ServerTool) ([]server.ServerTool, error) {
m3labTools := make([]server.ServerTool, 0)
for _, tool := range tools {
m3labTool := mcp.Tool{
Name: tool.Tool.Name,
Description: tool.Tool.Description,
Annotations: mcp.ToolAnnotation{
Title: tool.Tool.Annotations.Title,
ReadOnlyHint: tool.Tool.Annotations.ReadOnlyHint,
DestructiveHint: tool.Tool.Annotations.DestructiveHint,
IdempotentHint: tool.Tool.Annotations.IdempotentHint,
OpenWorldHint: tool.Tool.Annotations.OpenWorldHint,
},
}
if tool.Tool.InputSchema != nil {
schema, err := json.Marshal(tool.Tool.InputSchema)
if err != nil {
return nil, fmt.Errorf("failed to marshal tool input schema for tool %s: %v", tool.Tool.Name, err)
}
m3labTool.RawInputSchema = schema
}
m3labTools = append(m3labTools, server.ServerTool{Tool: m3labTool, Handler: server.ToolHandlerFunc(tool.Handler)})
}
return m3labTools, nil
}
type Full struct{}
var _ Toolset = (*Full)(nil)
func (p *Full) GetName() string {
return "full"
}
func (p *Full) GetDescription() string {
return "Complete toolset with all tools and extended outputs"
}
func (p *Full) GetTools(s *Server) []ServerTool {
return slices.Concat(
s.initConfiguration(),
s.initEvents(),
s.initNamespaces(),
s.initPods(),
s.initResources(),
s.initHelm(),
)
}
func init() {
Register(&Full{})
}

View File

@@ -9,6 +9,7 @@ import (
"strings"
"testing"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets/full"
"github.com/mark3labs/mcp-go/mcp"
"github.com/stretchr/testify/assert"
)
@@ -34,7 +35,7 @@ func TestFullToolsetTools(t *testing.T) {
"resources_create_or_update",
"resources_delete",
}
mcpCtx := &mcpContext{toolset: &Full{}}
mcpCtx := &mcpContext{toolset: &full.Full{}}
testCaseWithContext(t, mcpCtx, func(c *mcpContext) {
tools, err := c.mcpClient.ListTools(c.ctx, mcp.ListToolsRequest{})
t.Run("ListTools returns tools", func(t *testing.T) {
@@ -73,7 +74,7 @@ func TestFullToolsetTools(t *testing.T) {
func TestFullToolsetToolsInOpenShift(t *testing.T) {
mcpCtx := &mcpContext{
toolset: &Full{},
toolset: &full.Full{},
before: inOpenShift,
after: inOpenShiftClear,
}