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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,7 @@ golangci-lint: ## Download and install golangci-lint if not already installed
.PHONY: lint
lint: golangci-lint ## Lint the code
$(GOLANGCI_LINT) run --verbose --print-resources-usage

.PHONY: update-readme-tools
update-readme-tools: ## Update the README.md file with the latest toolsets
go run ./internal/tools/update-readme/main.go README.md
306 changes: 102 additions & 204 deletions README.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions internal/test/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package test

func Must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
99 changes: 99 additions & 0 deletions internal/tools/update-readme/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package main

import (
"context"
"fmt"
"maps"
"os"
"slices"
"strings"

internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"

_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/core"
_ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/helm"
)

type OpenShift struct{}

func (o *OpenShift) IsOpenShift(ctx context.Context) bool {
return true
}

var _ internalk8s.Openshift = (*OpenShift)(nil)

func main() {
readme, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
// Available Toolsets
toolsetsList := toolsets.Toolsets()
maxNameLen, maxDescLen := len("Toolset"), len("Description")
for _, toolset := range toolsetsList {
nameLen := len(toolset.GetName())
descLen := len(toolset.GetDescription())
if nameLen > maxNameLen {
maxNameLen = nameLen
}
if descLen > maxDescLen {
maxDescLen = descLen
}
}
availableToolsets := strings.Builder{}
availableToolsets.WriteString(fmt.Sprintf("| %-*s | %-*s |\n", maxNameLen, "Toolset", maxDescLen, "Description"))
availableToolsets.WriteString(fmt.Sprintf("|-%s-|-%s-|\n", strings.Repeat("-", maxNameLen), strings.Repeat("-", maxDescLen)))
for _, toolset := range toolsetsList {
availableToolsets.WriteString(fmt.Sprintf("| %-*s | %-*s |\n", maxNameLen, toolset.GetName(), maxDescLen, toolset.GetDescription()))
}
updated := replaceBetweenMarkers(
string(readme),
"<!-- AVAILABLE-TOOLSETS-START -->",
"<!-- AVAILABLE-TOOLSETS-END -->",
availableToolsets.String(),
)

// Available Toolset Tools
toolsetTools := strings.Builder{}
for _, toolset := range toolsetsList {
toolsetTools.WriteString("<details>\n\n<summary>" + toolset.GetName() + "</summary>\n\n")
tools := toolset.GetTools(&OpenShift{})
for _, tool := range tools {
toolsetTools.WriteString(fmt.Sprintf("- **%s** - %s\n", tool.Tool.Name, tool.Tool.Description))
for _, propName := range slices.Sorted(maps.Keys(tool.Tool.InputSchema.Properties)) {
property := tool.Tool.InputSchema.Properties[propName]
toolsetTools.WriteString(fmt.Sprintf(" - `%s` (`%s`)", propName, property.Type))
if slices.Contains(tool.Tool.InputSchema.Required, propName) {
toolsetTools.WriteString(" **(required)**")
}
toolsetTools.WriteString(fmt.Sprintf(" - %s\n", property.Description))
}
toolsetTools.WriteString("\n")
}
toolsetTools.WriteString("</details>\n\n")
}
updated = replaceBetweenMarkers(
updated,
"<!-- AVAILABLE-TOOLSETS-TOOLS-START -->",
"<!-- AVAILABLE-TOOLSETS-TOOLS-END -->",
toolsetTools.String(),
)

if err := os.WriteFile(os.Args[1], []byte(updated), 0o644); err != nil {
panic(err)
}
}

func replaceBetweenMarkers(content, startMarker, endMarker, replacement string) string {
startIdx := strings.Index(content, startMarker)
if startIdx == -1 {
return content
}
endIdx := strings.Index(content, endMarker)
if endIdx == -1 || endIdx <= startIdx {
return content
}
return content[:startIdx+len(startMarker)] + "\n\n" + replacement + "\n" + content[endIdx:]
}
2 changes: 1 addition & 1 deletion pkg/api/toolsets.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Toolset interface {
// Examples: "core", "metrics", "helm"
GetName() string
GetDescription() string
GetTools(k *internalk8s.Manager) []ServerTool
GetTools(o internalk8s.Openshift) []ServerTool
}

type ToolCallRequest interface {
Expand Down
21 changes: 16 additions & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type StaticConfig struct {
ReadOnly bool `toml:"read_only,omitempty"`
// When true, disable tools annotated with destructiveHint=true
DisableDestructive bool `toml:"disable_destructive,omitempty"`
Toolsets []string `toml:"toolsets,omitempty"`
EnabledTools []string `toml:"enabled_tools,omitempty"`
DisabledTools []string `toml:"disabled_tools,omitempty"`

Expand Down Expand Up @@ -50,22 +51,32 @@ type StaticConfig struct {
ServerURL string `toml:"server_url,omitempty"`
}

func Default() *StaticConfig {
return &StaticConfig{
ListOutput: "table",
Toolsets: []string{"core", "config", "helm"},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave helm out of the default set.

Copy link
Member Author

@manusa manusa Sep 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In <= v0.0.50 it's included by default.
I would do that after the first release with the toolsets feature so that we don't make it a breaking change.
We can also make it more palatable for upstream users if we remove it from the default set of toolsets but in exchange we add a few more features (e.g. helm toolset is no longer activated by default since it now includes multiple tooling specific to helm, you can enable it by starting the kubernetes MCP server with the following flag --toolsets core,config,helm).
IMO, the plan should be release v0.0.51 with toolsets without the breaking changes, then, release v0.0.52 with deactivated and specialized toolsets such as helm.

Similarly, for the config one, we could enable it only in stdio mode which is the only deployment+runtime mode where it makes sense.

}
}

type GroupVersionKind struct {
Group string `toml:"group"`
Version string `toml:"version"`
Kind string `toml:"kind,omitempty"`
}

// ReadConfig reads the toml file and returns the StaticConfig.
func ReadConfig(configPath string) (*StaticConfig, error) {
// Read reads the toml file and returns the StaticConfig.
func Read(configPath string) (*StaticConfig, error) {
configData, err := os.ReadFile(configPath)
if err != nil {
return nil, err
}
return ReadToml(configData)
}

var config *StaticConfig
err = toml.Unmarshal(configData, &config)
if err != nil {
// ReadToml reads the toml data and returns the StaticConfig.
func ReadToml(configData []byte) (*StaticConfig, error) {
config := Default()
if err := toml.Unmarshal(configData, config); err != nil {
return nil, err
}
return config, nil
Expand Down
Loading
Loading