Skip to content

Commit 5bfba5f

Browse files
committed
feat(toolsets): add support for multiple toolsets in configuration
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 209e843 commit 5bfba5f

37 files changed

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

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)