Skip to content

Commit 2b6c886

Browse files
authored
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 <[email protected]>
1 parent 4361a9e commit 2b6c886

File tree

14 files changed

+653
-290
lines changed

14 files changed

+653
-290
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/coreos/go-oidc/v3 v3.15.0
88
github.com/fsnotify/fsnotify v1.9.0
99
github.com/go-jose/go-jose/v4 v4.1.2
10+
github.com/google/jsonschema-go v0.2.2
1011
github.com/mark3labs/mcp-go v0.39.1
1112
github.com/pkg/errors v0.9.1
1213
github.com/spf13/afero v1.15.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O
130130
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
131131
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
132132
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
133+
github.com/google/jsonschema-go v0.2.2 h1:qb9KM/pATIqIPuE9gEDwPsco8HHCTlA88IGFYHDl03A=
134+
github.com/google/jsonschema-go v0.2.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
133135
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
134136
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
135137
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

pkg/http/http_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func (c *httpContext) beforeEach(t *testing.T) {
8787
}
8888
c.StaticConfig.Port = fmt.Sprintf("%d", ln.Addr().(*net.TCPAddr).Port)
8989
mcpServer, err := mcp.NewServer(mcp.Configuration{
90-
Toolset: mcp.Toolsets[0],
90+
Toolset: mcp.Toolsets()[0],
9191
StaticConfig: c.StaticConfig,
9292
})
9393
if err != nil {

pkg/kubernetes-mcp-server/cmd/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func NewMCPServer(streams genericiooptions.IOStreams) *cobra.Command {
115115
cmd.Flags().StringVar(&o.Port, "port", o.Port, "Start a streamable HTTP and SSE HTTP server on the specified port (e.g. 8080)")
116116
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)")
117117
cmd.Flags().StringVar(&o.Kubeconfig, "kubeconfig", o.Kubeconfig, "Path to the kubeconfig file to use for authentication")
118-
cmd.Flags().StringVar(&o.Toolset, "toolset", o.Toolset, "MCP toolset to use (one of: "+strings.Join(mcp.ToolsetNames, ", ")+")")
118+
cmd.Flags().StringVar(&o.Toolset, "toolset", o.Toolset, "MCP toolset to use (one of: "+strings.Join(mcp.ToolsetNames(), ", ")+")")
119119
cmd.Flags().StringVar(&o.ListOutput, "list-output", o.ListOutput, "Output format for resource list operations (one of: "+strings.Join(output.Names, ", ")+"). Defaults to table.")
120120
cmd.Flags().BoolVar(&o.ReadOnly, "read-only", o.ReadOnly, "If true, only tools annotated with readOnlyHint=true are exposed")
121121
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 {
239239
func (m *MCPServerOptions) Run() error {
240240
toolset := mcp.ToolsetFromString(m.Toolset)
241241
if toolset == nil {
242-
return fmt.Errorf("invalid toolset name: %s, valid names are: %s", m.Toolset, strings.Join(mcp.ToolsetNames, ", "))
242+
return fmt.Errorf("invalid toolset name: %s, valid names are: %s", m.Toolset, strings.Join(mcp.ToolsetNames(), ", "))
243243
}
244244
listOutput := output.FromString(m.StaticConfig.ListOutput)
245245
if listOutput == nil {

pkg/mcp/configuration.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,38 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/google/jsonschema-go/jsonschema"
78
"github.com/mark3labs/mcp-go/mcp"
8-
"github.com/mark3labs/mcp-go/server"
9+
"k8s.io/utils/ptr"
910

1011
"github.com/containers/kubernetes-mcp-server/pkg/output"
1112
)
1213

13-
func (s *Server) initConfiguration() []server.ServerTool {
14-
tools := []server.ServerTool{
15-
{Tool: mcp.NewTool("configuration_view",
16-
mcp.WithDescription("Get the current Kubernetes configuration content as a kubeconfig YAML"),
17-
mcp.WithBoolean("minified", mcp.Description("Return a minified version of the configuration. "+
18-
"If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. "+
19-
"If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. "+
20-
"(Optional, default true)")),
21-
// Tool annotations
22-
mcp.WithTitleAnnotation("Configuration: View"),
23-
mcp.WithReadOnlyHintAnnotation(true),
24-
mcp.WithDestructiveHintAnnotation(false),
25-
mcp.WithOpenWorldHintAnnotation(true),
26-
), Handler: s.configurationView},
14+
func (s *Server) initConfiguration() []ServerTool {
15+
tools := []ServerTool{
16+
{Tool: Tool{
17+
Name: "configuration_view",
18+
Description: "Get the current Kubernetes configuration content as a kubeconfig YAML",
19+
InputSchema: &jsonschema.Schema{
20+
Type: "object",
21+
Properties: map[string]*jsonschema.Schema{
22+
"minified": {
23+
Type: "boolean",
24+
Description: "Return a minified version of the configuration. " +
25+
"If set to true, keeps only the current-context and the relevant pieces of the configuration for that context. " +
26+
"If set to false, all contexts, clusters, auth-infos, and users are returned in the configuration. " +
27+
"(Optional, default true)",
28+
},
29+
},
30+
},
31+
Annotations: ToolAnnotations{
32+
Title: "Configuration: View",
33+
ReadOnlyHint: ptr.To(true),
34+
DestructiveHint: ptr.To(false),
35+
IdempotentHint: ptr.To(false),
36+
OpenWorldHint: ptr.To(true),
37+
},
38+
}, Handler: s.configurationView},
2739
}
2840
return tools
2941
}

pkg/mcp/events.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,35 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/google/jsonschema-go/jsonschema"
78
"github.com/mark3labs/mcp-go/mcp"
8-
"github.com/mark3labs/mcp-go/server"
9+
"k8s.io/utils/ptr"
910

1011
"github.com/containers/kubernetes-mcp-server/pkg/output"
1112
)
1213

13-
func (s *Server) initEvents() []server.ServerTool {
14-
return []server.ServerTool{
15-
{Tool: mcp.NewTool("events_list",
16-
mcp.WithDescription("List all the Kubernetes events in the current cluster from all namespaces"),
17-
mcp.WithString("namespace",
18-
mcp.Description("Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces")),
19-
// Tool annotations
20-
mcp.WithTitleAnnotation("Events: List"),
21-
mcp.WithReadOnlyHintAnnotation(true),
22-
mcp.WithDestructiveHintAnnotation(false),
23-
mcp.WithOpenWorldHintAnnotation(true),
24-
), Handler: s.eventsList},
14+
func (s *Server) initEvents() []ServerTool {
15+
return []ServerTool{
16+
{Tool: Tool{
17+
Name: "events_list",
18+
Description: "List all the Kubernetes events in the current cluster from all namespaces",
19+
InputSchema: &jsonschema.Schema{
20+
Type: "object",
21+
Properties: map[string]*jsonschema.Schema{
22+
"namespace": {
23+
Type: "string",
24+
Description: "Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces",
25+
},
26+
},
27+
},
28+
Annotations: ToolAnnotations{
29+
Title: "Events: List",
30+
ReadOnlyHint: ptr.To(true),
31+
DestructiveHint: ptr.To(false),
32+
IdempotentHint: ptr.To(false),
33+
OpenWorldHint: ptr.To(true),
34+
},
35+
}, Handler: s.eventsList},
2536
}
2637
}
2738

pkg/mcp/helm.go

Lines changed: 87 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,46 +4,96 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/google/jsonschema-go/jsonschema"
78
"github.com/mark3labs/mcp-go/mcp"
8-
"github.com/mark3labs/mcp-go/server"
9+
"k8s.io/utils/ptr"
910
)
1011

11-
func (s *Server) initHelm() []server.ServerTool {
12-
return []server.ServerTool{
13-
{Tool: mcp.NewTool("helm_install",
14-
mcp.WithDescription("Install a Helm chart in the current or provided namespace"),
15-
mcp.WithString("chart", mcp.Description("Chart reference to install (for example: stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress)"), mcp.Required()),
16-
mcp.WithObject("values", mcp.Description("Values to pass to the Helm chart (Optional)")),
17-
mcp.WithString("name", mcp.Description("Name of the Helm release (Optional, random name if not provided)")),
18-
mcp.WithString("namespace", mcp.Description("Namespace to install the Helm chart in (Optional, current namespace if not provided)")),
19-
// Tool annotations
20-
mcp.WithTitleAnnotation("Helm: Install"),
21-
mcp.WithReadOnlyHintAnnotation(false),
22-
mcp.WithDestructiveHintAnnotation(false),
23-
mcp.WithIdempotentHintAnnotation(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
24-
mcp.WithOpenWorldHintAnnotation(true),
25-
), Handler: s.helmInstall},
26-
{Tool: mcp.NewTool("helm_list",
27-
mcp.WithDescription("List all the Helm releases in the current or provided namespace (or in all namespaces if specified)"),
28-
mcp.WithString("namespace", mcp.Description("Namespace to list Helm releases from (Optional, all namespaces if not provided)")),
29-
mcp.WithBoolean("all_namespaces", mcp.Description("If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)")),
30-
// Tool annotations
31-
mcp.WithTitleAnnotation("Helm: List"),
32-
mcp.WithReadOnlyHintAnnotation(true),
33-
mcp.WithDestructiveHintAnnotation(false),
34-
mcp.WithOpenWorldHintAnnotation(true),
35-
), Handler: s.helmList},
36-
{Tool: mcp.NewTool("helm_uninstall",
37-
mcp.WithDescription("Uninstall a Helm release in the current or provided namespace"),
38-
mcp.WithString("name", mcp.Description("Name of the Helm release to uninstall"), mcp.Required()),
39-
mcp.WithString("namespace", mcp.Description("Namespace to uninstall the Helm release from (Optional, current namespace if not provided)")),
40-
// Tool annotations
41-
mcp.WithTitleAnnotation("Helm: Uninstall"),
42-
mcp.WithReadOnlyHintAnnotation(false),
43-
mcp.WithDestructiveHintAnnotation(true),
44-
mcp.WithIdempotentHintAnnotation(true),
45-
mcp.WithOpenWorldHintAnnotation(true),
46-
), Handler: s.helmUninstall},
12+
func (s *Server) initHelm() []ServerTool {
13+
return []ServerTool{
14+
{Tool: Tool{
15+
Name: "helm_install",
16+
Description: "Install a Helm chart in the current or provided namespace",
17+
InputSchema: &jsonschema.Schema{
18+
Type: "object",
19+
Properties: map[string]*jsonschema.Schema{
20+
"chart": {
21+
Type: "string",
22+
Description: "Chart reference to install (for example: stable/grafana, oci://ghcr.io/nginxinc/charts/nginx-ingress)",
23+
},
24+
"values": {
25+
Type: "object",
26+
Description: "Values to pass to the Helm chart (Optional)",
27+
Properties: make(map[string]*jsonschema.Schema),
28+
},
29+
"name": {
30+
Type: "string",
31+
Description: "Name of the Helm release (Optional, random name if not provided)",
32+
},
33+
"namespace": {
34+
Type: "string",
35+
Description: "Namespace to install the Helm chart in (Optional, current namespace if not provided)",
36+
},
37+
},
38+
Required: []string{"chart"},
39+
},
40+
Annotations: ToolAnnotations{
41+
Title: "Helm: Install",
42+
ReadOnlyHint: ptr.To(false),
43+
DestructiveHint: ptr.To(false),
44+
IdempotentHint: ptr.To(false), // TODO: consider replacing implementation with equivalent to: helm upgrade --install
45+
OpenWorldHint: ptr.To(true),
46+
},
47+
}, Handler: s.helmInstall},
48+
{Tool: Tool{
49+
Name: "helm_list",
50+
Description: "List all the Helm releases in the current or provided namespace (or in all namespaces if specified)",
51+
InputSchema: &jsonschema.Schema{
52+
Type: "object",
53+
Properties: map[string]*jsonschema.Schema{
54+
"namespace": {
55+
Type: "string",
56+
Description: "Namespace to list Helm releases from (Optional, all namespaces if not provided)",
57+
},
58+
"all_namespaces": {
59+
Type: "boolean",
60+
Description: "If true, lists all Helm releases in all namespaces ignoring the namespace argument (Optional)",
61+
},
62+
},
63+
},
64+
Annotations: ToolAnnotations{
65+
Title: "Helm: List",
66+
ReadOnlyHint: ptr.To(true),
67+
DestructiveHint: ptr.To(false),
68+
IdempotentHint: ptr.To(false),
69+
OpenWorldHint: ptr.To(true),
70+
},
71+
}, Handler: s.helmList},
72+
{Tool: Tool{
73+
Name: "helm_uninstall",
74+
Description: "Uninstall a Helm release in the current or provided namespace",
75+
InputSchema: &jsonschema.Schema{
76+
Type: "object",
77+
Properties: map[string]*jsonschema.Schema{
78+
"name": {
79+
Type: "string",
80+
Description: "Name of the Helm release to uninstall",
81+
},
82+
"namespace": {
83+
Type: "string",
84+
Description: "Namespace to uninstall the Helm release from (Optional, current namespace if not provided)",
85+
},
86+
},
87+
Required: []string{"name"},
88+
},
89+
Annotations: ToolAnnotations{
90+
Title: "Helm: Uninstall",
91+
ReadOnlyHint: ptr.To(false),
92+
DestructiveHint: ptr.To(true),
93+
IdempotentHint: ptr.To(true),
94+
OpenWorldHint: ptr.To(true),
95+
},
96+
}, Handler: s.helmUninstall},
4797
}
4898
}
4999

pkg/mcp/mcp.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ type Configuration struct {
3030
StaticConfig *config.StaticConfig
3131
}
3232

33-
func (c *Configuration) isToolApplicable(tool server.ServerTool) bool {
33+
func (c *Configuration) isToolApplicable(tool ServerTool) bool {
3434
if c.StaticConfig.ReadOnly && !ptr.Deref(tool.Tool.Annotations.ReadOnlyHint, false) {
3535
return false
3636
}
@@ -88,15 +88,19 @@ func (s *Server) reloadKubernetesClient() error {
8888
return err
8989
}
9090
s.k = k
91-
applicableTools := make([]server.ServerTool, 0)
91+
applicableTools := make([]ServerTool, 0)
9292
for _, tool := range s.configuration.Toolset.GetTools(s) {
9393
if !s.configuration.isToolApplicable(tool) {
9494
continue
9595
}
9696
applicableTools = append(applicableTools, tool)
9797
s.enabledTools = append(s.enabledTools, tool.Tool.Name)
9898
}
99-
s.server.SetTools(applicableTools...)
99+
m3labsServerTools, err := ServerToolToM3LabsServerTool(applicableTools)
100+
if err != nil {
101+
return fmt.Errorf("failed to convert tools: %v", err)
102+
}
103+
s.server.SetTools(m3labsServerTools...)
100104
return nil
101105
}
102106

pkg/mcp/namespaces.go

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,47 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/google/jsonschema-go/jsonschema"
78
"github.com/mark3labs/mcp-go/mcp"
8-
"github.com/mark3labs/mcp-go/server"
9+
"k8s.io/utils/ptr"
910

1011
"github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
1112
)
1213

13-
func (s *Server) initNamespaces() []server.ServerTool {
14-
ret := make([]server.ServerTool, 0)
15-
ret = append(ret, server.ServerTool{
16-
Tool: mcp.NewTool("namespaces_list",
17-
mcp.WithDescription("List all the Kubernetes namespaces in the current cluster"),
18-
// Tool annotations
19-
mcp.WithTitleAnnotation("Namespaces: List"),
20-
mcp.WithReadOnlyHintAnnotation(true),
21-
mcp.WithDestructiveHintAnnotation(false),
22-
mcp.WithOpenWorldHintAnnotation(true),
23-
), Handler: s.namespacesList,
14+
func (s *Server) initNamespaces() []ServerTool {
15+
ret := make([]ServerTool, 0)
16+
ret = append(ret, ServerTool{
17+
Tool: Tool{
18+
Name: "namespaces_list",
19+
Description: "List all the Kubernetes namespaces in the current cluster",
20+
InputSchema: &jsonschema.Schema{
21+
Type: "object",
22+
},
23+
Annotations: ToolAnnotations{
24+
Title: "Namespaces: List",
25+
ReadOnlyHint: ptr.To(true),
26+
DestructiveHint: ptr.To(false),
27+
IdempotentHint: ptr.To(false),
28+
OpenWorldHint: ptr.To(true),
29+
},
30+
}, Handler: s.namespacesList,
2431
})
2532
if s.k.IsOpenShift(context.Background()) {
26-
ret = append(ret, server.ServerTool{
27-
Tool: mcp.NewTool("projects_list",
28-
mcp.WithDescription("List all the OpenShift projects in the current cluster"),
29-
// Tool annotations
30-
mcp.WithTitleAnnotation("Projects: List"),
31-
mcp.WithReadOnlyHintAnnotation(true),
32-
mcp.WithDestructiveHintAnnotation(false),
33-
mcp.WithOpenWorldHintAnnotation(true),
34-
), Handler: s.projectsList,
33+
ret = append(ret, ServerTool{
34+
Tool: Tool{
35+
Name: "projects_list",
36+
Description: "List all the OpenShift projects in the current cluster",
37+
InputSchema: &jsonschema.Schema{
38+
Type: "object",
39+
},
40+
Annotations: ToolAnnotations{
41+
Title: "Projects: List",
42+
ReadOnlyHint: ptr.To(true),
43+
DestructiveHint: ptr.To(false),
44+
IdempotentHint: ptr.To(false),
45+
OpenWorldHint: ptr.To(true),
46+
},
47+
}, Handler: s.projectsList,
3548
})
3649
}
3750
return ret

0 commit comments

Comments
 (0)