refactor(mcp): toolset Tools definition is agnostic of MCP impl (#319)

Initial PR to make the toolsets agnostic of the usd MCP implementation (migration to go-sdk).
The decoupling will also be needed to move the different toolsets to separate nested packages (toolsets).

Signed-off-by: Marc Nuri <marc@marcnuri.com>
This commit is contained in:
Marc Nuri
2025-09-12 09:58:54 +02:00
committed by GitHub
parent 4361a9e7d8
commit 2b6c886d95
14 changed files with 653 additions and 290 deletions

1
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/coreos/go-oidc/v3 v3.15.0 github.com/coreos/go-oidc/v3 v3.15.0
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/go-jose/go-jose/v4 v4.1.2 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/mark3labs/mcp-go v0.39.1
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/spf13/afero v1.15.0 github.com/spf13/afero v1.15.0

2
go.sum
View File

@@ -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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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/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 h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

View File

@@ -87,7 +87,7 @@ func (c *httpContext) beforeEach(t *testing.T) {
} }
c.StaticConfig.Port = fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port) c.StaticConfig.Port = fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
mcpServer, err := mcp.NewServer(mcp.Configuration{ mcpServer, err := mcp.NewServer(mcp.Configuration{
Toolset: mcp.Toolsets[0], Toolset: mcp.Toolsets()[0],
StaticConfig: c.StaticConfig, StaticConfig: c.StaticConfig,
}) })
if err != nil { if err != nil {

View File

@@ -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.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.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.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().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.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") 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 { func (m *MCPServerOptions) Run() error {
toolset := mcp.ToolsetFromString(m.Toolset) toolset := mcp.ToolsetFromString(m.Toolset)
if toolset == nil { 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) listOutput := output.FromString(m.StaticConfig.ListOutput)
if listOutput == nil { if listOutput == nil {

View File

@@ -4,26 +4,38 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/output" "github.com/containers/kubernetes-mcp-server/pkg/output"
) )
func (s *Server) initConfiguration() []server.ServerTool { func (s *Server) initConfiguration() []ServerTool {
tools := []server.ServerTool{ tools := []ServerTool{
{Tool: mcp.NewTool("configuration_view", {Tool: Tool{
mcp.WithDescription("Get the current Kubernetes configuration content as a kubeconfig YAML"), Name: "configuration_view",
mcp.WithBoolean("minified", mcp.Description("Return a minified version of the configuration. "+ Description: "Get the current Kubernetes configuration content as a kubeconfig YAML",
"If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. "+ InputSchema: &jsonschema.Schema{
"If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. "+ Type: "object",
"(Optional, default true)")), Properties: map[string]*jsonschema.Schema{
// Tool annotations "minified": {
mcp.WithTitleAnnotation("Configuration: View"), Type: "boolean",
mcp.WithReadOnlyHintAnnotation(true), Description: "Return a minified version of the configuration. " +
mcp.WithDestructiveHintAnnotation(false), "If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. " +
mcp.WithOpenWorldHintAnnotation(true), "If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. " +
), Handler: s.configurationView}, "(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 return tools
} }

View File

@@ -4,24 +4,35 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/output" "github.com/containers/kubernetes-mcp-server/pkg/output"
) )
func (s *Server) initEvents() []server.ServerTool { func (s *Server) initEvents() []ServerTool {
return []server.ServerTool{ return []ServerTool{
{Tool: mcp.NewTool("events_list", {Tool: Tool{
mcp.WithDescription("List all the Kubernetes events in the current cluster from all namespaces"), Name: "events_list",
mcp.WithString("namespace", Description: "List all the Kubernetes events in the current cluster from all namespaces",
mcp.Description("Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces")), InputSchema: &jsonschema.Schema{
// Tool annotations Type: "object",
mcp.WithTitleAnnotation("Events: List"), Properties: map[string]*jsonschema.Schema{
mcp.WithReadOnlyHintAnnotation(true), "namespace": {
mcp.WithDestructiveHintAnnotation(false), Type: "string",
mcp.WithOpenWorldHintAnnotation(true), Description: "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
), Handler: s.eventsList}, },
},
},
Annotations: ToolAnnotations{
Title: "Events: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.eventsList},
} }
} }

View File

@@ -4,46 +4,96 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "k8s.io/utils/ptr"
) )
func (s *Server) initHelm() []server.ServerTool { func (s *Server) initHelm() []ServerTool {
return []server.ServerTool{ return []ServerTool{
{Tool: mcp.NewTool("helm_install", {Tool: Tool{
mcp.WithDescription("Install a Helm chart in the current or provided namespace"), Name: "helm_install",
mcp.WithString("chart", mcp.Description("Chart reference to install (for example: stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress)"), mcp.Required()), Description: "Install a Helm chart in the current or provided namespace",
mcp.WithObject("values", mcp.Description("Values to pass to the Helm chart (Optional)")), InputSchema: &jsonschema.Schema{
mcp.WithString("name", mcp.Description("Name of the Helm release (Optional, random name if not provided)")), Type: "object",
mcp.WithString("namespace", mcp.Description("Namespace to install the Helm chart in (Optional, current namespace if not provided)")), Properties: map[string]*jsonschema.Schema{
// Tool annotations "chart": {
mcp.WithTitleAnnotation("Helm: Install"), Type: "string",
mcp.WithReadOnlyHintAnnotation(false), Description: "Chart reference to install (for example: stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress)",
mcp.WithDestructiveHintAnnotation(false), },
mcp.WithIdempotentHintAnnotation(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install "values": {
mcp.WithOpenWorldHintAnnotation(true), Type: "object",
), Handler: s.helmInstall}, Description: "Values to pass to the Helm chart (Optional)",
{Tool: mcp.NewTool("helm_list", Properties: make(map[string]*jsonschema.Schema),
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)")), "name": {
mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")), Type: "string",
// Tool annotations Description: "Name of the Helm release (Optional, random name if not provided)",
mcp.WithTitleAnnotation("Helm: List"), },
mcp.WithReadOnlyHintAnnotation(true), "namespace": {
mcp.WithDestructiveHintAnnotation(false), Type: "string",
mcp.WithOpenWorldHintAnnotation(true), Description: "Namespace to install the Helm chart in (Optional, current namespace if not provided)",
), Handler: s.helmList}, },
{Tool: mcp.NewTool("helm_uninstall", },
mcp.WithDescription("Uninstall a Helm release in the current or provided namespace"), Required: []string{"chart"},
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)")), Annotations: ToolAnnotations{
// Tool annotations Title: "Helm: Install",
mcp.WithTitleAnnotation("Helm: Uninstall"), ReadOnlyHint: ptr.To(false),
mcp.WithReadOnlyHintAnnotation(false), DestructiveHint: ptr.To(false),
mcp.WithDestructiveHintAnnotation(true), IdempotentHint: ptr.To(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
mcp.WithIdempotentHintAnnotation(true), OpenWorldHint: ptr.To(true),
mcp.WithOpenWorldHintAnnotation(true), },
), Handler: s.helmUninstall}, }, 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},
} }
} }

View File

@@ -30,7 +30,7 @@ type Configuration struct {
StaticConfig *config.StaticConfig 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) { if c.StaticConfig.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) {
return false return false
} }
@@ -88,7 +88,7 @@ func (s *Server) reloadKubernetesClient() error {
return err return err
} }
s.k = k s.k = k
applicableTools := make([]server.ServerTool, 0) applicableTools := make([]ServerTool, 0)
for _, tool := range s.configuration.Toolset.GetTools(s) { for _, tool := range s.configuration.Toolset.GetTools(s) {
if !s.configuration.isToolApplicable(tool) { if !s.configuration.isToolApplicable(tool) {
continue continue
@@ -96,7 +96,11 @@ func (s *Server) reloadKubernetesClient() error {
applicableTools = append(applicableTools, tool) applicableTools = append(applicableTools, tool)
s.enabledTools = append(s.enabledTools, tool.Tool.Name) 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 return nil
} }

View File

@@ -4,34 +4,47 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
) )
func (s *Server) initNamespaces() []server.ServerTool { func (s *Server) initNamespaces() []ServerTool {
ret := make([]server.ServerTool, 0) ret := make([]ServerTool, 0)
ret = append(ret, server.ServerTool{ ret = append(ret, ServerTool{
Tool: mcp.NewTool("namespaces_list", Tool: Tool{
mcp.WithDescription("List all the Kubernetes namespaces in the current cluster"), Name: "namespaces_list",
// Tool annotations Description: "List all the Kubernetes namespaces in the current cluster",
mcp.WithTitleAnnotation("Namespaces: List"), InputSchema: &jsonschema.Schema{
mcp.WithReadOnlyHintAnnotation(true), Type: "object",
mcp.WithDestructiveHintAnnotation(false), },
mcp.WithOpenWorldHintAnnotation(true), Annotations: ToolAnnotations{
), Handler: s.namespacesList, 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()) { if s.k.IsOpenShift(context.Background()) {
ret = append(ret, server.ServerTool{ ret = append(ret, ServerTool{
Tool: mcp.NewTool("projects_list", Tool: Tool{
mcp.WithDescription("List all the OpenShift projects in the current cluster"), Name: "projects_list",
// Tool annotations Description: "List all the OpenShift projects in the current cluster",
mcp.WithTitleAnnotation("Projects: List"), InputSchema: &jsonschema.Schema{
mcp.WithReadOnlyHintAnnotation(true), Type: "object",
mcp.WithDestructiveHintAnnotation(false), },
mcp.WithOpenWorldHintAnnotation(true), Annotations: ToolAnnotations{
), Handler: s.projectsList, Title: "Projects: List",
ReadOnlyHint: ptr.To(true),
DestructiveHint: ptr.To(false),
IdempotentHint: ptr.To(false),
OpenWorldHint: ptr.To(true),
},
}, Handler: s.projectsList,
}) })
} }
return ret return ret

View File

@@ -6,119 +6,250 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"k8s.io/kubectl/pkg/metricsutil" "k8s.io/kubectl/pkg/metricsutil"
"k8s.io/utils/ptr"
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/output" "github.com/containers/kubernetes-mcp-server/pkg/output"
) )
func (s *Server) initPods() []server.ServerTool { func (s *Server) initPods() []ServerTool {
return []server.ServerTool{ return []ServerTool{
{Tool: mcp.NewTool("pods_list", {Tool: Tool{
mcp.WithDescription("List all the Kubernetes pods in the current cluster from all namespaces"), Name: "pods_list",
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]")), Description: "List all the Kubernetes pods in the current cluster from all namespaces",
// Tool annotations InputSchema: &jsonschema.Schema{
mcp.WithTitleAnnotation("Pods: List"), Type: "object",
mcp.WithReadOnlyHintAnnotation(true), Properties: map[string]*jsonschema.Schema{
mcp.WithDestructiveHintAnnotation(false), "labelSelector": {
mcp.WithOpenWorldHintAnnotation(true), Type: "string",
), Handler: s.podsListInAllNamespaces}, 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",
{Tool: mcp.NewTool("pods_list_in_namespace", Pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]",
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",
}
}, },
mcp.Required(), },
), Annotations: ToolAnnotations{
mcp.WithString("container", mcp.Description("Name of the Pod container where the command will be executed (Optional)")), Title: "Pods: List",
// Tool annotations ReadOnlyHint: ptr.To(true),
mcp.WithTitleAnnotation("Pods: Exec"), DestructiveHint: ptr.To(false),
mcp.WithReadOnlyHintAnnotation(false), IdempotentHint: ptr.To(false),
mcp.WithDestructiveHintAnnotation(true), // Depending on the Pod's entrypoint, executing certain commands may kill the Pod OpenWorldHint: ptr.To(true),
mcp.WithIdempotentHintAnnotation(false), },
mcp.WithOpenWorldHintAnnotation(true), }, Handler: s.podsListInAllNamespaces},
), Handler: s.podsExec}, {Tool: Tool{
{Tool: mcp.NewTool("pods_log", Name: "pods_list_in_namespace",
mcp.WithDescription("Get the logs of a Kubernetes Pod in the current or provided namespace with the provided name"), Description: "List all the Kubernetes pods in the specified namespace in the current cluster",
mcp.WithString("namespace", mcp.Description("Namespace to get the Pod logs from")), InputSchema: &jsonschema.Schema{
mcp.WithString("name", mcp.Description("Name of the Pod to get the logs from"), mcp.Required()), Type: "object",
mcp.WithString("container", mcp.Description("Name of the Pod container to get the logs from (Optional)")), Properties: map[string]*jsonschema.Schema{
mcp.WithBoolean("previous", mcp.Description("Return previous terminated container logs (Optional)")), "namespace": {
// Tool annotations Type: "string",
mcp.WithTitleAnnotation("Pods: Log"), Description: "Namespace to list pods from",
mcp.WithReadOnlyHintAnnotation(true), },
mcp.WithDestructiveHintAnnotation(false), "labelSelector": {
mcp.WithOpenWorldHintAnnotation(true), Type: "string",
), Handler: s.podsLog}, 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",
{Tool: mcp.NewTool("pods_run", Pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]",
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)")), Required: []string{"namespace"},
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)")), Annotations: ToolAnnotations{
// Tool annotations Title: "Pods: List in Namespace",
mcp.WithTitleAnnotation("Pods: Run"), ReadOnlyHint: ptr.To(true),
mcp.WithReadOnlyHintAnnotation(false), DestructiveHint: ptr.To(false),
mcp.WithDestructiveHintAnnotation(false), IdempotentHint: ptr.To(false),
mcp.WithIdempotentHintAnnotation(false), OpenWorldHint: ptr.To(true),
mcp.WithOpenWorldHintAnnotation(true), },
), Handler: s.podsRun}, }, 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},
} }
} }

View File

@@ -5,99 +5,143 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"k8s.io/apimachinery/pkg/runtime/schema" "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/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/output" "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" commonApiVersion := "v1 Pod, v1 Service, v1 Node, apps/v1 Deployment, networking.k8s.io/v1 Ingress"
if s.k.IsOpenShift(context.Background()) { if s.k.IsOpenShift(context.Background()) {
commonApiVersion += ", route.openshift.io/v1 Route" commonApiVersion += ", route.openshift.io/v1 Route"
} }
commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion) commonApiVersion = fmt.Sprintf("(common apiVersion and kind include: %s)", commonApiVersion)
return []server.ServerTool{ return []ServerTool{
{Tool: mcp.NewTool("resources_list", {Tool: Tool{
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"+ Name: "resources_list",
commonApiVersion), 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,
mcp.WithString("apiVersion", InputSchema: &jsonschema.Schema{
mcp.Description("apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"), Type: "object",
mcp.Required(), Properties: map[string]*jsonschema.Schema{
), "apiVersion": {
mcp.WithString("kind", Type: "string",
mcp.Description("kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)"), Description: "apiVersion of the resources (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)",
mcp.Required(), },
), "kind": {
mcp.WithString("namespace", Type: "string",
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")), Description: "kind of the resources (examples of valid kind are: Pod, Service, Deployment, Ingress)",
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]")), "namespace": {
// Tool annotations Type: "string",
mcp.WithTitleAnnotation("Resources: List"), 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.WithReadOnlyHintAnnotation(true), },
mcp.WithDestructiveHintAnnotation(false), "labelSelector": {
mcp.WithOpenWorldHintAnnotation(true), Type: "string",
), Handler: s.resourcesList}, 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",
{Tool: mcp.NewTool("resources_get", Pattern: "([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]",
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", Required: []string{"apiVersion", "kind"},
mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"), },
mcp.Required(), Annotations: ToolAnnotations{
), Title: "Resources: List",
mcp.WithString("kind", ReadOnlyHint: ptr.To(true),
mcp.Description("kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)"), DestructiveHint: ptr.To(false),
mcp.Required(), IdempotentHint: ptr.To(false),
), OpenWorldHint: ptr.To(true),
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"), }, Handler: s.resourcesList},
), {Tool: Tool{
mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()), Name: "resources_get",
// Tool annotations Description: "Get a Kubernetes resource in the current cluster by providing its apiVersion, kind, optionally the namespace, and its name\n" + commonApiVersion,
mcp.WithTitleAnnotation("Resources: Get"), InputSchema: &jsonschema.Schema{
mcp.WithReadOnlyHintAnnotation(true), Type: "object",
mcp.WithDestructiveHintAnnotation(false), Properties: map[string]*jsonschema.Schema{
mcp.WithOpenWorldHintAnnotation(true), "apiVersion": {
), Handler: s.resourcesGet}, Type: "string",
{Tool: mcp.NewTool("resources_create_or_update", Description: "apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)",
mcp.WithDescription("Create or update a Kubernetes resource in the current cluster by providing a YAML or JSON representation of the resource\n"+ },
commonApiVersion), "kind": {
mcp.WithString("resource", Type: "string",
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"), Description: "kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)",
mcp.Required(), },
), "namespace": {
// Tool annotations Type: "string",
mcp.WithTitleAnnotation("Resources: Create or Update"), 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.WithReadOnlyHintAnnotation(false), },
mcp.WithDestructiveHintAnnotation(true), "name": {
mcp.WithIdempotentHintAnnotation(true), Type: "string",
mcp.WithOpenWorldHintAnnotation(true), Description: "Name of the resource",
), 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"+ Required: []string{"apiVersion", "kind", "name"},
commonApiVersion), },
mcp.WithString("apiVersion", Annotations: ToolAnnotations{
mcp.Description("apiVersion of the resource (examples of valid apiVersion are: v1, apps/v1, networking.k8s.io/v1)"), Title: "Resources: Get",
mcp.Required(), ReadOnlyHint: ptr.To(true),
), DestructiveHint: ptr.To(false),
mcp.WithString("kind", IdempotentHint: ptr.To(false),
mcp.Description("kind of the resource (examples of valid kind are: Pod, Service, Deployment, Ingress)"), OpenWorldHint: ptr.To(true),
mcp.Required(), },
), }, Handler: s.resourcesGet},
mcp.WithString("namespace", {Tool: Tool{
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"), 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,
mcp.WithString("name", mcp.Description("Name of the resource"), mcp.Required()), InputSchema: &jsonschema.Schema{
// Tool annotations Type: "object",
mcp.WithTitleAnnotation("Resources: Delete"), Properties: map[string]*jsonschema.Schema{
mcp.WithReadOnlyHintAnnotation(false), "resource": {
mcp.WithDestructiveHintAnnotation(true), Type: "string",
mcp.WithIdempotentHintAnnotation(true), 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.WithOpenWorldHintAnnotation(true), },
), Handler: s.resourcesDelete}, },
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},
} }
} }

View File

@@ -65,7 +65,6 @@
}, },
"values": { "values": {
"description": "Values to pass to the Helm chart (Optional)", "description": "Values to pass to the Helm chart (Optional)",
"properties": {},
"type": "object" "type": "object"
} }
}, },

View File

@@ -65,7 +65,6 @@
}, },
"values": { "values": {
"description": "Values to pass to the Helm chart (Optional)", "description": "Values to pass to the Helm chart (Optional)",
"properties": {},
"type": "object" "type": "object"
} }
}, },

View File

@@ -1,25 +1,86 @@
package mcp package mcp
import ( import (
"context"
"encoding/json"
"fmt"
"slices" "slices"
"github.com/google/jsonschema-go/jsonschema"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server" "github.com/mark3labs/mcp-go/server"
) )
type Toolset interface { type Toolset interface {
GetName() string GetName() string
GetDescription() string GetDescription() string
GetTools(s *Server) []server.ServerTool GetTools(s *Server) []ServerTool
} }
var Toolsets = []Toolset{ type ServerTool struct {
&Full{}, 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 { func ToolsetFromString(name string) Toolset {
for _, toolset := range Toolsets { for _, toolset := range Toolsets() {
if toolset.GetName() == name { if toolset.GetName() == name {
return toolset return toolset
} }
@@ -27,15 +88,54 @@ func ToolsetFromString(name string) Toolset {
return nil 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{} type Full struct{}
var _ Toolset = (*Full)(nil)
func (p *Full) GetName() string { func (p *Full) GetName() string {
return "full" return "full"
} }
func (p *Full) GetDescription() string { func (p *Full) GetDescription() string {
return "Complete toolset with all tools and extended outputs" 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( return slices.Concat(
s.initConfiguration(), s.initConfiguration(),
s.initEvents(), s.initEvents(),
@@ -47,8 +147,5 @@ func (p *Full) GetTools(s *Server) []server.ServerTool {
} }
func init() { func init() {
ToolsetNames = make([]string, 0) Register(&Full{})
for _, toolset := range Toolsets {
ToolsetNames = append(ToolsetNames, toolset.GetName())
}
} }