Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 1 addition & 1 deletion pkg/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions pkg/kubernetes-mcp-server/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
42 changes: 27 additions & 15 deletions pkg/mcp/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
37 changes: 24 additions & 13 deletions pkg/mcp/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}
}

Expand Down
124 changes: 87 additions & 37 deletions pkg/mcp/helm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}
}

Expand Down
10 changes: 7 additions & 3 deletions pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -88,15 +88,19 @@ 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
}
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
}

Expand Down
55 changes: 34 additions & 21 deletions pkg/mcp/namespaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading