Skip to content
Open
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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,27 @@ In case multi-cluster support is enabled (default) and you have access to multip

<!-- AVAILABLE-TOOLSETS-TOOLS-END -->

### Prompts

Prompts guide the LLM through multi-step workflows using existing tools. They provide structured instructions for complex operations that benefit from AI reasoning and interpretation.

<!-- AVAILABLE-PROMPTSETS-PROMPTS-START -->

<details>

<summary>core</summary>

- **cluster_health_check** - Guide for performing comprehensive health check on Kubernetes/OpenShift clusters. Provides step-by-step instructions for examining cluster operators, nodes, pods, workloads, storage, and events to identify issues affecting cluster stability.
- `check_events` (`string`) - Include recent warning events in the health check (may increase execution time). Valid values: 'true', 'false', 'yes', 'no', '1', '0'. Default: 'true'
- `output_format` (`string`) - Output format for results: 'text' (human-readable) or 'json' (machine-readable). Valid values: 'text', 'json'. Default: 'text'
- `verbose` (`string`) - Enable detailed output with additional context and resource-level details. Valid values: 'true', 'false', 'yes', 'no', '1', '0'. Default: 'false'
- `namespace` (`string`) - Limit health check to specific namespace (optional, defaults to all namespaces). Valid values: any Kubernetes namespace name or leave empty for all namespaces

</details>


<!-- AVAILABLE-PROMPTSETS-PROMPTS-END -->

## 🧑‍💻 Development <a id="development"></a>

### Running with mcp-inspector
Expand Down
31 changes: 31 additions & 0 deletions internal/tools/update-readme/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"strings"

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

_ "github.com/containers/kubernetes-mcp-server/pkg/promptsets/core"
_ "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"
Expand Down Expand Up @@ -90,6 +92,35 @@ func main() {
toolsetTools.String(),
)

// Available Promptset Prompts
promptsetsList := promptsets.PromptSets()
promptsetPrompts := strings.Builder{}
for _, promptset := range promptsetsList {
prompts := promptset.GetPrompts(&OpenShift{})
if len(prompts) == 0 {
continue
}
promptsetPrompts.WriteString("<details>\n\n<summary>" + promptset.GetName() + "</summary>\n\n")
for _, prompt := range prompts {
promptsetPrompts.WriteString(fmt.Sprintf("- **%s** - %s\n", prompt.Name, prompt.Description))
for _, arg := range prompt.Arguments {
promptsetPrompts.WriteString(fmt.Sprintf(" - `%s` (`%s`)", arg.Name, "string"))
if arg.Required {
promptsetPrompts.WriteString(" **(required)**")
}
promptsetPrompts.WriteString(fmt.Sprintf(" - %s\n", arg.Description))
}
promptsetPrompts.WriteString("\n")
}
promptsetPrompts.WriteString("</details>\n\n")
}
updated = replaceBetweenMarkers(
updated,
"<!-- AVAILABLE-PROMPTSETS-PROMPTS-START -->",
"<!-- AVAILABLE-PROMPTSETS-PROMPTS-END -->",
promptsetPrompts.String(),
)

if err := os.WriteFile(localReadmePath, []byte(updated), 0o644); err != nil {
panic(err)
}
Expand Down
36 changes: 36 additions & 0 deletions pkg/api/prompts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package api

import (
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
)

// ServerPrompt represents a prompt that can be provided to the MCP server
type ServerPrompt struct {
Name string
Description string
Arguments []PromptArgument
GetMessages func(arguments map[string]string) []PromptMessage
}

// PromptArgument defines an argument that can be passed to a prompt
type PromptArgument struct {
Name string
Description string
Required bool
}

// PromptMessage represents a message in a prompt
type PromptMessage struct {
Role string // "user" or "assistant"
Content string
}

// PromptSet groups related prompts together
type PromptSet interface {
// GetName returns the name of the prompt set
GetName() string
// GetDescription returns a description of what this prompt set provides
GetDescription() string
// GetPrompts returns all prompts in this set
GetPrompts(o internalk8s.Openshift) []ServerPrompt
}
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type StaticConfig struct {
// When true, disable tools annotated with destructiveHint=true
DisableDestructive bool `toml:"disable_destructive,omitempty"`
Toolsets []string `toml:"toolsets,omitempty"`
Promptsets []string `toml:"promptsets,omitempty"`
EnabledTools []string `toml:"enabled_tools,omitempty"`
DisabledTools []string `toml:"disabled_tools,omitempty"`

Expand Down
53 changes: 52 additions & 1 deletion pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import (

"github.com/modelcontextprotocol/go-sdk/mcp"
authenticationapiv1 "k8s.io/api/authentication/v1"
"k8s.io/klog/v2"
"k8s.io/utils/ptr"

"github.com/containers/kubernetes-mcp-server/pkg/api"
"github.com/containers/kubernetes-mcp-server/pkg/config"
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
"github.com/containers/kubernetes-mcp-server/pkg/output"
"github.com/containers/kubernetes-mcp-server/pkg/promptsets"
"github.com/containers/kubernetes-mcp-server/pkg/toolsets"
"github.com/containers/kubernetes-mcp-server/pkg/version"
)
Expand All @@ -27,6 +29,7 @@ type Configuration struct {
*config.StaticConfig
listOutput output.Output
toolsets []api.Toolset
promptsets []api.PromptSet
}

func (c *Configuration) Toolsets() []api.Toolset {
Expand All @@ -38,6 +41,23 @@ func (c *Configuration) Toolsets() []api.Toolset {
return c.toolsets
}

func (c *Configuration) Promptsets() []api.PromptSet {
if c.promptsets == nil {
// Default to core if no promptsets configured
promptsetNames := c.StaticConfig.Promptsets
if len(promptsetNames) == 0 {
promptsetNames = []string{"core"}
}
for _, promptset := range promptsetNames {
ps := promptsets.PromptSetFromString(promptset)
if ps != nil {
c.promptsets = append(c.promptsets, ps)
}
}
}
return c.promptsets
}

func (c *Configuration) ListOutput() output.Output {
if c.listOutput == nil {
c.listOutput = output.FromString(c.StaticConfig.ListOutput)
Expand Down Expand Up @@ -77,7 +97,7 @@ func NewServer(configuration Configuration) (*Server, error) {
},
&mcp.ServerOptions{
HasResources: false,
HasPrompts: false,
HasPrompts: true,
HasTools: true,
}),
}
Expand Down Expand Up @@ -165,11 +185,42 @@ func (s *Server) reloadKubernetesClusterProvider() error {
s.server.AddTool(goSdkTool, goSdkToolHandler)
}

// Register prompts
if err := s.registerPrompts(p); err != nil {
klog.Warningf("Failed to register prompts: %v", err)
// Don't fail the whole reload if prompts fail
}

// start new watch
s.p.WatchTargets(s.reloadKubernetesClusterProvider)
return nil
}

// registerPrompts loads and registers all prompts with the MCP server
func (s *Server) registerPrompts(p internalk8s.Provider) error {
allPrompts := make([]api.ServerPrompt, 0)
for _, ps := range s.configuration.Promptsets() {
prompts := ps.GetPrompts(p)
allPrompts = append(allPrompts, prompts...)
klog.V(5).Infof("Loaded %d prompts from promptset '%s'", len(prompts), ps.GetName())
}

goSdkPrompts, goSdkHandlers, err := ServerPromptToGoSdkPrompt(s, allPrompts)
if err != nil {
return fmt.Errorf("failed to convert prompts: %v", err)
}

// Register each prompt with its handler
for name, prompt := range goSdkPrompts {
handler := goSdkHandlers[name]
s.server.AddPrompt(prompt, handler)
}

klog.V(3).Infof("Registered %d prompts", len(goSdkPrompts))

return nil
}

func (s *Server) ServeStdio(ctx context.Context) error {
return s.server.Run(ctx, &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr})
}
Expand Down
23 changes: 22 additions & 1 deletion pkg/mcp/mcp_watch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,33 @@ func (s *WatchKubeConfigSuite) WaitForNotification() *mcp.JSONRPCNotification {
return notification
}

// WaitForToolsNotification waits for a tools/list_changed notification specifically
func (s *WatchKubeConfigSuite) WaitForToolsNotification() *mcp.JSONRPCNotification {
withTimeout, cancel := context.WithTimeout(s.T().Context(), 5*time.Second)
defer cancel()
var notification *mcp.JSONRPCNotification
s.OnNotification(func(n mcp.JSONRPCNotification) {
if n.Method == "notifications/tools/list_changed" {
notification = &n
}
})
for notification == nil {
select {
case <-withTimeout.Done():
s.FailNow("timeout waiting for tools/list_changed notification")
default:
time.Sleep(100 * time.Millisecond)
}
}
return notification
}

func (s *WatchKubeConfigSuite) TestNotifiesToolsChange() {
// Given
s.InitMcpClient()
// When
s.WriteKubeconfig()
notification := s.WaitForNotification()
notification := s.WaitForToolsNotification()
// Then
s.NotNil(notification, "WatchKubeConfig did not notify")
s.Equal("notifications/tools/list_changed", notification.Method, "WatchKubeConfig did not notify tools change")
Expand Down
1 change: 1 addition & 0 deletions pkg/mcp/modules.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package mcp

import _ "github.com/containers/kubernetes-mcp-server/pkg/promptsets/core"
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/config"
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/core"
import _ "github.com/containers/kubernetes-mcp-server/pkg/toolsets/helm"
Expand Down
75 changes: 75 additions & 0 deletions pkg/mcp/prompts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package mcp

import (
"context"

"github.com/modelcontextprotocol/go-sdk/mcp"

"github.com/containers/kubernetes-mcp-server/pkg/api"
)

// ServerPromptToGoSdkPrompt converts our internal ServerPrompt to go-sdk Prompt format
func ServerPromptToGoSdkPrompt(s *Server, prompts []api.ServerPrompt) (map[string]*mcp.Prompt, map[string]mcp.PromptHandler, error) {
goSdkPrompts := make(map[string]*mcp.Prompt)
goSdkHandlers := make(map[string]mcp.PromptHandler)

for _, prompt := range prompts {
// Convert arguments to PromptArgument pointers
var arguments []*mcp.PromptArgument
for _, arg := range prompt.Arguments {
arguments = append(arguments, &mcp.PromptArgument{
Name: arg.Name,
Description: arg.Description,
Required: arg.Required,
})
}

goSdkPrompt := &mcp.Prompt{
Name: prompt.Name,
Description: prompt.Description,
Arguments: arguments,
}

// Create the prompt handler
handler := createPromptHandler(s, prompt)

goSdkPrompts[prompt.Name] = goSdkPrompt
goSdkHandlers[prompt.Name] = handler
}

return goSdkPrompts, goSdkHandlers, nil
}

// createPromptHandler creates a handler function for a prompt
func createPromptHandler(s *Server, prompt api.ServerPrompt) mcp.PromptHandler {
return func(ctx context.Context, request *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
// Get arguments from the request
params := request.GetParams()
arguments := make(map[string]string)
if params != nil {
// Cast to concrete type to access Arguments field
if getPromptParams, ok := params.(*mcp.GetPromptParams); ok && getPromptParams != nil && getPromptParams.Arguments != nil {
arguments = getPromptParams.Arguments
}
}

// Get messages from the prompt
promptMessages := prompt.GetMessages(arguments)

// Convert to mcp-go format - need to use pointers
messages := make([]*mcp.PromptMessage, 0, len(promptMessages))
for _, msg := range promptMessages {
messages = append(messages, &mcp.PromptMessage{
Role: mcp.Role(msg.Role),
Content: &mcp.TextContent{
Text: msg.Content,
},
})
}

return &mcp.GetPromptResult{
Description: prompt.Description,
Messages: messages,
}, nil
}
}
Loading