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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,11 @@ In case multi-cluster support is enabled (default) and you have access to multip
- **events_list** - List all the Kubernetes events in the current cluster from all namespaces
- `namespace` (`string`) - Optional Namespace to retrieve the events from. If not provided, will list events from all namespaces

- **cluster_health_check** - Perform comprehensive health check on Kubernetes/OpenShift cluster and report issues. Examines cluster operators (OpenShift), nodes, deployments, pods, persistent volumes, and events to identify problems affecting cluster stability or workload availability.
- `check_events` (`boolean`) - Include recent warning events in the health check (may increase execution time)
- `output_format` (`string`) - Output format for results: 'text' (human-readable) or 'json' (machine-readable)
- `verbose` (`boolean`) - Enable detailed output with additional context and resource-level details

- **namespaces_list** - List all the Kubernetes namespaces in the current cluster

- **projects_list** - List all the OpenShift projects in the current cluster
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
45 changes: 45 additions & 0 deletions pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"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 @@ -29,6 +30,7 @@ type Configuration struct {
*config.StaticConfig
listOutput output.Output
toolsets []api.Toolset
promptsets []api.PromptSet
}

func (c *Configuration) Toolsets() []api.Toolset {
Expand All @@ -40,6 +42,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 @@ -148,11 +167,37 @@ func (s *Server) reloadKubernetesClusterProvider() error {

s.server.SetTools(m3labsServerTools...)

// 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())
}

m3labsPrompts, err := ServerPromptToM3LabsPrompt(allPrompts)
if err != nil {
return fmt.Errorf("failed to convert prompts: %v", err)
}

s.server.SetPrompts(m3labsPrompts...)
klog.V(3).Infof("Registered %d prompts", len(m3labsPrompts))

return nil
}

func (s *Server) ServeStdio() error {
return server.ServeStdio(s.server)
}
Expand Down
18 changes: 13 additions & 5 deletions pkg/mcp/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,21 @@ func (s *WatchKubeConfigSuite) TestNotifiesToolsChange() {
s.InitMcpClient()
withTimeout, cancel := context.WithTimeout(s.T().Context(), 5*time.Second)
defer cancel()
var notification *mcp.JSONRPCNotification
var toolsNotification *mcp.JSONRPCNotification
var promptsNotification *mcp.JSONRPCNotification
s.OnNotification(func(n mcp.JSONRPCNotification) {
notification = &n
if n.Method == "notifications/tools/list_changed" {
toolsNotification = &n
}
if n.Method == "notifications/prompts/list_changed" {
promptsNotification = &n
}
})
// When
f, _ := os.OpenFile(s.Cfg.KubeConfig, os.O_APPEND|os.O_WRONLY, 0644)
_, _ = f.WriteString("\n")
_ = f.Close()
for notification == nil {
for toolsNotification == nil || promptsNotification == nil {
select {
case <-withTimeout.Done():
s.FailNow("timeout waiting for WatchKubeConfig notification")
Expand All @@ -49,8 +55,10 @@ func (s *WatchKubeConfigSuite) TestNotifiesToolsChange() {
}
}
// Then
s.NotNil(notification, "WatchKubeConfig did not notify")
s.Equal("notifications/tools/list_changed", notification.Method, "WatchKubeConfig did not notify tools change")
s.NotNil(toolsNotification, "WatchKubeConfig did not notify tools change")
s.Equal("notifications/tools/list_changed", toolsNotification.Method, "WatchKubeConfig did not notify tools change")
s.NotNil(promptsNotification, "WatchKubeConfig did not notify prompts change")
s.Equal("notifications/prompts/list_changed", promptsNotification.Method, "WatchKubeConfig did not notify prompts change")
}

func TestWatchKubeConfig(t *testing.T) {
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"
72 changes: 72 additions & 0 deletions pkg/mcp/prompts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package mcp

import (
"context"

"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"

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

// ServerPromptToM3LabsPrompt converts our internal ServerPrompt to mcp-go ServerPrompt format
func ServerPromptToM3LabsPrompt(prompts []api.ServerPrompt) ([]server.ServerPrompt, error) {
m3labsPrompts := make([]server.ServerPrompt, 0, len(prompts))

for _, prompt := range prompts {
// Convert arguments
arguments := make([]mcp.PromptArgument, 0, len(prompt.Arguments))
for _, arg := range prompt.Arguments {
arguments = append(arguments, mcp.PromptArgument{
Name: arg.Name,
Description: arg.Description,
Required: arg.Required,
})
}

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

m3labsPrompts = append(m3labsPrompts, server.ServerPrompt{
Prompt: mcp.Prompt{
Name: prompt.Name,
Description: prompt.Description,
Arguments: arguments,
},
Handler: handler,
})
}

return m3labsPrompts, nil
}

// createPromptHandler creates a handler function for a prompt
func createPromptHandler(prompt api.ServerPrompt) server.PromptHandlerFunc {
return func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
// Get arguments from the request (already a map[string]string)
arguments := request.Params.Arguments
if arguments == nil {
arguments = make(map[string]string)
}

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

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

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