diff --git a/go.mod b/go.mod index 44ea478..91e813b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/coreos/go-oidc/v3 v3.15.0 github.com/fsnotify/fsnotify v1.9.0 github.com/go-jose/go-jose/v4 v4.1.2 + github.com/google/jsonschema-go v0.2.2 github.com/mark3labs/mcp-go v0.39.1 github.com/pkg/errors v0.9.1 github.com/spf13/afero v1.15.0 diff --git a/go.sum b/go.sum index 9ab4c1d..8a4972d 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/jsonschema-go v0.2.2 h1:qb9KM/pATIqIPuE9gEDwPsco8HHCTlA88IGFYHDl03A= +github.com/google/jsonschema-go v0.2.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/pkg/http/http_test.go b/pkg/http/http_test.go index b481157..da04caa 100644 --- a/pkg/http/http_test.go +++ b/pkg/http/http_test.go @@ -87,7 +87,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: mcp.Toolsets()[0], StaticConfig: c.StaticConfig, }) if err != nil { diff --git a/pkg/kubernetes-mcp-server/cmd/root.go b/pkg/kubernetes-mcp-server/cmd/root.go index 9534c24..5575391 100644 --- a/pkg/kubernetes-mcp-server/cmd/root.go +++ b/pkg/kubernetes-mcp-server/cmd/root.go @@ -115,7 +115,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(mcp.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") @@ -239,7 +239,7 @@ func (m *MCPServerOptions) Validate() error { func (m *MCPServerOptions) Run() error { toolset := mcp.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(mcp.ToolsetNames(), ", ")) } listOutput := output.FromString(m.StaticConfig.ListOutput) if listOutput == nil { diff --git a/pkg/mcp/configuration.go b/pkg/mcp/configuration.go index 79ebaef..237c26c 100644 --- a/pkg/mcp/configuration.go +++ b/pkg/mcp/configuration.go @@ -4,26 +4,38 @@ import ( "context" "fmt" + "github.com/google/jsonschema-go/jsonschema" "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "k8s.io/utils/ptr" "github.com/containers/kubernetes-mcp-server/pkg/output" ) -func (s *Server) initConfiguration() []server.ServerTool { - tools := []server.ServerTool{ - {Tool: mcp.NewTool("configuration_view", - mcp.WithDescription("Get the current Kubernetes configuration content as a kubeconfig YAML"), - mcp.WithBoolean("minified", mcp.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)")), - // Tool annotations - mcp.WithTitleAnnotation("Configuration: View"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.configurationView}, +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 } diff --git a/pkg/mcp/events.go b/pkg/mcp/events.go index 4429a6c..22668a9 100644 --- a/pkg/mcp/events.go +++ b/pkg/mcp/events.go @@ -4,24 +4,35 @@ import ( "context" "fmt" + "github.com/google/jsonschema-go/jsonschema" "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "k8s.io/utils/ptr" "github.com/containers/kubernetes-mcp-server/pkg/output" ) -func (s *Server) initEvents() []server.ServerTool { - return []server.ServerTool{ - {Tool: mcp.NewTool("events_list", - mcp.WithDescription("List all the Kubernetes events in the current cluster from all namespaces"), - mcp.WithString("namespace", - mcp.Description("Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces")), - // Tool annotations - mcp.WithTitleAnnotation("Events: List"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.eventsList}, +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}, } } diff --git a/pkg/mcp/helm.go b/pkg/mcp/helm.go index e265965..ce04d17 100644 --- a/pkg/mcp/helm.go +++ b/pkg/mcp/helm.go @@ -4,46 +4,96 @@ import ( "context" "fmt" + "github.com/google/jsonschema-go/jsonschema" "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "k8s.io/utils/ptr" ) -func (s *Server) initHelm() []server.ServerTool { - return []server.ServerTool{ - {Tool: mcp.NewTool("helm_install", - mcp.WithDescription("Install a Helm chart in the current or provided namespace"), - mcp.WithString("chart", mcp.Description("Chart reference to install (for example: stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress)"), mcp.Required()), - mcp.WithObject("values", mcp.Description("Values to pass to the Helm chart (Optional)")), - mcp.WithString("name", mcp.Description("Name of the Helm release (Optional, random name if not provided)")), - mcp.WithString("namespace", mcp.Description("Namespace to install the Helm chart in (Optional, current namespace if not provided)")), - // Tool annotations - mcp.WithTitleAnnotation("Helm: Install"), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithIdempotentHintAnnotation(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.helmInstall}, - {Tool: mcp.NewTool("helm_list", - mcp.WithDescription("List all the Helm releases in the current or provided namespace (or in all namespaces if specified)"), - mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from (Optional, all namespaces if not provided)")), - mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")), - // Tool annotations - mcp.WithTitleAnnotation("Helm: List"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.helmList}, - {Tool: mcp.NewTool("helm_uninstall", - mcp.WithDescription("Uninstall a Helm release in the current or provided namespace"), - mcp.WithString("name", mcp.Description("Name of the Helm release to uninstall"), mcp.Required()), - mcp.WithString("namespace", mcp.Description("Namespace to uninstall the Helm release from (Optional, current namespace if not provided)")), - // Tool annotations - mcp.WithTitleAnnotation("Helm: Uninstall"), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithIdempotentHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.helmUninstall}, +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}, } } diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 0a93b0f..f0750d0 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -30,7 +30,7 @@ type Configuration struct { StaticConfig *config.StaticConfig } -func (c *Configuration) isToolApplicable(tool server.ServerTool) bool { +func (c *Configuration) isToolApplicable(tool ServerTool) bool { if c.StaticConfig.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) { return false } @@ -88,7 +88,7 @@ func (s *Server) reloadKubernetesClient() error { return err } s.k = k - applicableTools := make([]server.ServerTool, 0) + applicableTools := make([]ServerTool, 0) for _, tool := range s.configuration.Toolset.GetTools(s) { if !s.configuration.isToolApplicable(tool) { continue @@ -96,7 +96,11 @@ func (s *Server) reloadKubernetesClient() error { applicableTools = append(applicableTools, tool) s.enabledTools = append(s.enabledTools, tool.Tool.Name) } - s.server.SetTools(applicableTools...) + m3labsServerTools, err := ServerToolToM3LabsServerTool(applicableTools) + if err != nil { + return fmt.Errorf("failed to convert tools: %v", err) + } + s.server.SetTools(m3labsServerTools...) return nil } diff --git a/pkg/mcp/namespaces.go b/pkg/mcp/namespaces.go index a289015..b752b74 100644 --- a/pkg/mcp/namespaces.go +++ b/pkg/mcp/namespaces.go @@ -4,34 +4,47 @@ import ( "context" "fmt" + "github.com/google/jsonschema-go/jsonschema" "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" + "k8s.io/utils/ptr" "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" ) -func (s *Server) initNamespaces() []server.ServerTool { - ret := make([]server.ServerTool, 0) - ret = append(ret, server.ServerTool{ - Tool: mcp.NewTool("namespaces_list", - mcp.WithDescription("List all the Kubernetes namespaces in the current cluster"), - // Tool annotations - mcp.WithTitleAnnotation("Namespaces: List"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.namespacesList, +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, server.ServerTool{ - Tool: mcp.NewTool("projects_list", - mcp.WithDescription("List all the OpenShift projects in the current cluster"), - // Tool annotations - mcp.WithTitleAnnotation("Projects: List"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.projectsList, + 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 diff --git a/pkg/mcp/pods.go b/pkg/mcp/pods.go index a29f992..331e288 100644 --- a/pkg/mcp/pods.go +++ b/pkg/mcp/pods.go @@ -6,119 +6,250 @@ import ( "errors" "fmt" + "github.com/google/jsonschema-go/jsonschema" "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" "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() []server.ServerTool { - return []server.ServerTool{ - {Tool: mcp.NewTool("pods_list", - mcp.WithDescription("List all the Kubernetes pods in the current cluster from all namespaces"), - mcp.WithString("labelSelector", mcp.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"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")), - // Tool annotations - mcp.WithTitleAnnotation("Pods: List"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsListInAllNamespaces}, - {Tool: mcp.NewTool("pods_list_in_namespace", - mcp.WithDescription("List all the Kubernetes pods in the specified namespace in the current cluster"), - mcp.WithString("namespace", mcp.Description("Namespace to list pods from"), mcp.Required()), - mcp.WithString("labelSelector", mcp.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"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")), - // Tool annotations - mcp.WithTitleAnnotation("Pods: List in Namespace"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsListInNamespace}, - {Tool: mcp.NewTool("pods_get", - mcp.WithDescription("Get a Kubernetes Pod in the current or provided namespace with the provided name"), - mcp.WithString("namespace", mcp.Description("Namespace to get the Pod from")), - mcp.WithString("name", mcp.Description("Name of the Pod"), mcp.Required()), - // Tool annotations - mcp.WithTitleAnnotation("Pods: Get"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsGet}, - {Tool: mcp.NewTool("pods_delete", - mcp.WithDescription("Delete a Kubernetes Pod in the current or provided namespace with the provided name"), - mcp.WithString("namespace", mcp.Description("Namespace to delete the Pod from")), - mcp.WithString("name", mcp.Description("Name of the Pod to delete"), mcp.Required()), - // Tool annotations - mcp.WithTitleAnnotation("Pods: Delete"), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithIdempotentHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsDelete}, - {Tool: mcp.NewTool("pods_top", - mcp.WithDescription("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"), - mcp.WithBoolean("all_namespaces", mcp.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"), mcp.DefaultBool(true)), - mcp.WithString("namespace", mcp.Description("Namespace to get the Pods resource consumption from (Optional, current namespace if not provided and all_namespaces is false)")), - mcp.WithString("name", mcp.Description("Name of the Pod to get the resource consumption from (Optional, all Pods in the namespace if not provided)")), - mcp.WithString("label_selector", mcp.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)"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")), - // Tool annotations - mcp.WithTitleAnnotation("Pods: Top"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithIdempotentHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsTop}, - {Tool: mcp.NewTool("pods_exec", - mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"), - mcp.WithString("namespace", mcp.Description("Namespace of the Pod where the command will be executed")), - mcp.WithString("name", mcp.Description("Name of the Pod where the command will be executed"), mcp.Required()), - mcp.WithArray("command", mcp.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"]`), - // TODO: manual fix to ensure that the items property gets initialized (Gemini) - // https://www.googlecloudcommunity.com/gc/AI-ML/Gemini-API-400-Bad-Request-Array-fields-breaks-function-calling/m-p/769835?nobounce - func(schema map[string]interface{}) { - schema["type"] = "array" - schema["items"] = map[string]interface{}{ - "type": "string", - } +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]", + }, }, - mcp.Required(), - ), - mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")), - // Tool annotations - mcp.WithTitleAnnotation("Pods: Exec"), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithDestructiveHintAnnotation(true), // Depending on the Pod's entrypoint, executing certain commands may kill the Pod - mcp.WithIdempotentHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsExec}, - {Tool: mcp.NewTool("pods_log", - mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"), - mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")), - mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()), - mcp.WithString("container", mcp.Description("Name of the Pod container to get the logs from (Optional)")), - mcp.WithBoolean("previous", mcp.Description("Return previous terminated container logs (Optional)")), - // Tool annotations - mcp.WithTitleAnnotation("Pods: Log"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsLog}, - {Tool: mcp.NewTool("pods_run", - mcp.WithDescription("Run a Kubernetes Pod in the current or provided namespace with the provided container image and optional name"), - mcp.WithString("namespace", mcp.Description("Namespace to run the Pod in")), - mcp.WithString("name", mcp.Description("Name of the Pod (Optional, random name if not provided)")), - mcp.WithString("image", mcp.Description("Container Image to run in the Pod"), mcp.Required()), - mcp.WithNumber("port", mcp.Description("TCP/IP port to expose from the Pod container (Optional, no port exposed if not provided)")), - // Tool annotations - mcp.WithTitleAnnotation("Pods: Run"), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithIdempotentHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.podsRun}, + }, + 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}, } } diff --git a/pkg/mcp/resources.go b/pkg/mcp/resources.go index dfb99dd..eb8550a 100644 --- a/pkg/mcp/resources.go +++ b/pkg/mcp/resources.go @@ -5,99 +5,143 @@ import ( "errors" "fmt" + "github.com/google/jsonschema-go/jsonschema" "github.com/mark3labs/mcp-go/mcp" - "github.com/mark3labs/mcp-go/server" "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() []server.ServerTool { +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 []server.ServerTool{ - {Tool: mcp.NewTool("resources_list", - mcp.WithDescription("List Kubernetes resources and objects in the current cluster by providing their apiVersion and kind and optionally the namespace and label selector\n"+ - commonApiVersion), - mcp.WithString("apiVersion", - mcp.Description("apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"), - mcp.Required(), - ), - mcp.WithString("kind", - mcp.Description("kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)"), - mcp.Required(), - ), - mcp.WithString("namespace", - mcp.Description("Optional Namespace to retrieve the namespaced resources from (ignored in case of cluster scoped resources). If not provided, will list resources from all namespaces")), - mcp.WithString("labelSelector", - mcp.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"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")), - // Tool annotations - mcp.WithTitleAnnotation("Resources: List"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.resourcesList}, - {Tool: mcp.NewTool("resources_get", - mcp.WithDescription("Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+ - commonApiVersion), - mcp.WithString("apiVersion", - mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"), - mcp.Required(), - ), - mcp.WithString("kind", - mcp.Description("kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)"), - mcp.Required(), - ), - mcp.WithString("namespace", - mcp.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"), - ), - mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()), - // Tool annotations - mcp.WithTitleAnnotation("Resources: Get"), - mcp.WithReadOnlyHintAnnotation(true), - mcp.WithDestructiveHintAnnotation(false), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.resourcesGet}, - {Tool: mcp.NewTool("resources_create_or_update", - mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n"+ - commonApiVersion), - mcp.WithString("resource", - mcp.Description("A JSON or YAML containing a representation of the Kubernetes resource. Should include top-level fields such as apiVersion,kind,metadata, and spec"), - mcp.Required(), - ), - // Tool annotations - mcp.WithTitleAnnotation("Resources: Create or Update"), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithIdempotentHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.resourcesCreateOrUpdate}, - {Tool: mcp.NewTool("resources_delete", - mcp.WithDescription("Delete a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n"+ - commonApiVersion), - mcp.WithString("apiVersion", - mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"), - mcp.Required(), - ), - mcp.WithString("kind", - mcp.Description("kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)"), - mcp.Required(), - ), - mcp.WithString("namespace", - mcp.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"), - ), - mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()), - // Tool annotations - mcp.WithTitleAnnotation("Resources: Delete"), - mcp.WithReadOnlyHintAnnotation(false), - mcp.WithDestructiveHintAnnotation(true), - mcp.WithIdempotentHintAnnotation(true), - mcp.WithOpenWorldHintAnnotation(true), - ), Handler: s.resourcesDelete}, + 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}, } } diff --git a/pkg/mcp/testdata/toolsets-full-tools-openshift.json b/pkg/mcp/testdata/toolsets-full-tools-openshift.json index f557c7e..dae607c 100644 --- a/pkg/mcp/testdata/toolsets-full-tools-openshift.json +++ b/pkg/mcp/testdata/toolsets-full-tools-openshift.json @@ -65,7 +65,6 @@ }, "values": { "description": "Values to pass to the Helm chart (Optional)", - "properties": {}, "type": "object" } }, diff --git a/pkg/mcp/testdata/toolsets-full-tools.json b/pkg/mcp/testdata/toolsets-full-tools.json index 190f214..a606539 100644 --- a/pkg/mcp/testdata/toolsets-full-tools.json +++ b/pkg/mcp/testdata/toolsets-full-tools.json @@ -65,7 +65,6 @@ }, "values": { "description": "Values to pass to the Helm chart (Optional)", - "properties": {}, "type": "object" } }, diff --git a/pkg/mcp/toolsets.go b/pkg/mcp/toolsets.go index 9601080..cc5481a 100644 --- a/pkg/mcp/toolsets.go +++ b/pkg/mcp/toolsets.go @@ -1,25 +1,86 @@ 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) []server.ServerTool + GetTools(s *Server) []ServerTool } -var Toolsets = []Toolset{ - &Full{}, +type ServerTool struct { + Tool Tool + Handler ToolHandlerFunc } -var ToolsetNames []string +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 { + for _, toolset := range Toolsets() { if toolset.GetName() == name { return toolset } @@ -27,15 +88,54 @@ func ToolsetFromString(name string) 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) []server.ServerTool { +func (p *Full) GetTools(s *Server) []ServerTool { return slices.Concat( s.initConfiguration(), s.initEvents(), @@ -47,8 +147,5 @@ func (p *Full) GetTools(s *Server) []server.ServerTool { } func init() { - ToolsetNames = make([]string, 0) - for _, toolset := range Toolsets { - ToolsetNames = append(ToolsetNames, toolset.GetName()) - } + Register(&Full{}) }