mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
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:
1
go.mod
1
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
124
pkg/mcp/helm.go
124
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},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
341
pkg/mcp/pods.go
341
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},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@
|
||||
},
|
||||
"values": {
|
||||
"description": "Values to pass to the Helm chart (Optional)",
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
|
||||
1
pkg/mcp/testdata/toolsets-full-tools.json
vendored
1
pkg/mcp/testdata/toolsets-full-tools.json
vendored
@@ -65,7 +65,6 @@
|
||||
},
|
||||
"values": {
|
||||
"description": "Values to pass to the Helm chart (Optional)",
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user