From 3b8405b5c0336233bb7161aa00b68f3cb008dcd2 Mon Sep 17 00:00:00 2001 From: "elvandlie@gmail.com" Date: Tue, 12 May 2026 20:02:14 +0700 Subject: [PATCH 1/3] mcp: add readonly guard to config add/remove handlers The deploy and delete handlers check s.readonly and refuse to act in readonly mode. However, the six config mutation handlers (envs add/remove, labels add/remove, volumes add/remove) execute unconditionally, allowing an AI agent to modify func.yaml even in readonly mode. Add the same readonly guard to all six config mutation handlers. Signed-off-by: elvandlie@gmail.com --- pkg/mcp/tools_config_envs.go | 8 ++++++++ pkg/mcp/tools_config_labels.go | 8 ++++++++ pkg/mcp/tools_config_volumes.go | 8 ++++++++ 3 files changed, 24 insertions(+) diff --git a/pkg/mcp/tools_config_envs.go b/pkg/mcp/tools_config_envs.go index 397b8c1eef..fbba4702b4 100644 --- a/pkg/mcp/tools_config_envs.go +++ b/pkg/mcp/tools_config_envs.go @@ -142,6 +142,10 @@ type ConfigEnvsAddOutput struct { } func (s *Server) configEnvsAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigEnvsAddInput) (result *mcp.CallToolResult, output ConfigEnvsAddOutput, err error) { + if s.readonly { + err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + return + } if err = input.validate(); err != nil { return } @@ -186,6 +190,10 @@ type ConfigEnvsRemoveOutput struct { } func (s *Server) configEnvsRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigEnvsRemoveInput) (result *mcp.CallToolResult, output ConfigEnvsRemoveOutput, err error) { + if s.readonly { + err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + return + } out, err := s.executor.Execute(ctx, "config", input.Args()...) if err != nil { err = fmt.Errorf("%w\n%s", err, string(out)) diff --git a/pkg/mcp/tools_config_labels.go b/pkg/mcp/tools_config_labels.go index 0c62e87290..8a03e26f0d 100644 --- a/pkg/mcp/tools_config_labels.go +++ b/pkg/mcp/tools_config_labels.go @@ -79,6 +79,10 @@ type ConfigLabelsAddOutput struct { } func (s *Server) configLabelsAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigLabelsAddInput) (result *mcp.CallToolResult, output ConfigLabelsAddOutput, err error) { + if s.readonly { + err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + return + } out, err := s.executor.Execute(ctx, "config", input.Args()...) if err != nil { err = fmt.Errorf("%w\n%s", err, string(out)) @@ -120,6 +124,10 @@ type ConfigLabelsRemoveOutput struct { } func (s *Server) configLabelsRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigLabelsRemoveInput) (result *mcp.CallToolResult, output ConfigLabelsRemoveOutput, err error) { + if s.readonly { + err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + return + } out, err := s.executor.Execute(ctx, "config", input.Args()...) if err != nil { err = fmt.Errorf("%w\n%s", err, string(out)) diff --git a/pkg/mcp/tools_config_volumes.go b/pkg/mcp/tools_config_volumes.go index 232d866d30..abeeed28d7 100644 --- a/pkg/mcp/tools_config_volumes.go +++ b/pkg/mcp/tools_config_volumes.go @@ -87,6 +87,10 @@ type ConfigVolumesAddOutput struct { } func (s *Server) configVolumesAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigVolumesAddInput) (result *mcp.CallToolResult, output ConfigVolumesAddOutput, err error) { + if s.readonly { + err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + return + } out, err := s.executor.Execute(ctx, "config", input.Args()...) if err != nil { err = fmt.Errorf("%w\n%s", err, string(out)) @@ -128,6 +132,10 @@ type ConfigVolumesRemoveOutput struct { } func (s *Server) configVolumesRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigVolumesRemoveInput) (result *mcp.CallToolResult, output ConfigVolumesRemoveOutput, err error) { + if s.readonly { + err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + return + } out, err := s.executor.Execute(ctx, "config", input.Args()...) if err != nil { err = fmt.Errorf("%w\n%s", err, string(out)) From 48179e44aefcf59f5fdca091dd1e54497ed6c1e6 Mon Sep 17 00:00:00 2001 From: "elvandlie@gmail.com" Date: Tue, 12 May 2026 21:53:21 +0700 Subject: [PATCH 2/3] mcp: add readonly guard tests for config mutation handlers Signed-off-by: elvandlie@gmail.com --- pkg/mcp/tools_config_envs.go | 4 +-- pkg/mcp/tools_config_envs_test.go | 50 ++++++++++++++++++++++++++++ pkg/mcp/tools_config_labels.go | 4 +-- pkg/mcp/tools_config_labels_test.go | 50 ++++++++++++++++++++++++++++ pkg/mcp/tools_config_volumes.go | 4 +-- pkg/mcp/tools_config_volumes_test.go | 50 ++++++++++++++++++++++++++++ 6 files changed, 156 insertions(+), 6 deletions(-) diff --git a/pkg/mcp/tools_config_envs.go b/pkg/mcp/tools_config_envs.go index fbba4702b4..fe36d2b04b 100644 --- a/pkg/mcp/tools_config_envs.go +++ b/pkg/mcp/tools_config_envs.go @@ -142,7 +142,7 @@ type ConfigEnvsAddOutput struct { } func (s *Server) configEnvsAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigEnvsAddInput) (result *mcp.CallToolResult, output ConfigEnvsAddOutput, err error) { - if s.readonly { + if s.readonly.Load() { err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") return } @@ -190,7 +190,7 @@ type ConfigEnvsRemoveOutput struct { } func (s *Server) configEnvsRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigEnvsRemoveInput) (result *mcp.CallToolResult, output ConfigEnvsRemoveOutput, err error) { - if s.readonly { + if s.readonly.Load() { err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") return } diff --git a/pkg/mcp/tools_config_envs_test.go b/pkg/mcp/tools_config_envs_test.go index 327ab06f9a..218e241a91 100644 --- a/pkg/mcp/tools_config_envs_test.go +++ b/pkg/mcp/tools_config_envs_test.go @@ -627,3 +627,53 @@ func TestTool_ConfigEnvsRemove_Error(t *testing.T) { t.Fatal("expected error result, got success") } } + +// TestTool_ConfigEnvsAdd_Readonly ensures the config_envs_add tool is blocked in readonly mode. +func TestTool_ConfigEnvsAdd_Readonly(t *testing.T) { + executor := mock.NewExecutor() + + client, server, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + server.readonly.Store(true) + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "config_envs_add", + Arguments: map[string]any{"path": ".", "name": "KEY", "value": "VAL"}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected error result in readonly mode, got success") + } + if executor.ExecuteInvoked { + t.Fatal("executor should not be invoked in readonly mode") + } +} + +// TestTool_ConfigEnvsRemove_Readonly ensures the config_envs_remove tool is blocked in readonly mode. +func TestTool_ConfigEnvsRemove_Readonly(t *testing.T) { + executor := mock.NewExecutor() + + client, server, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + server.readonly.Store(true) + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "config_envs_remove", + Arguments: map[string]any{"path": ".", "name": "KEY"}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected error result in readonly mode, got success") + } + if executor.ExecuteInvoked { + t.Fatal("executor should not be invoked in readonly mode") + } +} diff --git a/pkg/mcp/tools_config_labels.go b/pkg/mcp/tools_config_labels.go index 8a03e26f0d..034c604aec 100644 --- a/pkg/mcp/tools_config_labels.go +++ b/pkg/mcp/tools_config_labels.go @@ -79,7 +79,7 @@ type ConfigLabelsAddOutput struct { } func (s *Server) configLabelsAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigLabelsAddInput) (result *mcp.CallToolResult, output ConfigLabelsAddOutput, err error) { - if s.readonly { + if s.readonly.Load() { err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") return } @@ -124,7 +124,7 @@ type ConfigLabelsRemoveOutput struct { } func (s *Server) configLabelsRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigLabelsRemoveInput) (result *mcp.CallToolResult, output ConfigLabelsRemoveOutput, err error) { - if s.readonly { + if s.readonly.Load() { err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") return } diff --git a/pkg/mcp/tools_config_labels_test.go b/pkg/mcp/tools_config_labels_test.go index e3d2a42c83..831dc89f97 100644 --- a/pkg/mcp/tools_config_labels_test.go +++ b/pkg/mcp/tools_config_labels_test.go @@ -242,3 +242,53 @@ func TestTool_ConfigLabelsRemove_Error(t *testing.T) { t.Fatal("expected error result, got success") } } + +// TestTool_ConfigLabelsAdd_Readonly ensures the config_labels_add tool is blocked in readonly mode. +func TestTool_ConfigLabelsAdd_Readonly(t *testing.T) { + executor := mock.NewExecutor() + + client, server, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + server.readonly.Store(true) + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "config_labels_add", + Arguments: map[string]any{"path": ".", "name": "app", "value": "test"}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected error result in readonly mode, got success") + } + if executor.ExecuteInvoked { + t.Fatal("executor should not be invoked in readonly mode") + } +} + +// TestTool_ConfigLabelsRemove_Readonly ensures the config_labels_remove tool is blocked in readonly mode. +func TestTool_ConfigLabelsRemove_Readonly(t *testing.T) { + executor := mock.NewExecutor() + + client, server, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + server.readonly.Store(true) + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "config_labels_remove", + Arguments: map[string]any{"path": ".", "name": "app"}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected error result in readonly mode, got success") + } + if executor.ExecuteInvoked { + t.Fatal("executor should not be invoked in readonly mode") + } +} diff --git a/pkg/mcp/tools_config_volumes.go b/pkg/mcp/tools_config_volumes.go index abeeed28d7..2ff4ee734a 100644 --- a/pkg/mcp/tools_config_volumes.go +++ b/pkg/mcp/tools_config_volumes.go @@ -87,7 +87,7 @@ type ConfigVolumesAddOutput struct { } func (s *Server) configVolumesAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigVolumesAddInput) (result *mcp.CallToolResult, output ConfigVolumesAddOutput, err error) { - if s.readonly { + if s.readonly.Load() { err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") return } @@ -132,7 +132,7 @@ type ConfigVolumesRemoveOutput struct { } func (s *Server) configVolumesRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigVolumesRemoveInput) (result *mcp.CallToolResult, output ConfigVolumesRemoveOutput, err error) { - if s.readonly { + if s.readonly.Load() { err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") return } diff --git a/pkg/mcp/tools_config_volumes_test.go b/pkg/mcp/tools_config_volumes_test.go index bc10965554..8562f11b78 100644 --- a/pkg/mcp/tools_config_volumes_test.go +++ b/pkg/mcp/tools_config_volumes_test.go @@ -246,3 +246,53 @@ func TestTool_ConfigVolumesRemove_Error(t *testing.T) { t.Fatal("expected error result, got success") } } + +// TestTool_ConfigVolumesAdd_Readonly ensures the config_volumes_add tool is blocked in readonly mode. +func TestTool_ConfigVolumesAdd_Readonly(t *testing.T) { + executor := mock.NewExecutor() + + client, server, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + server.readonly.Store(true) + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "config_volumes_add", + Arguments: map[string]any{"path": "."}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected error result in readonly mode, got success") + } + if executor.ExecuteInvoked { + t.Fatal("executor should not be invoked in readonly mode") + } +} + +// TestTool_ConfigVolumesRemove_Readonly ensures the config_volumes_remove tool is blocked in readonly mode. +func TestTool_ConfigVolumesRemove_Readonly(t *testing.T) { + executor := mock.NewExecutor() + + client, server, err := newTestPair(t, WithExecutor(executor)) + if err != nil { + t.Fatal(err) + } + server.readonly.Store(true) + + result, err := client.CallTool(t.Context(), &mcp.CallToolParams{ + Name: "config_volumes_remove", + Arguments: map[string]any{"path": "."}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatal("expected error result in readonly mode, got success") + } + if executor.ExecuteInvoked { + t.Fatal("executor should not be invoked in readonly mode") + } +} From 6aa9ebefea7a50d8c71b928ecd005a42ceeaaee6 Mon Sep 17 00:00:00 2001 From: Tempris Admin Date: Fri, 26 Jun 2026 16:32:06 +0700 Subject: [PATCH 3/3] align readonly MCP mutation errors --- pkg/mcp/instructions_warning.md | 9 +++++---- pkg/mcp/mcp.go | 2 +- pkg/mcp/tools_config_envs.go | 4 ++-- pkg/mcp/tools_config_envs_test.go | 9 +++++++++ pkg/mcp/tools_config_labels.go | 4 ++-- pkg/mcp/tools_config_labels_test.go | 9 +++++++++ pkg/mcp/tools_config_volumes.go | 4 ++-- pkg/mcp/tools_config_volumes_test.go | 9 +++++++++ 8 files changed, 39 insertions(+), 11 deletions(-) diff --git a/pkg/mcp/instructions_warning.md b/pkg/mcp/instructions_warning.md index 82332e7177..58e491adf0 100644 --- a/pkg/mcp/instructions_warning.md +++ b/pkg/mcp/instructions_warning.md @@ -8,21 +8,22 @@ The Functions MCP server is currently running in **read-only mode**. **Available operations:** - Create Functions -- Build Functions -- Configure Functions (envs, labels, volumes) +- Build Functions (without pushing the image) - Inspect Functions +- List Function configuration - Run/stop Functions locally (not cluster operations, so unaffected by read-only mode) **Disabled operations:** +- Configure Functions (envs, labels, volumes add/remove) - Deploy to cluster - Delete from cluster - Push a built image to a registry (`build` with `push` enabled) -These write operations are disabled to prevent unintended cluster modifications. +These write operations are disabled to prevent unintended local or cluster modifications. ## Enabling Write Mode -If the user needs to deploy or delete Functions, or push a built image to a registry, you MUST inform them to enable write mode: +If the user needs to deploy, delete, or configure Functions, or push a built image to a registry, you MUST inform them to enable write mode: 1. Close/exit this application completely 2. Set the environment variable: `FUNC_ENABLE_MCP_WRITE=true` diff --git a/pkg/mcp/mcp.go b/pkg/mcp/mcp.go index 423c066b3e..52bd89cd08 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -30,7 +30,7 @@ const ( type Server struct { OnInit func(context.Context) // Invoked when the server is initialized prefix string // Command prefix ("func" or "kn func") - readonly atomic.Bool // disables deploy, delete, and pushing built images when true + readonly atomic.Bool // disables mutation tools when true executor executor transport mcp.Transport // Transport to use (defaults to StdioTransport) impl *mcp.Server // implements the protocol diff --git a/pkg/mcp/tools_config_envs.go b/pkg/mcp/tools_config_envs.go index fe36d2b04b..1327f40317 100644 --- a/pkg/mcp/tools_config_envs.go +++ b/pkg/mcp/tools_config_envs.go @@ -143,7 +143,7 @@ type ConfigEnvsAddOutput struct { func (s *Server) configEnvsAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigEnvsAddInput) (result *mcp.CallToolResult, output ConfigEnvsAddOutput, err error) { if s.readonly.Load() { - err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) return } if err = input.validate(); err != nil { @@ -191,7 +191,7 @@ type ConfigEnvsRemoveOutput struct { func (s *Server) configEnvsRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigEnvsRemoveInput) (result *mcp.CallToolResult, output ConfigEnvsRemoveOutput, err error) { if s.readonly.Load() { - err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) return } out, err := s.executor.Execute(ctx, "config", input.Args()...) diff --git a/pkg/mcp/tools_config_envs_test.go b/pkg/mcp/tools_config_envs_test.go index 218e241a91..aa725cddfc 100644 --- a/pkg/mcp/tools_config_envs_test.go +++ b/pkg/mcp/tools_config_envs_test.go @@ -3,6 +3,7 @@ package mcp import ( "context" "errors" + "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -648,6 +649,10 @@ func TestTool_ConfigEnvsAdd_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + got := resultToString(result) + if !strings.Contains(got, "readonly mode") { + t.Fatalf("expected readonly-mode error, got: %q", got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } @@ -673,6 +678,10 @@ func TestTool_ConfigEnvsRemove_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + got := resultToString(result) + if !strings.Contains(got, "readonly mode") { + t.Fatalf("expected readonly-mode error, got: %q", got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } diff --git a/pkg/mcp/tools_config_labels.go b/pkg/mcp/tools_config_labels.go index 034c604aec..2fa3b23b27 100644 --- a/pkg/mcp/tools_config_labels.go +++ b/pkg/mcp/tools_config_labels.go @@ -80,7 +80,7 @@ type ConfigLabelsAddOutput struct { func (s *Server) configLabelsAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigLabelsAddInput) (result *mcp.CallToolResult, output ConfigLabelsAddOutput, err error) { if s.readonly.Load() { - err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) return } out, err := s.executor.Execute(ctx, "config", input.Args()...) @@ -125,7 +125,7 @@ type ConfigLabelsRemoveOutput struct { func (s *Server) configLabelsRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigLabelsRemoveInput) (result *mcp.CallToolResult, output ConfigLabelsRemoveOutput, err error) { if s.readonly.Load() { - err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) return } out, err := s.executor.Execute(ctx, "config", input.Args()...) diff --git a/pkg/mcp/tools_config_labels_test.go b/pkg/mcp/tools_config_labels_test.go index 831dc89f97..47e0ae9eec 100644 --- a/pkg/mcp/tools_config_labels_test.go +++ b/pkg/mcp/tools_config_labels_test.go @@ -3,6 +3,7 @@ package mcp import ( "context" "errors" + "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -263,6 +264,10 @@ func TestTool_ConfigLabelsAdd_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + got := resultToString(result) + if !strings.Contains(got, "readonly mode") { + t.Fatalf("expected readonly-mode error, got: %q", got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } @@ -288,6 +293,10 @@ func TestTool_ConfigLabelsRemove_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + got := resultToString(result) + if !strings.Contains(got, "readonly mode") { + t.Fatalf("expected readonly-mode error, got: %q", got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } diff --git a/pkg/mcp/tools_config_volumes.go b/pkg/mcp/tools_config_volumes.go index 2ff4ee734a..da36e27a39 100644 --- a/pkg/mcp/tools_config_volumes.go +++ b/pkg/mcp/tools_config_volumes.go @@ -88,7 +88,7 @@ type ConfigVolumesAddOutput struct { func (s *Server) configVolumesAddHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigVolumesAddInput) (result *mcp.CallToolResult, output ConfigVolumesAddOutput, err error) { if s.readonly.Load() { - err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) return } out, err := s.executor.Execute(ctx, "config", input.Args()...) @@ -133,7 +133,7 @@ type ConfigVolumesRemoveOutput struct { func (s *Server) configVolumesRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigVolumesRemoveInput) (result *mcp.CallToolResult, output ConfigVolumesRemoveOutput, err error) { if s.readonly.Load() { - err = fmt.Errorf("the server is currently in readonly mode. Please set FUNC_ENABLE_MCP_WRITE and restart the client") + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) return } out, err := s.executor.Execute(ctx, "config", input.Args()...) diff --git a/pkg/mcp/tools_config_volumes_test.go b/pkg/mcp/tools_config_volumes_test.go index 8562f11b78..a2e7bdde33 100644 --- a/pkg/mcp/tools_config_volumes_test.go +++ b/pkg/mcp/tools_config_volumes_test.go @@ -3,6 +3,7 @@ package mcp import ( "context" "errors" + "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -267,6 +268,10 @@ func TestTool_ConfigVolumesAdd_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + got := resultToString(result) + if !strings.Contains(got, "readonly mode") { + t.Fatalf("expected readonly-mode error, got: %q", got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } @@ -292,6 +297,10 @@ func TestTool_ConfigVolumesRemove_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + got := resultToString(result) + if !strings.Contains(got, "readonly mode") { + t.Fatalf("expected readonly-mode error, got: %q", got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") }