From c8bb280aeed4ccd935e1c48380316833d11c9509 Mon Sep 17 00:00:00 2001 From: Nader Ziada Date: Tue, 9 Dec 2025 16:54:26 -0500 Subject: [PATCH 1/2] add toolset prompts infrastructure Enable toolset implementers to define MCP prompts programmatically as part of their toolset definition. - Add GetPrompts() method to Toolset interface - Collect prompts from all enabled toolsets - Merge config and toolset prompts (config takes precedence) - Add disable_toolset_prompts configuration option - Update all existing toolsets to implement GetPrompts() Signed-off-by: Nader Ziada --- README.md | 4 +- docs/PROMPTS.md | 64 +++- pkg/api/toolsets.go | 3 + pkg/config/config.go | 3 + pkg/mcp/mcp.go | 27 +- pkg/mcp/mcp_toolset_prompts_test.go | 452 ++++++++++++++++++++++++++++ pkg/toolsets/config/toolset.go | 5 + pkg/toolsets/core/toolset.go | 5 + pkg/toolsets/helm/toolset.go | 5 + pkg/toolsets/kiali/toolset.go | 5 + pkg/toolsets/kubevirt/toolset.go | 5 + pkg/toolsets/toolsets_test.go | 2 + 12 files changed, 575 insertions(+), 5 deletions(-) create mode 100644 pkg/mcp/mcp_toolset_prompts_test.go diff --git a/README.md b/README.md index d7a8d4440..76d0ad72e 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ pkill -HUP kubernetes-mcp-server ### MCP Prompts -The server supports MCP prompts for workflow templates. Define custom prompts in `config.toml`: +1. The server supports MCP prompts for workflow templates. Define custom prompts in `config.toml`: ```toml [[prompts]] @@ -311,6 +311,8 @@ role = "user" content = "Help me with {{resource_name}}" ``` +2. Toolset prompts implemented by toolset developers (can be disabled with disable_toolset_prompts = true) + See docs/PROMPTS.md for detailed documentation. ## 🛠️ Tools and Functionalities diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index fe6f6d654..c5b7b7a19 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -59,4 +59,66 @@ Use `{{argument_name}}` placeholders in message content. The template engine rep ## Configuration File Location -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. \ No newline at end of file +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. + +## Toolset Prompts + +Toolsets can provide built-in prompts by implementing the `GetPrompts()` method. This allows toolset developers to ship workflow templates alongside their tools. + +### Implementing Toolset Prompts + +```go +func (t *MyToolset) GetPrompts(o internalk8s.Openshift) []api.ServerPrompt { + return []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "my-workflow", + Description: "Custom workflow for my toolset", + Arguments: []api.PromptArgument{ + { + Name: "namespace", + Description: "Target namespace", + Required: true, + }, + }, + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + args := params.GetArguments() + namespace := args["namespace"] + + // Build messages dynamically based on arguments + messages := []api.PromptMessage{ + { + Role: "user", + Content: api.PromptContent{ + Type: "text", + Text: fmt.Sprintf("Help me with namespace: %s", namespace), + }, + }, + } + + return api.NewPromptCallResult("Workflow description", messages, nil), nil + }, + }, + } +} +``` + +### Disabling Toolset Prompts + +Add `disable_toolset_prompts = true` in your `config.toml` to use only config-defined prompts: + +```toml +disable_toolset_prompts = true + +[[prompts]] +name = "my-custom-prompt" +# ... rest of config +``` + +### Prompt Merging + +When both toolset and config prompts exist: +- Config-defined prompts **override** toolset prompts with the same name +- This allows administrators to customize built-in workflows +- Prompts with unique names from both sources are available diff --git a/pkg/api/toolsets.go b/pkg/api/toolsets.go index 3084f2954..3c07e8757 100644 --- a/pkg/api/toolsets.go +++ b/pkg/api/toolsets.go @@ -43,6 +43,9 @@ type Toolset interface { // Will be used to generate documentation and help text. GetDescription() string GetTools(o Openshift) []ServerTool + // GetPrompts returns the prompts provided by this toolset. + // Returns nil if the toolset doesn't provide any prompts. + GetPrompts(o Openshift) []ServerPrompt } type ToolCallRequest interface { diff --git a/pkg/config/config.go b/pkg/config/config.go index 456f6ec26..324ee91df 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -81,6 +81,9 @@ type StaticConfig struct { promptsDefined bool // Internal: tracks if prompts were defined in config promptsMetadata toml.MetaData // Internal: metadata for prompts decoding + // When true, disable toolset-defined prompts (only use config-defined prompts) + DisableToolsetPrompts bool `toml:"disable_toolset_prompts,omitempty"` + // Server instructions to be provided by the MCP server to the MCP client // This can be used to provide specific instructions on how the client should use the server ServerInstructions string `toml:"server_instructions,omitempty"` diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index bd61836fb..10dedd06e 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -167,6 +167,24 @@ func (s *Server) reloadToolsets() error { // Track previously enabled prompts previousPrompts := s.enabledPrompts + // Build and register prompts from all toolsets + applicablePrompts := make([]api.ServerPrompt, 0) + s.enabledPrompts = make([]string, 0) + + // Load embedded toolset prompts (unless disabled) + if !s.configuration.DisableToolsetPrompts { + for _, toolset := range s.configuration.Toolsets() { + prompts := toolset.GetPrompts(s.p) + if prompts == nil { + continue + } + for _, prompt := range prompts { + applicablePrompts = append(applicablePrompts, prompt) + s.enabledPrompts = append(s.enabledPrompts, prompt.Prompt.Name) + } + } + } + // Load config prompts into registry prompts.Clear() if s.configuration.HasPrompts() { @@ -180,9 +198,12 @@ func (s *Server) reloadToolsets() error { // Get prompts from registry configPrompts := prompts.ConfigPrompts() + // Merge: config prompts override embedded prompts with same name + applicablePrompts = mergePrompts(applicablePrompts, configPrompts) + // Update enabled prompts list s.enabledPrompts = make([]string, 0) - for _, prompt := range configPrompts { + for _, prompt := range applicablePrompts { s.enabledPrompts = append(s.enabledPrompts, prompt.Prompt.Name) } @@ -195,8 +216,8 @@ func (s *Server) reloadToolsets() error { } s.server.RemovePrompts(promptsToRemove...) - // Register all config prompts - for _, prompt := range configPrompts { + // Register all applicable prompts + for _, prompt := range applicablePrompts { mcpPrompt, promptHandler, err := ServerPromptToGoSdkPrompt(s, prompt) if err != nil { return fmt.Errorf("failed to convert prompt %s: %v", prompt.Prompt.Name, err) diff --git a/pkg/mcp/mcp_toolset_prompts_test.go b/pkg/mcp/mcp_toolset_prompts_test.go new file mode 100644 index 000000000..0f1bb7668 --- /dev/null +++ b/pkg/mcp/mcp_toolset_prompts_test.go @@ -0,0 +1,452 @@ +package mcp + +import ( + "testing" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/stretchr/testify/suite" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/config" + "github.com/containers/kubernetes-mcp-server/pkg/toolsets" +) + +// McpToolsetPromptsSuite tests toolset prompts integration +type McpToolsetPromptsSuite struct { + BaseMcpSuite + originalToolsets []api.Toolset +} + +func (s *McpToolsetPromptsSuite) SetupTest() { + s.BaseMcpSuite.SetupTest() + s.originalToolsets = toolsets.Toolsets() +} + +func (s *McpToolsetPromptsSuite) TearDownTest() { + s.BaseMcpSuite.TearDownTest() + // Restore original toolsets + toolsets.Clear() + for _, toolset := range s.originalToolsets { + toolsets.Register(toolset) + } +} + +func (s *McpToolsetPromptsSuite) TestToolsetReturningPrompts() { + testToolset := &mockToolsetWithPrompts{ + name: "test-toolset", + description: "Test toolset with prompts", + prompts: []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "toolset-prompt", + Description: "A prompt from a toolset", + Arguments: []api.PromptArgument{ + {Name: "arg1", Description: "Test argument", Required: true}, + }, + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + args := params.GetArguments() + messages := []api.PromptMessage{ + { + Role: "user", + Content: api.PromptContent{ + Type: "text", + Text: "Toolset prompt with " + args["arg1"], + }, + }, + } + return api.NewPromptCallResult("Toolset prompt result", messages, nil), nil + }, + }, + }, + } + + toolsets.Clear() + toolsets.Register(testToolset) + s.Cfg.Toolsets = []string{"test-toolset"} + + s.InitMcpClient() + + prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) + + s.Run("ListPrompts returns toolset prompts", func() { + s.NoError(err) + s.NotNil(prompts) + }) + + s.Run("toolset prompt is available", func() { + s.Require().NotNil(prompts) + var found bool + for _, prompt := range prompts.Prompts { + if prompt.Name == "toolset-prompt" { + found = true + s.Equal("A prompt from a toolset", prompt.Description) + s.Require().Len(prompt.Arguments, 1) + s.Equal("arg1", prompt.Arguments[0].Name) + s.True(prompt.Arguments[0].Required) + break + } + } + s.True(found, "expected toolset prompt to be available") + }) + + s.Run("toolset prompt handler executes correctly", func() { + result, err := s.GetPrompt(s.T().Context(), mcp.GetPromptRequest{ + Params: mcp.GetPromptParams{ + Name: "toolset-prompt", + Arguments: map[string]string{ + "arg1": "test-value", + }, + }, + }) + + s.NoError(err) + s.Require().NotNil(result) + s.Equal("Toolset prompt result", result.Description) + s.Require().Len(result.Messages, 1) + s.Equal("user", string(result.Messages[0].Role)) + + textContent, ok := result.Messages[0].Content.(mcp.TextContent) + s.Require().True(ok, "expected TextContent") + s.Equal("Toolset prompt with test-value", textContent.Text) + }) +} + +func (s *McpToolsetPromptsSuite) TestToolsetReturningNilPrompts() { + testToolset := &mockToolsetWithPrompts{ + name: "empty-toolset", + description: "Toolset with no prompts", + prompts: nil, + } + + toolsets.Clear() + toolsets.Register(testToolset) + s.Cfg.Toolsets = []string{"empty-toolset"} + + s.InitMcpClient() + + prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) + + s.Run("ListPrompts succeeds with nil toolset prompts", func() { + s.NoError(err) + s.NotNil(prompts) + }) + + s.Run("no prompts returned from nil toolset", func() { + s.Require().NotNil(prompts) + s.Empty(prompts.Prompts) + }) +} + +func (s *McpToolsetPromptsSuite) TestToolsetReturningEmptyPrompts() { + testToolset := &mockToolsetWithPrompts{ + name: "empty-slice-toolset", + description: "Toolset with empty prompts slice", + prompts: []api.ServerPrompt{}, + } + + toolsets.Clear() + toolsets.Register(testToolset) + s.Cfg.Toolsets = []string{"empty-slice-toolset"} + + s.InitMcpClient() + + prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) + + s.Run("ListPrompts succeeds with empty toolset prompts", func() { + s.NoError(err) + s.NotNil(prompts) + }) + + s.Run("no prompts returned from empty slice toolset", func() { + s.Require().NotNil(prompts) + s.Empty(prompts.Prompts) + }) +} + +func (s *McpToolsetPromptsSuite) TestMultipleToolsetsPromptCollection() { + toolset1 := &mockToolsetWithPrompts{ + name: "toolset1", + description: "First toolset", + prompts: []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "prompt1", + Description: "Prompt from toolset1", + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + return api.NewPromptCallResult("Prompt1", []api.PromptMessage{}, nil), nil + }, + }, + }, + } + + toolset2 := &mockToolsetWithPrompts{ + name: "toolset2", + description: "Second toolset", + prompts: []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "prompt2", + Description: "Prompt from toolset2", + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + return api.NewPromptCallResult("Prompt2", []api.PromptMessage{}, nil), nil + }, + }, + }, + } + + toolsets.Clear() + toolsets.Register(toolset1) + toolsets.Register(toolset2) + s.Cfg.Toolsets = []string{"toolset1", "toolset2"} + + s.InitMcpClient() + + prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) + + s.Run("ListPrompts collects from multiple toolsets", func() { + s.NoError(err) + s.Require().NotNil(prompts) + s.Require().Len(prompts.Prompts, 2) + }) + + s.Run("prompts from both toolsets are available", func() { + s.Require().NotNil(prompts) + promptNames := make(map[string]bool) + for _, prompt := range prompts.Prompts { + promptNames[prompt.Name] = true + } + s.True(promptNames["prompt1"], "expected prompt1 from toolset1") + s.True(promptNames["prompt2"], "expected prompt2 from toolset2") + }) +} + +func (s *McpToolsetPromptsSuite) TestConfigPromptsOverrideToolsetPrompts() { + testToolset := &mockToolsetWithPrompts{ + name: "test-toolset", + description: "Test toolset", + prompts: []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "shared-prompt", + Description: "Toolset version", + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + return api.NewPromptCallResult("Toolset", []api.PromptMessage{ + { + Role: "user", + Content: api.PromptContent{ + Type: "text", + Text: "From toolset", + }, + }, + }, nil), nil + }, + }, + }, + } + + toolsets.Clear() + toolsets.Register(testToolset) + + // Add config prompt with same name + cfg, err := config.ReadToml([]byte(` +toolsets = ["test-toolset"] + +[[prompts]] +name = "shared-prompt" +description = "Config version" + +[[prompts.messages]] +role = "user" +content = "From config" + `)) + s.Require().NoError(err) + // Preserve kubeconfig from SetupTest + cfg.KubeConfig = s.Cfg.KubeConfig + s.Cfg = cfg + + s.InitMcpClient() + + prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) + + s.Run("ListPrompts returns prompts", func() { + s.NoError(err) + s.Require().NotNil(prompts) + s.Require().Len(prompts.Prompts, 1) + }) + + s.Run("config prompt overrides toolset prompt", func() { + s.Require().NotNil(prompts) + s.Equal("shared-prompt", prompts.Prompts[0].Name) + s.Equal("Config version", prompts.Prompts[0].Description) + }) + + s.Run("config prompt handler is used", func() { + result, err := s.GetPrompt(s.T().Context(), mcp.GetPromptRequest{ + Params: mcp.GetPromptParams{ + Name: "shared-prompt", + }, + }) + + s.NoError(err) + s.Require().NotNil(result) + s.Require().Len(result.Messages, 1) + + textContent, ok := result.Messages[0].Content.(mcp.TextContent) + s.Require().True(ok) + s.Equal("From config", textContent.Text) + }) +} + +func (s *McpToolsetPromptsSuite) TestPromptsNotExposedWhenToolsetDisabled() { + enabledToolset := &mockToolsetWithPrompts{ + name: "enabled-toolset", + description: "Enabled toolset", + prompts: []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "enabled-prompt", + Description: "From enabled toolset", + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + return api.NewPromptCallResult("Enabled", []api.PromptMessage{}, nil), nil + }, + }, + }, + } + + disabledToolset := &mockToolsetWithPrompts{ + name: "disabled-toolset", + description: "Disabled toolset", + prompts: []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "disabled-prompt", + Description: "From disabled toolset", + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + return api.NewPromptCallResult("Disabled", []api.PromptMessage{}, nil), nil + }, + }, + }, + } + + toolsets.Clear() + toolsets.Register(enabledToolset) + toolsets.Register(disabledToolset) + // Only enable one toolset + s.Cfg.Toolsets = []string{"enabled-toolset"} + + s.InitMcpClient() + + prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) + + s.Run("ListPrompts returns prompts", func() { + s.NoError(err) + s.Require().NotNil(prompts) + }) + + s.Run("only enabled toolset prompts are available", func() { + s.Require().NotNil(prompts) + s.Require().Len(prompts.Prompts, 1) + s.Equal("enabled-prompt", prompts.Prompts[0].Name) + }) + + s.Run("disabled toolset prompts are not available", func() { + s.Require().NotNil(prompts) + for _, prompt := range prompts.Prompts { + s.NotEqual("disabled-prompt", prompt.Name) + } + }) +} + +func (s *McpToolsetPromptsSuite) TestDisableToolsetPromptsConfig() { + testToolset := &mockToolsetWithPrompts{ + name: "test-toolset", + description: "Test toolset", + prompts: []api.ServerPrompt{ + { + Prompt: api.Prompt{ + Name: "toolset-prompt", + Description: "Should be disabled", + }, + Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { + return api.NewPromptCallResult("Toolset", []api.PromptMessage{}, nil), nil + }, + }, + }, + } + + toolsets.Clear() + toolsets.Register(testToolset) + + // Add config prompt + cfg, err := config.ReadToml([]byte(` +toolsets = ["test-toolset"] +disable_toolset_prompts = true + +[[prompts]] +name = "config-prompt" +description = "Config prompt only" + +[[prompts.messages]] +role = "user" +content = "From config" + `)) + s.Require().NoError(err) + // Preserve kubeconfig from SetupTest + cfg.KubeConfig = s.Cfg.KubeConfig + s.Cfg = cfg + + s.InitMcpClient() + + prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) + + s.Run("ListPrompts returns prompts", func() { + s.NoError(err) + s.Require().NotNil(prompts) + }) + + s.Run("only config prompts are available", func() { + s.Require().NotNil(prompts) + s.Require().Len(prompts.Prompts, 1) + s.Equal("config-prompt", prompts.Prompts[0].Name) + }) + + s.Run("toolset prompts are not available when disabled", func() { + s.Require().NotNil(prompts) + for _, prompt := range prompts.Prompts { + s.NotEqual("toolset-prompt", prompt.Name) + } + }) +} + +// Mock toolset for testing +type mockToolsetWithPrompts struct { + name string + description string + prompts []api.ServerPrompt +} + +func (m *mockToolsetWithPrompts) GetName() string { + return m.name +} + +func (m *mockToolsetWithPrompts) GetDescription() string { + return m.description +} + +func (m *mockToolsetWithPrompts) GetTools(_ api.Openshift) []api.ServerTool { + return nil +} + +func (m *mockToolsetWithPrompts) GetPrompts(_ api.Openshift) []api.ServerPrompt { + return m.prompts +} + +func TestMcpToolsetPromptsSuite(t *testing.T) { + suite.Run(t, new(McpToolsetPromptsSuite)) +} diff --git a/pkg/toolsets/config/toolset.go b/pkg/toolsets/config/toolset.go index d995b26c8..2dca94149 100644 --- a/pkg/toolsets/config/toolset.go +++ b/pkg/toolsets/config/toolset.go @@ -25,6 +25,11 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } +func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { + // Config toolset does not provide prompts + return nil +} + func init() { toolsets.Register(&Toolset{}) } diff --git a/pkg/toolsets/core/toolset.go b/pkg/toolsets/core/toolset.go index 5a1a8e888..f8b428f77 100644 --- a/pkg/toolsets/core/toolset.go +++ b/pkg/toolsets/core/toolset.go @@ -29,6 +29,11 @@ func (t *Toolset) GetTools(o api.Openshift) []api.ServerTool { ) } +func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { + // Core toolset prompts will be added in Feature 3 + return nil +} + func init() { toolsets.Register(&Toolset{}) } diff --git a/pkg/toolsets/helm/toolset.go b/pkg/toolsets/helm/toolset.go index 36a0ba28c..a0e2e9119 100644 --- a/pkg/toolsets/helm/toolset.go +++ b/pkg/toolsets/helm/toolset.go @@ -25,6 +25,11 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } +func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { + // Helm toolset does not provide prompts + return nil +} + func init() { toolsets.Register(&Toolset{}) } diff --git a/pkg/toolsets/kiali/toolset.go b/pkg/toolsets/kiali/toolset.go index 70db0e8c3..f42385146 100644 --- a/pkg/toolsets/kiali/toolset.go +++ b/pkg/toolsets/kiali/toolset.go @@ -32,6 +32,11 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } +func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { + // Kiali toolset does not provide prompts + return nil +} + func init() { toolsets.Register(&Toolset{}) } diff --git a/pkg/toolsets/kubevirt/toolset.go b/pkg/toolsets/kubevirt/toolset.go index 221f82543..8aa8aa9c1 100644 --- a/pkg/toolsets/kubevirt/toolset.go +++ b/pkg/toolsets/kubevirt/toolset.go @@ -28,6 +28,11 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } +func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { + // KubeVirt toolset does not provide prompts + return nil +} + func init() { toolsets.Register(&Toolset{}) } diff --git a/pkg/toolsets/toolsets_test.go b/pkg/toolsets/toolsets_test.go index 971964d75..70185eee9 100644 --- a/pkg/toolsets/toolsets_test.go +++ b/pkg/toolsets/toolsets_test.go @@ -34,6 +34,8 @@ func (t *TestToolset) GetDescription() string { return t.description } func (t *TestToolset) GetTools(_ api.Openshift) []api.ServerTool { return nil } +func (t *TestToolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { return nil } + var _ api.Toolset = (*TestToolset)(nil) func (s *ToolsetsSuite) TestToolsetNames() { From 7f97bafb089f683ce8264936601aab7d7d57c54c Mon Sep 17 00:00:00 2001 From: Nader Ziada Date: Fri, 12 Dec 2025 10:21:52 -0500 Subject: [PATCH 2/2] remove disable_toolset_prompts flag and openshift flag Signed-off-by: Nader Ziada --- README.md | 2 +- docs/PROMPTS.md | 14 +------ pkg/api/toolsets.go | 2 +- pkg/config/config.go | 3 -- pkg/mcp/mcp.go | 20 +++++---- pkg/mcp/mcp_toolset_prompts_test.go | 63 +---------------------------- pkg/toolsets/config/toolset.go | 2 +- pkg/toolsets/core/toolset.go | 2 +- pkg/toolsets/helm/toolset.go | 2 +- pkg/toolsets/kiali/toolset.go | 2 +- pkg/toolsets/kubevirt/toolset.go | 2 +- pkg/toolsets/toolsets_test.go | 2 +- 12 files changed, 19 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 76d0ad72e..644cb4247 100644 --- a/README.md +++ b/README.md @@ -311,7 +311,7 @@ role = "user" content = "Help me with {{resource_name}}" ``` -2. Toolset prompts implemented by toolset developers (can be disabled with disable_toolset_prompts = true) +2. Toolset prompts implemented by toolset developers See docs/PROMPTS.md for detailed documentation. diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index c5b7b7a19..11c45589c 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -68,7 +68,7 @@ Toolsets can provide built-in prompts by implementing the `GetPrompts()` method. ### Implementing Toolset Prompts ```go -func (t *MyToolset) GetPrompts(o internalk8s.Openshift) []api.ServerPrompt { +func (t *MyToolset) GetPrompts() []api.ServerPrompt { return []api.ServerPrompt{ { Prompt: api.Prompt{ @@ -104,18 +104,6 @@ func (t *MyToolset) GetPrompts(o internalk8s.Openshift) []api.ServerPrompt { } ``` -### Disabling Toolset Prompts - -Add `disable_toolset_prompts = true` in your `config.toml` to use only config-defined prompts: - -```toml -disable_toolset_prompts = true - -[[prompts]] -name = "my-custom-prompt" -# ... rest of config -``` - ### Prompt Merging When both toolset and config prompts exist: diff --git a/pkg/api/toolsets.go b/pkg/api/toolsets.go index 3c07e8757..59b1f3c70 100644 --- a/pkg/api/toolsets.go +++ b/pkg/api/toolsets.go @@ -45,7 +45,7 @@ type Toolset interface { GetTools(o Openshift) []ServerTool // GetPrompts returns the prompts provided by this toolset. // Returns nil if the toolset doesn't provide any prompts. - GetPrompts(o Openshift) []ServerPrompt + GetPrompts() []ServerPrompt } type ToolCallRequest interface { diff --git a/pkg/config/config.go b/pkg/config/config.go index 324ee91df..456f6ec26 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -81,9 +81,6 @@ type StaticConfig struct { promptsDefined bool // Internal: tracks if prompts were defined in config promptsMetadata toml.MetaData // Internal: metadata for prompts decoding - // When true, disable toolset-defined prompts (only use config-defined prompts) - DisableToolsetPrompts bool `toml:"disable_toolset_prompts,omitempty"` - // Server instructions to be provided by the MCP server to the MCP client // This can be used to provide specific instructions on how the client should use the server ServerInstructions string `toml:"server_instructions,omitempty"` diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 10dedd06e..3c990ccb2 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -171,17 +171,15 @@ func (s *Server) reloadToolsets() error { applicablePrompts := make([]api.ServerPrompt, 0) s.enabledPrompts = make([]string, 0) - // Load embedded toolset prompts (unless disabled) - if !s.configuration.DisableToolsetPrompts { - for _, toolset := range s.configuration.Toolsets() { - prompts := toolset.GetPrompts(s.p) - if prompts == nil { - continue - } - for _, prompt := range prompts { - applicablePrompts = append(applicablePrompts, prompt) - s.enabledPrompts = append(s.enabledPrompts, prompt.Prompt.Name) - } + // Load embedded toolset prompts + for _, toolset := range s.configuration.Toolsets() { + prompts := toolset.GetPrompts() + if prompts == nil { + continue + } + for _, prompt := range prompts { + applicablePrompts = append(applicablePrompts, prompt) + s.enabledPrompts = append(s.enabledPrompts, prompt.Prompt.Name) } } diff --git a/pkg/mcp/mcp_toolset_prompts_test.go b/pkg/mcp/mcp_toolset_prompts_test.go index 0f1bb7668..3256f882f 100644 --- a/pkg/mcp/mcp_toolset_prompts_test.go +++ b/pkg/mcp/mcp_toolset_prompts_test.go @@ -363,67 +363,6 @@ func (s *McpToolsetPromptsSuite) TestPromptsNotExposedWhenToolsetDisabled() { }) } -func (s *McpToolsetPromptsSuite) TestDisableToolsetPromptsConfig() { - testToolset := &mockToolsetWithPrompts{ - name: "test-toolset", - description: "Test toolset", - prompts: []api.ServerPrompt{ - { - Prompt: api.Prompt{ - Name: "toolset-prompt", - Description: "Should be disabled", - }, - Handler: func(params api.PromptHandlerParams) (*api.PromptCallResult, error) { - return api.NewPromptCallResult("Toolset", []api.PromptMessage{}, nil), nil - }, - }, - }, - } - - toolsets.Clear() - toolsets.Register(testToolset) - - // Add config prompt - cfg, err := config.ReadToml([]byte(` -toolsets = ["test-toolset"] -disable_toolset_prompts = true - -[[prompts]] -name = "config-prompt" -description = "Config prompt only" - -[[prompts.messages]] -role = "user" -content = "From config" - `)) - s.Require().NoError(err) - // Preserve kubeconfig from SetupTest - cfg.KubeConfig = s.Cfg.KubeConfig - s.Cfg = cfg - - s.InitMcpClient() - - prompts, err := s.ListPrompts(s.T().Context(), mcp.ListPromptsRequest{}) - - s.Run("ListPrompts returns prompts", func() { - s.NoError(err) - s.Require().NotNil(prompts) - }) - - s.Run("only config prompts are available", func() { - s.Require().NotNil(prompts) - s.Require().Len(prompts.Prompts, 1) - s.Equal("config-prompt", prompts.Prompts[0].Name) - }) - - s.Run("toolset prompts are not available when disabled", func() { - s.Require().NotNil(prompts) - for _, prompt := range prompts.Prompts { - s.NotEqual("toolset-prompt", prompt.Name) - } - }) -} - // Mock toolset for testing type mockToolsetWithPrompts struct { name string @@ -443,7 +382,7 @@ func (m *mockToolsetWithPrompts) GetTools(_ api.Openshift) []api.ServerTool { return nil } -func (m *mockToolsetWithPrompts) GetPrompts(_ api.Openshift) []api.ServerPrompt { +func (m *mockToolsetWithPrompts) GetPrompts() []api.ServerPrompt { return m.prompts } diff --git a/pkg/toolsets/config/toolset.go b/pkg/toolsets/config/toolset.go index 2dca94149..3d08fb597 100644 --- a/pkg/toolsets/config/toolset.go +++ b/pkg/toolsets/config/toolset.go @@ -25,7 +25,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } -func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { +func (t *Toolset) GetPrompts() []api.ServerPrompt { // Config toolset does not provide prompts return nil } diff --git a/pkg/toolsets/core/toolset.go b/pkg/toolsets/core/toolset.go index f8b428f77..c371f1616 100644 --- a/pkg/toolsets/core/toolset.go +++ b/pkg/toolsets/core/toolset.go @@ -29,7 +29,7 @@ func (t *Toolset) GetTools(o api.Openshift) []api.ServerTool { ) } -func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { +func (t *Toolset) GetPrompts() []api.ServerPrompt { // Core toolset prompts will be added in Feature 3 return nil } diff --git a/pkg/toolsets/helm/toolset.go b/pkg/toolsets/helm/toolset.go index a0e2e9119..6bdbfd419 100644 --- a/pkg/toolsets/helm/toolset.go +++ b/pkg/toolsets/helm/toolset.go @@ -25,7 +25,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } -func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { +func (t *Toolset) GetPrompts() []api.ServerPrompt { // Helm toolset does not provide prompts return nil } diff --git a/pkg/toolsets/kiali/toolset.go b/pkg/toolsets/kiali/toolset.go index f42385146..6c36800bf 100644 --- a/pkg/toolsets/kiali/toolset.go +++ b/pkg/toolsets/kiali/toolset.go @@ -32,7 +32,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } -func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { +func (t *Toolset) GetPrompts() []api.ServerPrompt { // Kiali toolset does not provide prompts return nil } diff --git a/pkg/toolsets/kubevirt/toolset.go b/pkg/toolsets/kubevirt/toolset.go index 8aa8aa9c1..9c87ddafd 100644 --- a/pkg/toolsets/kubevirt/toolset.go +++ b/pkg/toolsets/kubevirt/toolset.go @@ -28,7 +28,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { ) } -func (t *Toolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { +func (t *Toolset) GetPrompts() []api.ServerPrompt { // KubeVirt toolset does not provide prompts return nil } diff --git a/pkg/toolsets/toolsets_test.go b/pkg/toolsets/toolsets_test.go index 70185eee9..c2e869814 100644 --- a/pkg/toolsets/toolsets_test.go +++ b/pkg/toolsets/toolsets_test.go @@ -34,7 +34,7 @@ func (t *TestToolset) GetDescription() string { return t.description } func (t *TestToolset) GetTools(_ api.Openshift) []api.ServerTool { return nil } -func (t *TestToolset) GetPrompts(_ api.Openshift) []api.ServerPrompt { return nil } +func (t *TestToolset) GetPrompts() []api.ServerPrompt { return nil } var _ api.Toolset = (*TestToolset)(nil)