Skip to content

Commit 48cf204

Browse files
authored
feat(toolsets): add support for multiple toolsets in configuration (#323)
Users can now enable or disable different toolsets either by providing a command-line flag or by setting the toolsets array field in the TOML configuration. Downstream Kubernetes API developers can declare toolsets for their APIs by creating a new nested package in pkg/toolsets and registering it in pkg/mcp/modules.go Signed-off-by: Marc Nuri <[email protected]>
1 parent 3fc4fa4 commit 48cf204

37 files changed

+673
-492
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,7 @@ golangci-lint: ## Download and install golangci-lint if not already installed
111111
.PHONY: lint
112112
lint: golangci-lint ## Lint the code
113113
$(GOLANGCI_LINT) run --verbose --print-resources-usage
114+
115+
.PHONY: update-readme-tools
116+
update-readme-tools: ## Update the README.md file with the latest toolsets
117+
go run ./internal/tools/update-readme/main.go README.md

README.md

Lines changed: 102 additions & 204 deletions
Large diffs are not rendered by default.

internal/test/test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package test
2+
3+
func Must[T any](v T, err error) T {
4+
if err != nil {
5+
panic(err)
6+
}
7+
return v
8+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"maps"
7+
"os"
8+
"slices"
9+
"strings"
10+
11+
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
12+
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
13+
14+
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config"
15+
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/core"
16+
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/helm"
17+
)
18+
19+
type OpenShift struct{}
20+
21+
func (o *OpenShift) IsOpenShift(ctx context.Context) bool {
22+
return true
23+
}
24+
25+
var _ internalk8s.Openshift = (*OpenShift)(nil)
26+
27+
func main() {
28+
readme, err := os.ReadFile(os.Args[1])
29+
if err != nil {
30+
panic(err)
31+
}
32+
// Available Toolsets
33+
toolsetsList := toolsets.Toolsets()
34+
maxNameLen, maxDescLen := len("Toolset"), len("Description")
35+
for _, toolset := range toolsetsList {
36+
nameLen := len(toolset.GetName())
37+
descLen := len(toolset.GetDescription())
38+
if nameLen > maxNameLen {
39+
maxNameLen = nameLen
40+
}
41+
if descLen > maxDescLen {
42+
maxDescLen = descLen
43+
}
44+
}
45+
availableToolsets := strings.Builder{}
46+
availableToolsets.WriteString(fmt.Sprintf("| %-*s | %-*s |\n", maxNameLen, "Toolset", maxDescLen, "Description"))
47+
availableToolsets.WriteString(fmt.Sprintf("|-%s-|-%s-|\n", strings.Repeat("-", maxNameLen), strings.Repeat("-", maxDescLen)))
48+
for _, toolset := range toolsetsList {
49+
availableToolsets.WriteString(fmt.Sprintf("| %-*s | %-*s |\n", maxNameLen, toolset.GetName(), maxDescLen, toolset.GetDescription()))
50+
}
51+
updated := replaceBetweenMarkers(
52+
string(readme),
53+
"<!-- AVAILABLE-TOOLSETS-START -->",
54+
"<!-- AVAILABLE-TOOLSETS-END -->",
55+
availableToolsets.String(),
56+
)
57+
58+
// Available Toolset Tools
59+
toolsetTools := strings.Builder{}
60+
for _, toolset := range toolsetsList {
61+
toolsetTools.WriteString("<details>\n\n<summary>" + toolset.GetName() + "</summary>\n\n")
62+
tools := toolset.GetTools(&OpenShift{})
63+
for _, tool := range tools {
64+
toolsetTools.WriteString(fmt.Sprintf("- **%s** - %s\n", tool.Tool.Name, tool.Tool.Description))
65+
for _, propName := range slices.Sorted(maps.Keys(tool.Tool.InputSchema.Properties)) {
66+
property := tool.Tool.InputSchema.Properties[propName]
67+
toolsetTools.WriteString(fmt.Sprintf(" - `%s` (`%s`)", propName, property.Type))
68+
if slices.Contains(tool.Tool.InputSchema.Required, propName) {
69+
toolsetTools.WriteString(" **(required)**")
70+
}
71+
toolsetTools.WriteString(fmt.Sprintf(" - %s\n", property.Description))
72+
}
73+
toolsetTools.WriteString("\n")
74+
}
75+
toolsetTools.WriteString("</details>\n\n")
76+
}
77+
updated = replaceBetweenMarkers(
78+
updated,
79+
"<!-- AVAILABLE-TOOLSETS-TOOLS-START -->",
80+
"<!-- AVAILABLE-TOOLSETS-TOOLS-END -->",
81+
toolsetTools.String(),
82+
)
83+
84+
if err := os.WriteFile(os.Args[1], []byte(updated), 0o644); err != nil {
85+
panic(err)
86+
}
87+
}
88+
89+
func replaceBetweenMarkers(content, startMarker, endMarker, replacement string) string {
90+
startIdx := strings.Index(content, startMarker)
91+
if startIdx == -1 {
92+
return content
93+
}
94+
endIdx := strings.Index(content, endMarker)
95+
if endIdx == -1 || endIdx <= startIdx {
96+
return content
97+
}
98+
return content[:startIdx+len(startMarker)] + "\n\n" + replacement + "\n" + content[endIdx:]
99+
}

pkg/api/toolsets.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type Toolset interface {
2020
// Examples: "core", "metrics", "helm"
2121
GetName() string
2222
GetDescription() string
23-
GetTools(k *internalk8s.Manager) []ServerTool
23+
GetTools(o internalk8s.Openshift) []ServerTool
2424
}
2525

2626
type ToolCallRequest interface {

pkg/config/config.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type StaticConfig struct {
2020
ReadOnly bool `toml:"read_only,omitempty"`
2121
// When true, disable tools annotated with destructiveHint=true
2222
DisableDestructive bool `toml:"disable_destructive,omitempty"`
23+
Toolsets []string `toml:"toolsets,omitempty"`
2324
EnabledTools []string `toml:"enabled_tools,omitempty"`
2425
DisabledTools []string `toml:"disabled_tools,omitempty"`
2526

@@ -50,22 +51,32 @@ type StaticConfig struct {
5051
ServerURL string `toml:"server_url,omitempty"`
5152
}
5253

54+
func Default() *StaticConfig {
55+
return &StaticConfig{
56+
ListOutput: "table",
57+
Toolsets: []string{"core", "config", "helm"},
58+
}
59+
}
60+
5361
type GroupVersionKind struct {
5462
Group string `toml:"group"`
5563
Version string `toml:"version"`
5664
Kind string `toml:"kind,omitempty"`
5765
}
5866

59-
// ReadConfig reads the toml file and returns the StaticConfig.
60-
func ReadConfig(configPath string) (*StaticConfig, error) {
67+
// Read reads the toml file and returns the StaticConfig.
68+
func Read(configPath string) (*StaticConfig, error) {
6169
configData, err := os.ReadFile(configPath)
6270
if err != nil {
6371
return nil, err
6472
}
73+
return ReadToml(configData)
74+
}
6575

66-
var config *StaticConfig
67-
err = toml.Unmarshal(configData, &config)
68-
if err != nil {
76+
// ReadToml reads the toml data and returns the StaticConfig.
77+
func ReadToml(configData []byte) (*StaticConfig, error) {
78+
config := Default()
79+
if err := toml.Unmarshal(configData, config); err != nil {
6980
return nil, err
7081
}
7182
return config, nil

0 commit comments

Comments
 (0)