Skip to content

Commit 7994bf0

Browse files
committed
add configurable prompts support
Enable administrators to define custom MCP prompts in config.toml using template substitution with {{argument}} syntax. Features: - PromptLoader for loading prompts from TOML config - Core prompt types (ServerPrompt, Prompt, PromptArgument, PromptMessage) - Template argument substitution with {{variable}} syntax - Required argument validation - Integration with MCP server to register and serve config prompts Signed-off-by: Nader Ziada <[email protected]>
1 parent 724fa9f commit 7994bf0

File tree

15 files changed

+1120
-7
lines changed

15 files changed

+1120
-7
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,27 @@ vim /etc/kubernetes-mcp-server/conf.d/99-local.toml
292292
pkill -HUP kubernetes-mcp-server
293293
```
294294

295+
### MCP Prompts
296+
297+
The server supports MCP prompts for workflow templates. Define custom prompts in `config.toml`:
298+
299+
```toml
300+
[[prompts]]
301+
name = "my-workflow"
302+
title = "my workflow"
303+
description = "Custom workflow"
304+
305+
[[prompts.arguments]]
306+
name = "resource_name"
307+
required = true
308+
309+
[[prompts.messages]]
310+
role = "user"
311+
content = "Help me with {{resource_name}}"
312+
```
313+
314+
See docs/PROMPTS.md for detailed documentation.
315+
295316
## 🛠️ Tools and Functionalities <a id="tools-and-functionalities"></a>
296317

297318
The Kubernetes MCP server supports enabling or disabling specific groups of tools and functionalities (tools, resources, prompts, and so on) via the `--toolsets` command-line flag or `toolsets` configuration option.

docs/PROMPTS.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# MCP Prompts Support
2+
3+
The Kubernetes MCP Server supports [MCP Prompts](https://modelcontextprotocol.io/docs/concepts/prompts), which provide pre-defined workflow templates and guidance to AI assistants.
4+
5+
## What are MCP Prompts?
6+
7+
MCP Prompts are pre-defined templates that guide AI assistants through specific workflows. They combine:
8+
- **Structured guidance**: Step-by-step instructions for common tasks
9+
- **Parameterization**: Arguments that customize the prompt for specific contexts
10+
- **Conversation templates**: Pre-formatted messages that guide the interaction
11+
12+
## Creating Custom Prompts
13+
14+
Define custom prompts in your `config.toml` file - no code changes or recompilation needed!
15+
16+
### Example
17+
18+
```toml
19+
[[prompts]]
20+
name = "check-pod-logs"
21+
title = "Check Pod Logs"
22+
description = "Quick way to check pod logs"
23+
24+
[[prompts.arguments]]
25+
name = "pod_name"
26+
description = "Name of the pod"
27+
required = true
28+
29+
[[prompts.arguments]]
30+
name = "namespace"
31+
description = "Namespace of the pod"
32+
required = false
33+
34+
[[prompts.messages]]
35+
role = "user"
36+
content = "Show me the logs for pod {{pod_name}} in {{namespace}}"
37+
38+
[[prompts.messages]]
39+
role = "assistant"
40+
content = "I'll retrieve and analyze the logs for you."
41+
```
42+
43+
## Configuration Reference
44+
45+
### Prompt Fields
46+
- **name** (required): Unique identifier for the prompt
47+
- **title** (optional): Human-readable display name
48+
- **description** (required): Brief explanation of what the prompt does
49+
- **arguments** (optional): List of parameters the prompt accepts
50+
- **messages** (required): Conversation template with role/content pairs
51+
52+
### Argument Fields
53+
- **name** (required): Argument identifier
54+
- **description** (optional): Explanation of the argument's purpose
55+
- **required** (optional): Whether the argument must be provided (default: false)
56+
57+
### Argument Substitution
58+
Use `{{argument_name}}` placeholders in message content. The template engine replaces these with actual values when the prompt is called.
59+
60+
## Configuration File Location
61+
62+
Place your prompts in the `config.toml` file used by the MCP server. Specify the config file path using the `--config` flag when starting the server.

pkg/api/prompt_config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package api
2+
3+
import (
4+
"context"
5+
6+
"github.com/BurntSushi/toml"
7+
"github.com/containers/kubernetes-mcp-server/pkg/config"
8+
)
9+
10+
// promptsParser parses prompts from TOML configuration
11+
func promptsParser(ctx context.Context, primitive toml.Primitive, md toml.MetaData) (interface{}, error) {
12+
var prompts []Prompt
13+
if err := md.PrimitiveDecode(primitive, &prompts); err != nil {
14+
return nil, err
15+
}
16+
return prompts, nil
17+
}
18+
19+
func init() {
20+
// Register the prompts parser with the config package
21+
config.RegisterPromptsParser(promptsParser)
22+
}

pkg/api/prompt_loader.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// PromptLoader loads prompts from configuration
9+
type PromptLoader struct {
10+
prompts []Prompt
11+
}
12+
13+
// NewPromptLoader creates a new prompt loader
14+
func NewPromptLoader() *PromptLoader {
15+
return &PromptLoader{
16+
prompts: make([]Prompt, 0),
17+
}
18+
}
19+
20+
// GetServerPrompts converts loaded prompts to ServerPrompt instances with handlers
21+
func (l *PromptLoader) GetServerPrompts() []ServerPrompt {
22+
serverPrompts := make([]ServerPrompt, 0, len(l.prompts))
23+
for _, prompt := range l.prompts {
24+
serverPrompts = append(serverPrompts, ServerPrompt{
25+
Prompt: prompt,
26+
Handler: l.createHandler(prompt),
27+
})
28+
}
29+
return serverPrompts
30+
}
31+
32+
// createHandler creates a prompt handler function for a prompt
33+
func (l *PromptLoader) createHandler(prompt Prompt) PromptHandlerFunc {
34+
return func(params PromptHandlerParams) (*PromptCallResult, error) {
35+
args := params.GetArguments()
36+
37+
// Validate required arguments
38+
for _, arg := range prompt.Arguments {
39+
if arg.Required {
40+
if _, exists := args[arg.Name]; !exists {
41+
return nil, fmt.Errorf("required argument '%s' is missing", arg.Name)
42+
}
43+
}
44+
}
45+
46+
// Render messages with argument substitution
47+
messages := make([]PromptMessage, 0, len(prompt.Templates))
48+
for _, template := range prompt.Templates {
49+
content := l.substituteArguments(template.Content, args)
50+
messages = append(messages, PromptMessage{
51+
Role: template.Role,
52+
Content: PromptContent{
53+
Type: "text",
54+
Text: content,
55+
},
56+
})
57+
}
58+
59+
return NewPromptCallResult(prompt.Description, messages, nil), nil
60+
}
61+
}
62+
63+
// substituteArguments replaces {{argument}} placeholders in content with actual values
64+
func (l *PromptLoader) substituteArguments(content string, args map[string]string) string {
65+
result := content
66+
for key, value := range args {
67+
placeholder := fmt.Sprintf("{{%s}}", key)
68+
result = strings.ReplaceAll(result, placeholder, value)
69+
}
70+
return result
71+
}
72+
73+
// LoadFromParsedConfig loads prompts from already-parsed config data
74+
// parsedPrompts is stored as interface{} in config.Config to avoid circular dependencies
75+
func (l *PromptLoader) LoadFromParsedConfig(parsedPrompts interface{}) error {
76+
if parsedPrompts == nil {
77+
return nil
78+
}
79+
80+
// Type assert to []Prompt
81+
prompts, ok := parsedPrompts.([]Prompt)
82+
if !ok {
83+
return fmt.Errorf("unexpected type for parsed prompts: %T, expected []api.Prompt", parsedPrompts)
84+
}
85+
86+
l.prompts = append(l.prompts, prompts...)
87+
return nil
88+
}

pkg/api/prompts.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package api
2+
3+
import (
4+
"context"
5+
6+
internalk8s "github.com/containers/kubernetes-mcp-server/pkg/kubernetes"
7+
)
8+
9+
// ServerPrompt represents a prompt that can be registered with the MCP server.
10+
// Prompts provide pre-defined workflow templates and guidance to AI assistants.
11+
type ServerPrompt struct {
12+
Prompt Prompt
13+
Handler PromptHandlerFunc
14+
ClusterAware *bool
15+
ArgumentSchema map[string]PromptArgument
16+
}
17+
18+
// IsClusterAware indicates whether the prompt can accept a "cluster" or "context" parameter
19+
// to operate on a specific Kubernetes cluster context.
20+
// Defaults to true if not explicitly set
21+
func (s *ServerPrompt) IsClusterAware() bool {
22+
if s.ClusterAware != nil {
23+
return *s.ClusterAware
24+
}
25+
return true
26+
}
27+
28+
// Prompt represents the metadata and content of an MCP prompt.
29+
// See MCP specification: https://spec.modelcontextprotocol.io/specification/server/prompts/
30+
type Prompt struct {
31+
Name string `yaml:"name" json:"name" toml:"name"`
32+
Title string `yaml:"title,omitempty" json:"title,omitempty" toml:"title,omitempty"`
33+
Description string `yaml:"description,omitempty" json:"description,omitempty" toml:"description,omitempty"`
34+
Arguments []PromptArgument `yaml:"arguments,omitempty" json:"arguments,omitempty" toml:"arguments,omitempty"`
35+
Templates []PromptTemplate `yaml:"messages,omitempty" json:"messages,omitempty" toml:"messages,omitempty"`
36+
}
37+
38+
// PromptArgument defines a parameter that can be passed to a prompt.
39+
// See MCP specification: https://spec.modelcontextprotocol.io/specification/server/prompts/
40+
type PromptArgument struct {
41+
Name string `yaml:"name" json:"name" toml:"name"`
42+
Description string `yaml:"description,omitempty" json:"description,omitempty" toml:"description,omitempty"`
43+
Required bool `yaml:"required" json:"required" toml:"required"`
44+
}
45+
46+
// PromptTemplate represents a message template from configuration with placeholders like {{arg}}.
47+
// This is used for configuration parsing and gets rendered into PromptMessage at runtime.
48+
type PromptTemplate struct {
49+
Role string `yaml:"role" json:"role" toml:"role"`
50+
Content string `yaml:"content" json:"content" toml:"content"`
51+
}
52+
53+
// PromptMessage represents a single message in a prompt response.
54+
// See MCP specification: https://spec.modelcontextprotocol.io/specification/server/prompts/
55+
type PromptMessage struct {
56+
Role string `yaml:"role" json:"role" toml:"role"`
57+
Content PromptContent `yaml:"content" json:"content" toml:"content"`
58+
}
59+
60+
// PromptContent represents the content of a prompt message.
61+
// See MCP specification: https://spec.modelcontextprotocol.io/specification/server/prompts/
62+
type PromptContent struct {
63+
Type string `yaml:"type" json:"type" toml:"type"`
64+
Text string `yaml:"text,omitempty" json:"text,omitempty" toml:"text,omitempty"`
65+
}
66+
67+
// PromptCallRequest interface for accessing prompt call arguments
68+
type PromptCallRequest interface {
69+
GetArguments() map[string]string
70+
}
71+
72+
// PromptCallResult represents the result of executing a prompt
73+
type PromptCallResult struct {
74+
Description string
75+
Messages []PromptMessage
76+
Error error
77+
}
78+
79+
// NewPromptCallResult creates a new PromptCallResult
80+
func NewPromptCallResult(description string, messages []PromptMessage, err error) *PromptCallResult {
81+
return &PromptCallResult{
82+
Description: description,
83+
Messages: messages,
84+
Error: err,
85+
}
86+
}
87+
88+
// PromptHandlerParams contains the parameters passed to a prompt handler
89+
type PromptHandlerParams struct {
90+
context.Context
91+
*internalk8s.Kubernetes
92+
PromptCallRequest
93+
}
94+
95+
// PromptHandlerFunc is a function that handles prompt execution
96+
type PromptHandlerFunc func(params PromptHandlerParams) (*PromptCallResult, error)

pkg/api/prompts_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package api
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"k8s.io/utils/ptr"
8+
)
9+
10+
func TestServerPrompt_IsClusterAware(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
clusterAware *bool
14+
want bool
15+
}{
16+
{
17+
name: "nil defaults to true",
18+
clusterAware: nil,
19+
want: true,
20+
},
21+
{
22+
name: "explicitly true",
23+
clusterAware: ptr.To(true),
24+
want: true,
25+
},
26+
{
27+
name: "explicitly false",
28+
clusterAware: ptr.To(false),
29+
want: false,
30+
},
31+
}
32+
33+
for _, tt := range tests {
34+
t.Run(tt.name, func(t *testing.T) {
35+
sp := &ServerPrompt{
36+
ClusterAware: tt.clusterAware,
37+
}
38+
assert.Equal(t, tt.want, sp.IsClusterAware())
39+
})
40+
}
41+
}
42+
43+
func TestNewPromptCallResult(t *testing.T) {
44+
tests := []struct {
45+
name string
46+
description string
47+
messages []PromptMessage
48+
err error
49+
}{
50+
{
51+
name: "successful result",
52+
description: "Test description",
53+
messages: []PromptMessage{
54+
{
55+
Role: "user",
56+
Content: PromptContent{
57+
Type: "text",
58+
Text: "Hello",
59+
},
60+
},
61+
},
62+
err: nil,
63+
},
64+
{
65+
name: "result with error",
66+
description: "Error description",
67+
messages: nil,
68+
err: assert.AnError,
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
result := NewPromptCallResult(tt.description, tt.messages, tt.err)
75+
assert.Equal(t, tt.description, result.Description)
76+
assert.Equal(t, tt.messages, result.Messages)
77+
assert.Equal(t, tt.err, result.Error)
78+
})
79+
}
80+
}

0 commit comments

Comments
 (0)