mirror of
				https://github.com/openshift/openshift-mcp-server.git
				synced 2025-10-17 14:27:48 +03:00 
			
		
		
		
	feat(mcp): toolset definitions completely agnostic from underlying MCP impl (#322)
Signed-off-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
		
							
								
								
									
										99
									
								
								pkg/api/toolsets.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								pkg/api/toolsets.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| package api | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
|  | ||||
| 	internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/output" | ||||
| 	"github.com/google/jsonschema-go/jsonschema" | ||||
| ) | ||||
|  | ||||
| type ServerTool struct { | ||||
| 	Tool    Tool | ||||
| 	Handler ToolHandlerFunc | ||||
| } | ||||
|  | ||||
| type Toolset interface { | ||||
| 	// GetName returns the name of the toolset. | ||||
| 	// Used to identify the toolset in configuration, logs, and command-line arguments. | ||||
| 	// Examples: "core", "metrics", "helm" | ||||
| 	GetName() string | ||||
| 	GetDescription() string | ||||
| 	GetTools(k *internalk8s.Manager) []ServerTool | ||||
| } | ||||
|  | ||||
| type ToolCallRequest interface { | ||||
| 	GetArguments() map[string]any | ||||
| } | ||||
|  | ||||
| type ToolCallResult struct { | ||||
| 	// Raw content returned by the tool. | ||||
| 	Content string | ||||
| 	// Error (non-protocol) to send back to the LLM. | ||||
| 	Error error | ||||
| } | ||||
|  | ||||
| func NewToolCallResult(content string, err error) *ToolCallResult { | ||||
| 	return &ToolCallResult{ | ||||
| 		Content: content, | ||||
| 		Error:   err, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type ToolHandlerParams struct { | ||||
| 	context.Context | ||||
| 	*internalk8s.Kubernetes | ||||
| 	ToolCallRequest | ||||
| 	ListOutput output.Output | ||||
| } | ||||
|  | ||||
| type ToolHandlerFunc func(params ToolHandlerParams) (*ToolCallResult, 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"` | ||||
| } | ||||
|  | ||||
| func ToRawMessage(v any) json.RawMessage { | ||||
| 	if v == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	b, err := json.Marshal(v) | ||||
| 	if err != nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	return b | ||||
| } | ||||
| @@ -21,6 +21,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/containers/kubernetes-mcp-server/internal/test" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/toolsets" | ||||
| 	"github.com/coreos/go-oidc/v3/oidc" | ||||
| 	"github.com/coreos/go-oidc/v3/oidc/oidctest" | ||||
| 	"golang.org/x/sync/errgroup" | ||||
| @@ -87,7 +88,7 @@ func (c *httpContext) beforeEach(t *testing.T) { | ||||
| 	} | ||||
| 	c.StaticConfig.Port = fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port) | ||||
| 	mcpServer, err := mcp.NewServer(mcp.Configuration{ | ||||
| 		Toolset:      mcp.Toolsets()[0], | ||||
| 		Toolset:      toolsets.Toolsets()[0], | ||||
| 		StaticConfig: c.StaticConfig, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
|   | ||||
| @@ -13,6 +13,7 @@ import ( | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/toolsets" | ||||
| 	"github.com/coreos/go-oidc/v3/oidc" | ||||
| 	"github.com/spf13/cobra" | ||||
|  | ||||
| @@ -115,7 +116,7 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command { | ||||
| 	cmd.Flags().StringVar(&o.Port, "port", o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)") | ||||
| 	cmd.Flags().StringVar(&o.SSEBaseUrl, "sse-base-url", o.SSEBaseUrl, "SSE public base URL to use when sending the endpoint message (e.g. https://example.com)") | ||||
| 	cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication") | ||||
| 	cmd.Flags().StringVar(&o.Toolset, "toolset", o.Toolset, "MCP toolset to use (one of: "+strings.Join(mcp.ToolsetNames(), ", ")+")") | ||||
| 	cmd.Flags().StringVar(&o.Toolset, "toolset", o.Toolset, "MCP toolset to use (one of: "+strings.Join(toolsets.ToolsetNames(), ", ")+")") | ||||
| 	cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to table.") | ||||
| 	cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed") | ||||
| 	cmd.Flags().BoolVar(&o.DisableDestructive, "disable-destructive", o.DisableDestructive, "If true, tools annotated with destructiveHint=true are disabled") | ||||
| @@ -237,9 +238,9 @@ func (m *MCPServerOptions) Validate() error { | ||||
| } | ||||
|  | ||||
| func (m *MCPServerOptions) Run() error { | ||||
| 	toolset := mcp.ToolsetFromString(m.Toolset) | ||||
| 	toolset := toolsets.ToolsetFromString(m.Toolset) | ||||
| 	if toolset == nil { | ||||
| 		return fmt.Errorf("invalid toolset name: %s, valid names are: %s", m.Toolset, strings.Join(mcp.ToolsetNames(), ", ")) | ||||
| 		return fmt.Errorf("invalid toolset name: %s, valid names are: %s", m.Toolset, strings.Join(toolsets.ToolsetNames(), ", ")) | ||||
| 	} | ||||
| 	listOutput := output.FromString(m.StaticConfig.ListOutput) | ||||
| 	if listOutput == nil { | ||||
|   | ||||
| @@ -81,24 +81,24 @@ func (m *Manager) ToRawKubeConfigLoader() clientcmd.ClientConfig { | ||||
| 	return m.clientCmdConfig | ||||
| } | ||||
|  | ||||
| func (m *Manager) ConfigurationView(minify bool) (runtime.Object, error) { | ||||
| func (k *Kubernetes) ConfigurationView(minify bool) (runtime.Object, error) { | ||||
| 	var cfg clientcmdapi.Config | ||||
| 	var err error | ||||
| 	if m.IsInCluster() { | ||||
| 	if k.manager.IsInCluster() { | ||||
| 		cfg = *clientcmdapi.NewConfig() | ||||
| 		cfg.Clusters["cluster"] = &clientcmdapi.Cluster{ | ||||
| 			Server:                m.cfg.Host, | ||||
| 			InsecureSkipTLSVerify: m.cfg.Insecure, | ||||
| 			Server:                k.manager.cfg.Host, | ||||
| 			InsecureSkipTLSVerify: k.manager.cfg.Insecure, | ||||
| 		} | ||||
| 		cfg.AuthInfos["user"] = &clientcmdapi.AuthInfo{ | ||||
| 			Token: m.cfg.BearerToken, | ||||
| 			Token: k.manager.cfg.BearerToken, | ||||
| 		} | ||||
| 		cfg.Contexts["context"] = &clientcmdapi.Context{ | ||||
| 			Cluster:  "cluster", | ||||
| 			AuthInfo: "user", | ||||
| 		} | ||||
| 		cfg.CurrentContext = "context" | ||||
| 	} else if cfg, err = m.clientCmdConfig.RawConfig(); err != nil { | ||||
| 	} else if cfg, err = k.manager.clientCmdConfig.RawConfig(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	if minify { | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
							
								
								
									
										54
									
								
								pkg/mcp/m3labs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								pkg/mcp/m3labs.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -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) | ||||
| 	} | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										3
									
								
								pkg/mcp/modules.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| package mcp | ||||
|  | ||||
| import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/full" | ||||
| @@ -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 | ||||
| } | ||||
| @@ -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{}) | ||||
| } | ||||
| @@ -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, | ||||
| 	} | ||||
|   | ||||
| @@ -1,19 +1,18 @@ | ||||
| package mcp | ||||
| package full | ||||
| 
 | ||||
| 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/api" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/output" | ||||
| ) | ||||
| 
 | ||||
| func (s *Server) initConfiguration() []ServerTool { | ||||
| 	tools := []ServerTool{ | ||||
| 		{Tool: Tool{ | ||||
| func initConfiguration() []api.ServerTool { | ||||
| 	tools := []api.ServerTool{ | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "configuration_view", | ||||
| 			Description: "Get the current Kubernetes configuration content as a kubeconfig YAML", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -28,31 +27,31 @@ func (s *Server) initConfiguration() []ServerTool { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.ToolAnnotations{ | ||||
| 				Title:           "Configuration: View", | ||||
| 				ReadOnlyHint:    ptr.To(true), | ||||
| 				DestructiveHint: ptr.To(false), | ||||
| 				IdempotentHint:  ptr.To(false), | ||||
| 				OpenWorldHint:   ptr.To(true), | ||||
| 			}, | ||||
| 		}, Handler: s.configurationView}, | ||||
| 		}, Handler: configurationView}, | ||||
| 	} | ||||
| 	return tools | ||||
| } | ||||
| 
 | ||||
| func (s *Server) configurationView(_ context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| func configurationView(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	minify := true | ||||
| 	minified := ctr.GetArguments()["minified"] | ||||
| 	minified := params.GetArguments()["minified"] | ||||
| 	if _, ok := minified.(bool); ok { | ||||
| 		minify = minified.(bool) | ||||
| 	} | ||||
| 	ret, err := s.k.ConfigurationView(minify) | ||||
| 	ret, err := params.ConfigurationView(minify) | ||||
| 	if err != nil { | ||||
| 		return NewTextResult("", fmt.Errorf("failed to get configuration: %v", err)), nil | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 	return api.NewToolCallResult(configurationYaml, err), nil | ||||
| } | ||||
| @@ -1,19 +1,18 @@ | ||||
| package mcp | ||||
| package full | ||||
| 
 | ||||
| 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/api" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/output" | ||||
| ) | ||||
| 
 | ||||
| func (s *Server) initEvents() []ServerTool { | ||||
| 	return []ServerTool{ | ||||
| 		{Tool: Tool{ | ||||
| func initEvents() []api.ServerTool { | ||||
| 	return []api.ServerTool{ | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "events_list", | ||||
| 			Description: "List all the Kubernetes events in the current cluster from all namespaces", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -25,36 +24,32 @@ func (s *Server) initEvents() []ServerTool { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.ToolAnnotations{ | ||||
| 				Title:           "Events: List", | ||||
| 				ReadOnlyHint:    ptr.To(true), | ||||
| 				DestructiveHint: ptr.To(false), | ||||
| 				IdempotentHint:  ptr.To(false), | ||||
| 				OpenWorldHint:   ptr.To(true), | ||||
| 			}, | ||||
| 		}, Handler: s.eventsList}, | ||||
| 		}, Handler: eventsList}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Server) eventsList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	namespace := ctr.GetArguments()["namespace"] | ||||
| func eventsList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	namespace := params.GetArguments()["namespace"] | ||||
| 	if namespace == nil { | ||||
| 		namespace = "" | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	eventMap, err := params.EventsList(params, namespace.(string)) | ||||
| 	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 | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to list events in all namespaces: %v", err)), nil | ||||
| 	} | ||||
| 	if len(eventMap) == 0 { | ||||
| 		return NewTextResult("No events found", nil), nil | ||||
| 		return api.NewToolCallResult("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 | ||||
| 	return api.NewToolCallResult(fmt.Sprintf("The following events (YAML format) were found:\n%s", yamlEvents), err), nil | ||||
| } | ||||
| @@ -1,17 +1,17 @@ | ||||
| package mcp | ||||
| package full | ||||
| 
 | ||||
| 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/api" | ||||
| ) | ||||
| 
 | ||||
| func (s *Server) initHelm() []ServerTool { | ||||
| 	return []ServerTool{ | ||||
| 		{Tool: Tool{ | ||||
| func initHelm() []api.ServerTool { | ||||
| 	return []api.ServerTool{ | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "helm_install", | ||||
| 			Description: "Install a Helm chart in the current or provided namespace", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -37,15 +37,15 @@ func (s *Server) initHelm() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"chart"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: helmInstall}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -61,15 +61,15 @@ func (s *Server) initHelm() []ServerTool { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: helmList}, | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "helm_uninstall", | ||||
| 			Description: "Uninstall a Helm release in the current or provided namespace", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -86,83 +86,71 @@ func (s *Server) initHelm() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"name"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.ToolAnnotations{ | ||||
| 				Title:           "Helm: Uninstall", | ||||
| 				ReadOnlyHint:    ptr.To(false), | ||||
| 				DestructiveHint: ptr.To(true), | ||||
| 				IdempotentHint:  ptr.To(true), | ||||
| 				OpenWorldHint:   ptr.To(true), | ||||
| 			}, | ||||
| 		}, Handler: s.helmUninstall}, | ||||
| 		}, Handler: helmUninstall}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Server) helmInstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| func helmInstall(params api.ToolHandlerParams) (*api.ToolCallResult, 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 | ||||
| 	if chart, ok = params.GetArguments()["chart"].(string); !ok { | ||||
| 		return api.NewToolCallResult("", 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 { | ||||
| 	if v, ok := params.GetArguments()["values"].(map[string]interface{}); ok { | ||||
| 		values = v | ||||
| 	} | ||||
| 	name := "" | ||||
| 	if v, ok := ctr.GetArguments()["name"].(string); ok { | ||||
| 	if v, ok := params.GetArguments()["name"].(string); ok { | ||||
| 		name = v | ||||
| 	} | ||||
| 	namespace := "" | ||||
| 	if v, ok := ctr.GetArguments()["namespace"].(string); ok { | ||||
| 	if v, ok := params.GetArguments()["namespace"].(string); ok { | ||||
| 		namespace = v | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.NewHelm().Install(params, chart, values, name, namespace) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to install helm chart '%s': %w", chart, err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(ret, err), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) helmList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| func helmList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	allNamespaces := false | ||||
| 	if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok { | ||||
| 	if v, ok := params.GetArguments()["all_namespaces"].(bool); ok { | ||||
| 		allNamespaces = v | ||||
| 	} | ||||
| 	namespace := "" | ||||
| 	if v, ok := ctr.GetArguments()["namespace"].(string); ok { | ||||
| 	if v, ok := params.GetArguments()["namespace"].(string); ok { | ||||
| 		namespace = v | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.NewHelm().List(namespace, allNamespaces) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to list helm releases in namespace '%s': %w", namespace, err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(ret, err), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) helmUninstall(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| func helmUninstall(params api.ToolHandlerParams) (*api.ToolCallResult, 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 | ||||
| 	if name, ok = params.GetArguments()["name"].(string); !ok { | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to uninstall helm chart, missing argument name")), nil | ||||
| 	} | ||||
| 	namespace := "" | ||||
| 	if v, ok := ctr.GetArguments()["namespace"].(string); ok { | ||||
| 	if v, ok := params.GetArguments()["namespace"].(string); ok { | ||||
| 		namespace = v | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.NewHelm().Uninstall(name, namespace) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to uninstall helm chart '%s': %w", name, err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(ret, err), nil | ||||
| } | ||||
							
								
								
									
										68
									
								
								pkg/toolsets/full/namespaces.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								pkg/toolsets/full/namespaces.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| package full | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/google/jsonschema-go/jsonschema" | ||||
| 	"k8s.io/utils/ptr" | ||||
|  | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/api" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/kubernetes" | ||||
| 	internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" | ||||
| ) | ||||
|  | ||||
| func initNamespaces(k *internalk8s.Manager) []api.ServerTool { | ||||
| 	ret := make([]api.ServerTool, 0) | ||||
| 	ret = append(ret, api.ServerTool{ | ||||
| 		Tool: api.Tool{ | ||||
| 			Name:        "namespaces_list", | ||||
| 			Description: "List all the Kubernetes namespaces in the current cluster", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| 				Type: "object", | ||||
| 			}, | ||||
| 			Annotations: api.ToolAnnotations{ | ||||
| 				Title:           "Namespaces: List", | ||||
| 				ReadOnlyHint:    ptr.To(true), | ||||
| 				DestructiveHint: ptr.To(false), | ||||
| 				IdempotentHint:  ptr.To(false), | ||||
| 				OpenWorldHint:   ptr.To(true), | ||||
| 			}, | ||||
| 		}, Handler: namespacesList, | ||||
| 	}) | ||||
| 	if k.IsOpenShift(context.Background()) { | ||||
| 		ret = append(ret, api.ServerTool{ | ||||
| 			Tool: api.Tool{ | ||||
| 				Name:        "projects_list", | ||||
| 				Description: "List all the OpenShift projects in the current cluster", | ||||
| 				InputSchema: &jsonschema.Schema{ | ||||
| 					Type: "object", | ||||
| 				}, | ||||
| 				Annotations: api.ToolAnnotations{ | ||||
| 					Title:           "Projects: List", | ||||
| 					ReadOnlyHint:    ptr.To(true), | ||||
| 					DestructiveHint: ptr.To(false), | ||||
| 					IdempotentHint:  ptr.To(false), | ||||
| 					OpenWorldHint:   ptr.To(true), | ||||
| 				}, | ||||
| 			}, Handler: projectsList, | ||||
| 		}) | ||||
| 	} | ||||
| 	return ret | ||||
| } | ||||
|  | ||||
| func namespacesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ret, err := params.NamespacesList(params, kubernetes.ResourceListOptions{AsTable: params.ListOutput.AsTable()}) | ||||
| 	if err != nil { | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to list namespaces: %v", err)), nil | ||||
| 	} | ||||
| 	return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil | ||||
| } | ||||
|  | ||||
| func projectsList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ret, err := params.ProjectsList(params, kubernetes.ResourceListOptions{AsTable: params.ListOutput.AsTable()}) | ||||
| 	if err != nil { | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to list projects: %v", err)), nil | ||||
| 	} | ||||
| 	return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil | ||||
| } | ||||
| @@ -1,23 +1,22 @@ | ||||
| package mcp | ||||
| package full | ||||
| 
 | ||||
| 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/api" | ||||
| 	"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{ | ||||
| func initPods() []api.ServerTool { | ||||
| 	return []api.ServerTool{ | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "pods_list", | ||||
| 			Description: "List all the Kubernetes pods in the current cluster from all namespaces", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -30,15 +29,15 @@ func (s *Server) initPods() []ServerTool { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: podsListInAllNamespaces}, | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "pods_list_in_namespace", | ||||
| 			Description: "List all the Kubernetes pods in the specified namespace in the current cluster", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -56,15 +55,15 @@ func (s *Server) initPods() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"namespace"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: podsListInNamespace}, | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "pods_get", | ||||
| 			Description: "Get a Kubernetes Pod in the current or provided namespace with the provided name", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -81,15 +80,15 @@ func (s *Server) initPods() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"name"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: podsGet}, | ||||
| 		{Tool: api.Tool{ | ||||
| 			Name:        "pods_delete", | ||||
| 			Description: "Delete a Kubernetes Pod in the current or provided namespace with the provided name", | ||||
| 			InputSchema: &jsonschema.Schema{ | ||||
| @@ -106,15 +105,15 @@ func (s *Server) initPods() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"name"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: podsDelete}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -123,7 +122,7 @@ func (s *Server) initPods() []ServerTool { | ||||
| 					"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), | ||||
| 						Default:     api.ToRawMessage(true), | ||||
| 					}, | ||||
| 					"namespace": { | ||||
| 						Type:        "string", | ||||
| @@ -140,15 +139,15 @@ func (s *Server) initPods() []ServerTool { | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: podsTop}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -176,15 +175,15 @@ func (s *Server) initPods() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"name", "command"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: podsExec}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -209,15 +208,15 @@ func (s *Server) initPods() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"name"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: podsLog}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -242,144 +241,124 @@ func (s *Server) initPods() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"image"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.ToolAnnotations{ | ||||
| 				Title:           "Pods: Run", | ||||
| 				ReadOnlyHint:    ptr.To(false), | ||||
| 				DestructiveHint: ptr.To(false), | ||||
| 				IdempotentHint:  ptr.To(false), | ||||
| 				OpenWorldHint:   ptr.To(true), | ||||
| 			}, | ||||
| 		}, Handler: s.podsRun}, | ||||
| 		}, Handler: podsRun}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsListInAllNamespaces(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	labelSelector := ctr.GetArguments()["labelSelector"] | ||||
| func podsListInAllNamespaces(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	labelSelector := params.GetArguments()["labelSelector"] | ||||
| 	resourceListOptions := kubernetes.ResourceListOptions{ | ||||
| 		AsTable: s.configuration.ListOutput.AsTable(), | ||||
| 		AsTable: params.ListOutput.AsTable(), | ||||
| 	} | ||||
| 	if labelSelector != nil { | ||||
| 		resourceListOptions.LabelSelector = labelSelector.(string) | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.PodsListInAllNamespaces(params, resourceListOptions) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to list pods in all namespaces: %v", err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	ns := ctr.GetArguments()["namespace"] | ||||
| func podsListInNamespace(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ns := params.GetArguments()["namespace"] | ||||
| 	if ns == nil { | ||||
| 		return NewTextResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil | ||||
| 		return api.NewToolCallResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil | ||||
| 	} | ||||
| 	resourceListOptions := kubernetes.ResourceListOptions{ | ||||
| 		AsTable: s.configuration.ListOutput.AsTable(), | ||||
| 		AsTable: params.ListOutput.AsTable(), | ||||
| 	} | ||||
| 	labelSelector := ctr.GetArguments()["labelSelector"] | ||||
| 	labelSelector := params.GetArguments()["labelSelector"] | ||||
| 	if labelSelector != nil { | ||||
| 		resourceListOptions.LabelSelector = labelSelector.(string) | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.PodsListInNamespace(params, ns.(string), resourceListOptions) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to list pods in namespace %s: %v", ns, err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	ns := ctr.GetArguments()["namespace"] | ||||
| func podsGet(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ns := params.GetArguments()["namespace"] | ||||
| 	if ns == nil { | ||||
| 		ns = "" | ||||
| 	} | ||||
| 	name := ctr.GetArguments()["name"] | ||||
| 	name := params.GetArguments()["name"] | ||||
| 	if name == nil { | ||||
| 		return NewTextResult("", errors.New("failed to get pod, missing argument name")), nil | ||||
| 		return api.NewToolCallResult("", errors.New("failed to get pod, missing argument name")), nil | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.PodsGet(params, ns.(string), name.(string)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to get pod %s in namespace %s: %v", name, ns, err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(output.MarshalYaml(ret)), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	ns := ctr.GetArguments()["namespace"] | ||||
| func podsDelete(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ns := params.GetArguments()["namespace"] | ||||
| 	if ns == nil { | ||||
| 		ns = "" | ||||
| 	} | ||||
| 	name := ctr.GetArguments()["name"] | ||||
| 	name := params.GetArguments()["name"] | ||||
| 	if name == nil { | ||||
| 		return NewTextResult("", errors.New("failed to delete pod, missing argument name")), nil | ||||
| 		return api.NewToolCallResult("", errors.New("failed to delete pod, missing argument name")), nil | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.PodsDelete(params, ns.(string), name.(string)) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to delete pod %s in namespace %s: %v", name, ns, err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(ret, err), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsTop(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| func podsTop(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	podsTopOptions := kubernetes.PodsTopOptions{AllNamespaces: true} | ||||
| 	if v, ok := ctr.GetArguments()["namespace"].(string); ok { | ||||
| 	if v, ok := params.GetArguments()["namespace"].(string); ok { | ||||
| 		podsTopOptions.Namespace = v | ||||
| 	} | ||||
| 	if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok { | ||||
| 	if v, ok := params.GetArguments()["all_namespaces"].(bool); ok { | ||||
| 		podsTopOptions.AllNamespaces = v | ||||
| 	} | ||||
| 	if v, ok := ctr.GetArguments()["name"].(string); ok { | ||||
| 	if v, ok := params.GetArguments()["name"].(string); ok { | ||||
| 		podsTopOptions.Name = v | ||||
| 	} | ||||
| 	if v, ok := ctr.GetArguments()["label_selector"].(string); ok { | ||||
| 	if v, ok := params.GetArguments()["label_selector"].(string); ok { | ||||
| 		podsTopOptions.LabelSelector = v | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.PodsTop(params, podsTopOptions) | ||||
| 	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 | ||||
| 		return api.NewToolCallResult("", 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 api.NewToolCallResult("", fmt.Errorf("failed to get pods top: %v", err)), nil | ||||
| 	} | ||||
| 	return NewTextResult(buf.String(), nil), nil | ||||
| 	return api.NewToolCallResult(buf.String(), nil), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	ns := ctr.GetArguments()["namespace"] | ||||
| func podsExec(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ns := params.GetArguments()["namespace"] | ||||
| 	if ns == nil { | ||||
| 		ns = "" | ||||
| 	} | ||||
| 	name := ctr.GetArguments()["name"] | ||||
| 	name := params.GetArguments()["name"] | ||||
| 	if name == nil { | ||||
| 		return NewTextResult("", errors.New("failed to exec in pod, missing argument name")), nil | ||||
| 		return api.NewToolCallResult("", errors.New("failed to exec in pod, missing argument name")), nil | ||||
| 	} | ||||
| 	container := ctr.GetArguments()["container"] | ||||
| 	container := params.GetArguments()["container"] | ||||
| 	if container == nil { | ||||
| 		container = "" | ||||
| 	} | ||||
| 	commandArg := ctr.GetArguments()["command"] | ||||
| 	commandArg := params.GetArguments()["command"] | ||||
| 	command := make([]string, 0) | ||||
| 	if _, ok := commandArg.([]interface{}); ok { | ||||
| 		for _, cmd := range commandArg.([]interface{}) { | ||||
| @@ -388,80 +367,68 @@ func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.Ca | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		return NewTextResult("", errors.New("failed to exec in pod, invalid command argument")), nil | ||||
| 		return api.NewToolCallResult("", errors.New("failed to exec in pod, invalid command argument")), nil | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.PodsExec(params, ns.(string), name.(string), container.(string), command) | ||||
| 	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 | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 	return api.NewToolCallResult(ret, err), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsLog(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	ns := ctr.GetArguments()["namespace"] | ||||
| func podsLog(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ns := params.GetArguments()["namespace"] | ||||
| 	if ns == nil { | ||||
| 		ns = "" | ||||
| 	} | ||||
| 	name := ctr.GetArguments()["name"] | ||||
| 	name := params.GetArguments()["name"] | ||||
| 	if name == nil { | ||||
| 		return NewTextResult("", errors.New("failed to get pod log, missing argument name")), nil | ||||
| 		return api.NewToolCallResult("", errors.New("failed to get pod log, missing argument name")), nil | ||||
| 	} | ||||
| 	container := ctr.GetArguments()["container"] | ||||
| 	container := params.GetArguments()["container"] | ||||
| 	if container == nil { | ||||
| 		container = "" | ||||
| 	} | ||||
| 	previous := ctr.GetArguments()["previous"] | ||||
| 	previous := params.GetArguments()["previous"] | ||||
| 	var previousBool bool | ||||
| 	if previous != nil { | ||||
| 		previousBool = previous.(bool) | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.PodsLog(params, ns.(string), name.(string), container.(string), previousBool) | ||||
| 	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 | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 	return api.NewToolCallResult(ret, err), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) podsRun(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	ns := ctr.GetArguments()["namespace"] | ||||
| func podsRun(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	ns := params.GetArguments()["namespace"] | ||||
| 	if ns == nil { | ||||
| 		ns = "" | ||||
| 	} | ||||
| 	name := ctr.GetArguments()["name"] | ||||
| 	name := params.GetArguments()["name"] | ||||
| 	if name == nil { | ||||
| 		name = "" | ||||
| 	} | ||||
| 	image := ctr.GetArguments()["image"] | ||||
| 	image := params.GetArguments()["image"] | ||||
| 	if image == nil { | ||||
| 		return NewTextResult("", errors.New("failed to run pod, missing argument image")), nil | ||||
| 		return api.NewToolCallResult("", errors.New("failed to run pod, missing argument image")), nil | ||||
| 	} | ||||
| 	port := ctr.GetArguments()["port"] | ||||
| 	port := params.GetArguments()["port"] | ||||
| 	if port == nil { | ||||
| 		port = float64(0) | ||||
| 	} | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	resources, err := params.PodsRun(params, ns.(string), name.(string), image.(string), int32(port.(float64))) | ||||
| 	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 | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 	return api.NewToolCallResult("# The following resources (YAML) have been created or updated successfully\n"+marshalledYaml, err), nil | ||||
| } | ||||
| @@ -1,4 +1,4 @@ | ||||
| package mcp | ||||
| package full | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| @@ -6,22 +6,23 @@ import ( | ||||
| 	"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/api" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/kubernetes" | ||||
| 	internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/output" | ||||
| ) | ||||
| 
 | ||||
| func (s *Server) initResources() []ServerTool { | ||||
| func initResources(k *internalk8s.Manager) []api.ServerTool { | ||||
| 	commonApiVersion := "v1 Pod, v1 Service, v1 Node, apps/v1 Deployment, networking.k8s.io/v1 Ingress" | ||||
| 	if s.k.IsOpenShift(context.Background()) { | ||||
| 	if k.IsOpenShift(context.Background()) { | ||||
| 		commonApiVersion += ", route.openshift.io/v1 Route" | ||||
| 	} | ||||
| 	commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion) | ||||
| 	return []ServerTool{ | ||||
| 		{Tool: Tool{ | ||||
| 	return []api.ServerTool{ | ||||
| 		{Tool: api.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{ | ||||
| @@ -47,15 +48,15 @@ func (s *Server) initResources() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"apiVersion", "kind"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: resourcesList}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -80,15 +81,15 @@ func (s *Server) initResources() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"apiVersion", "kind", "name"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: resourcesGet}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -101,15 +102,15 @@ func (s *Server) initResources() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"resource"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.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{ | ||||
| 		}, Handler: resourcesCreateOrUpdate}, | ||||
| 		{Tool: api.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{ | ||||
| @@ -134,149 +135,133 @@ func (s *Server) initResources() []ServerTool { | ||||
| 				}, | ||||
| 				Required: []string{"apiVersion", "kind", "name"}, | ||||
| 			}, | ||||
| 			Annotations: ToolAnnotations{ | ||||
| 			Annotations: api.ToolAnnotations{ | ||||
| 				Title:           "Resources: Delete", | ||||
| 				ReadOnlyHint:    ptr.To(false), | ||||
| 				DestructiveHint: ptr.To(true), | ||||
| 				IdempotentHint:  ptr.To(true), | ||||
| 				OpenWorldHint:   ptr.To(true), | ||||
| 			}, | ||||
| 		}, Handler: s.resourcesDelete}, | ||||
| 		}, Handler: resourcesDelete}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (s *Server) resourcesList(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	namespace := ctr.GetArguments()["namespace"] | ||||
| func resourcesList(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	namespace := params.GetArguments()["namespace"] | ||||
| 	if namespace == nil { | ||||
| 		namespace = "" | ||||
| 	} | ||||
| 	labelSelector := ctr.GetArguments()["labelSelector"] | ||||
| 	labelSelector := params.GetArguments()["labelSelector"] | ||||
| 	resourceListOptions := kubernetes.ResourceListOptions{ | ||||
| 		AsTable: s.configuration.ListOutput.AsTable(), | ||||
| 		AsTable: params.ListOutput.AsTable(), | ||||
| 	} | ||||
| 
 | ||||
| 	if labelSelector != nil { | ||||
| 		l, ok := labelSelector.(string) | ||||
| 		if !ok { | ||||
| 			return NewTextResult("", fmt.Errorf("labelSelector is not a string")), nil | ||||
| 			return api.NewToolCallResult("", fmt.Errorf("labelSelector is not a string")), nil | ||||
| 		} | ||||
| 		resourceListOptions.LabelSelector = l | ||||
| 	} | ||||
| 	gvk, err := parseGroupVersionKind(ctr.GetArguments()) | ||||
| 	gvk, err := parseGroupVersionKind(params.GetArguments()) | ||||
| 	if err != nil { | ||||
| 		return NewTextResult("", fmt.Errorf("failed to list resources, %s", err)), nil | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil | ||||
| 	} | ||||
| 
 | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.ResourcesList(params, gvk, ns, resourceListOptions) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to list resources: %v", err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(params.ListOutput.PrintObj(ret)), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) resourcesGet(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	namespace := ctr.GetArguments()["namespace"] | ||||
| func resourcesGet(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	namespace := params.GetArguments()["namespace"] | ||||
| 	if namespace == nil { | ||||
| 		namespace = "" | ||||
| 	} | ||||
| 	gvk, err := parseGroupVersionKind(ctr.GetArguments()) | ||||
| 	gvk, err := parseGroupVersionKind(params.GetArguments()) | ||||
| 	if err != nil { | ||||
| 		return NewTextResult("", fmt.Errorf("failed to get resource, %s", err)), nil | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to get resource, %s", err)), nil | ||||
| 	} | ||||
| 	name := ctr.GetArguments()["name"] | ||||
| 	name := params.GetArguments()["name"] | ||||
| 	if name == nil { | ||||
| 		return NewTextResult("", errors.New("failed to get resource, missing argument name")), nil | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil | ||||
| 	} | ||||
| 
 | ||||
| 	n, ok := name.(string) | ||||
| 	if !ok { | ||||
| 		return NewTextResult("", fmt.Errorf("name is not a string")), nil | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("name is not a string")), nil | ||||
| 	} | ||||
| 
 | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	ret, err := params.ResourcesGet(params, gvk, ns, n) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to get resource: %v", err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult(output.MarshalYaml(ret)), nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) resourcesCreateOrUpdate(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||||
| 	resource := ctr.GetArguments()["resource"] | ||||
| func resourcesCreateOrUpdate(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	resource := params.GetArguments()["resource"] | ||||
| 	if resource == nil || resource == "" { | ||||
| 		return NewTextResult("", errors.New("failed to create or update resources, missing argument resource")), nil | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("resource is not a string")), nil | ||||
| 	} | ||||
| 
 | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	resources, err := params.ResourcesCreateOrUpdate(params, r) | ||||
| 	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 | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 	return api.NewToolCallResult("# 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"] | ||||
| func resourcesDelete(params api.ToolHandlerParams) (*api.ToolCallResult, error) { | ||||
| 	namespace := params.GetArguments()["namespace"] | ||||
| 	if namespace == nil { | ||||
| 		namespace = "" | ||||
| 	} | ||||
| 	gvk, err := parseGroupVersionKind(ctr.GetArguments()) | ||||
| 	gvk, err := parseGroupVersionKind(params.GetArguments()) | ||||
| 	if err != nil { | ||||
| 		return NewTextResult("", fmt.Errorf("failed to delete resource, %s", err)), nil | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to delete resource, %s", err)), nil | ||||
| 	} | ||||
| 	name := ctr.GetArguments()["name"] | ||||
| 	name := params.GetArguments()["name"] | ||||
| 	if name == nil { | ||||
| 		return NewTextResult("", errors.New("failed to delete resource, missing argument name")), nil | ||||
| 		return api.NewToolCallResult("", 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 | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("namespace is not a string")), nil | ||||
| 	} | ||||
| 
 | ||||
| 	n, ok := name.(string) | ||||
| 	if !ok { | ||||
| 		return NewTextResult("", fmt.Errorf("name is not a string")), nil | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("name is not a string")), nil | ||||
| 	} | ||||
| 
 | ||||
| 	derived, err := s.k.Derived(ctx) | ||||
| 	err = params.ResourcesDelete(params, gvk, ns, n) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 		return api.NewToolCallResult("", fmt.Errorf("failed to delete resource: %v", err)), nil | ||||
| 	} | ||||
| 	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 | ||||
| 	return api.NewToolCallResult("Resource deleted successfully", err), nil | ||||
| } | ||||
| 
 | ||||
| func parseGroupVersionKind(arguments map[string]interface{}) (*schema.GroupVersionKind, error) { | ||||
							
								
								
									
										36
									
								
								pkg/toolsets/full/toolset.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								pkg/toolsets/full/toolset.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| package full | ||||
|  | ||||
| import ( | ||||
| 	"slices" | ||||
|  | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/api" | ||||
| 	internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/toolsets" | ||||
| ) | ||||
|  | ||||
| type Full struct{} | ||||
|  | ||||
| var _ api.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(k *internalk8s.Manager) []api.ServerTool { | ||||
| 	return slices.Concat( | ||||
| 		initConfiguration(), | ||||
| 		initEvents(), | ||||
| 		initNamespaces(k), | ||||
| 		initPods(), | ||||
| 		initResources(k), | ||||
| 		initHelm(), | ||||
| 	) | ||||
| } | ||||
|  | ||||
| func init() { | ||||
| 	toolsets.Register(&Full{}) | ||||
| } | ||||
							
								
								
									
										40
									
								
								pkg/toolsets/toolsets.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								pkg/toolsets/toolsets.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| package toolsets | ||||
|  | ||||
| import ( | ||||
| 	"slices" | ||||
|  | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/api" | ||||
| ) | ||||
|  | ||||
| var toolsets []api.Toolset | ||||
|  | ||||
| // Clear removes all registered toolsets, TESTING PURPOSES ONLY. | ||||
| func Clear() { | ||||
| 	toolsets = []api.Toolset{} | ||||
| } | ||||
|  | ||||
| func Register(toolset api.Toolset) { | ||||
| 	toolsets = append(toolsets, toolset) | ||||
| } | ||||
|  | ||||
| func Toolsets() []api.Toolset { | ||||
| 	return toolsets | ||||
| } | ||||
|  | ||||
| func ToolsetNames() []string { | ||||
| 	names := make([]string, 0) | ||||
| 	for _, toolset := range Toolsets() { | ||||
| 		names = append(names, toolset.GetName()) | ||||
| 	} | ||||
| 	slices.Sort(names) | ||||
| 	return names | ||||
| } | ||||
|  | ||||
| func ToolsetFromString(name string) api.Toolset { | ||||
| 	for _, toolset := range Toolsets() { | ||||
| 		if toolset.GetName() == name { | ||||
| 			return toolset | ||||
| 		} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
							
								
								
									
										60
									
								
								pkg/toolsets/toolsets_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								pkg/toolsets/toolsets_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| package toolsets | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/api" | ||||
| 	"github.com/containers/kubernetes-mcp-server/pkg/kubernetes" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| ) | ||||
|  | ||||
| type ToolsetsSuite struct { | ||||
| 	suite.Suite | ||||
| } | ||||
|  | ||||
| func (s *ToolsetsSuite) SetupTest() { | ||||
| 	Clear() | ||||
| } | ||||
|  | ||||
| type TestToolset struct { | ||||
| 	name        string | ||||
| 	description string | ||||
| } | ||||
|  | ||||
| func (t *TestToolset) GetName() string { return t.name } | ||||
|  | ||||
| func (t *TestToolset) GetDescription() string { return t.description } | ||||
|  | ||||
| func (t *TestToolset) GetTools(k *kubernetes.Manager) []api.ServerTool { return nil } | ||||
|  | ||||
| var _ api.Toolset = (*TestToolset)(nil) | ||||
|  | ||||
| func (s *ToolsetsSuite) TestToolsetNames() { | ||||
| 	s.Run("Returns empty list if no toolsets registered", func() { | ||||
| 		s.Empty(ToolsetNames(), "Expected empty list of toolset names") | ||||
| 	}) | ||||
|  | ||||
| 	Register(&TestToolset{name: "z"}) | ||||
| 	Register(&TestToolset{name: "b"}) | ||||
| 	Register(&TestToolset{name: "1"}) | ||||
| 	s.Run("Returns sorted list of registered toolset names", func() { | ||||
| 		names := ToolsetNames() | ||||
| 		s.Equal([]string{"1", "b", "z"}, names, "Expected sorted list of toolset names") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func (s *ToolsetsSuite) TestToolsetFromString() { | ||||
| 	s.Run("Returns nil if toolset not found", func() { | ||||
| 		s.Nil(ToolsetFromString("non-existent"), "Expected nil for non-existent toolset") | ||||
| 	}) | ||||
| 	s.Run("Returns the correct toolset if found", func() { | ||||
| 		Register(&TestToolset{name: "existent"}) | ||||
| 		res := ToolsetFromString("existent") | ||||
| 		s.NotNil(res, "Expected to find the registered toolset") | ||||
| 		s.Equal("existent", res.GetName(), "Expected to find the registered toolset by name") | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestToolsets(t *testing.T) { | ||||
| 	suite.Run(t, new(ToolsetsSuite)) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Marc Nuri
					Marc Nuri