From afe3599e4b4f39d214cd11874437d66e4314a8ce 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 9fb344786eacb7177767d226c1d97431e8e850df 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 8bee2738d47ec3ae34778f269dd6598a2be5c50f 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/errors.go | 5 +++++ pkg/mcp/instructions_warning.md | 9 +++++---- pkg/mcp/mcp.go | 2 +- pkg/mcp/tools_build.go | 2 +- pkg/mcp/tools_config_envs.go | 4 ++-- pkg/mcp/tools_config_envs_test.go | 6 ++++++ pkg/mcp/tools_config_labels.go | 4 ++-- pkg/mcp/tools_config_labels_test.go | 6 ++++++ pkg/mcp/tools_config_volumes.go | 4 ++-- pkg/mcp/tools_config_volumes_test.go | 6 ++++++ pkg/mcp/tools_delete.go | 2 +- pkg/mcp/tools_deploy.go | 2 +- 12 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 pkg/mcp/errors.go diff --git a/pkg/mcp/errors.go b/pkg/mcp/errors.go new file mode 100644 index 0000000000..7c25a77604 --- /dev/null +++ b/pkg/mcp/errors.go @@ -0,0 +1,5 @@ +package mcp + +import "errors" + +var errReadOnlyMode = errors.New("the server is currently in read-only mode; to enable write operations, set FUNC_ENABLE_MCP_WRITE in the server environment and restart the server") diff --git a/pkg/mcp/instructions_warning.md b/pkg/mcp/instructions_warning.md index 98697ddeec..3c98d68c64 100644 --- a/pkg/mcp/instructions_warning.md +++ b/pkg/mcp/instructions_warning.md @@ -8,19 +8,20 @@ The Functions MCP server is currently running in **read-only mode**. **Available operations:** - Create Functions -- Build Functions -- Configure Functions (envs, labels, volumes) - Inspect Functions +- List Function configuration **Disabled operations:** +- Build Function images +- Configure Functions (envs, labels, volumes add/remove) - Deploy to cluster - Delete from cluster -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, you MUST inform them to enable write mode: +If the user needs to mutate Functions, 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 02ea0f5d44..7f38ca88e6 100644 --- a/pkg/mcp/mcp.go +++ b/pkg/mcp/mcp.go @@ -24,7 +24,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 and delete 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_build.go b/pkg/mcp/tools_build.go index e1f6a6758f..4200b534e6 100644 --- a/pkg/mcp/tools_build.go +++ b/pkg/mcp/tools_build.go @@ -21,7 +21,7 @@ var buildTool = &mcp.Tool{ func (s *Server) buildHandler(ctx context.Context, r *mcp.CallToolRequest, input BuildInput) (result *mcp.CallToolResult, output BuildOutput, err error) { if s.readonly.Load() { - err = fmt.Errorf("the server is currently in read-only mode; to enable write operations, set FUNC_ENABLE_MCP_WRITE in the server environment and restart the server") + err = errReadOnlyMode return } diff --git a/pkg/mcp/tools_config_envs.go b/pkg/mcp/tools_config_envs.go index fe36d2b04b..fb96d3cdc5 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 = errReadOnlyMode 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 = errReadOnlyMode 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..21f3a955e8 100644 --- a/pkg/mcp/tools_config_envs_test.go +++ b/pkg/mcp/tools_config_envs_test.go @@ -648,6 +648,9 @@ func TestTool_ConfigEnvsAdd_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + if got := resultToString(result); got != errReadOnlyMode.Error() { + t.Errorf("expected readonly error message %q, got %q", errReadOnlyMode.Error(), got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } @@ -673,6 +676,9 @@ func TestTool_ConfigEnvsRemove_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + if got := resultToString(result); got != errReadOnlyMode.Error() { + t.Errorf("expected readonly error message %q, got %q", errReadOnlyMode.Error(), 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..9003aa9114 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 = errReadOnlyMode 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 = errReadOnlyMode 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..8c709488c1 100644 --- a/pkg/mcp/tools_config_labels_test.go +++ b/pkg/mcp/tools_config_labels_test.go @@ -263,6 +263,9 @@ func TestTool_ConfigLabelsAdd_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + if got := resultToString(result); got != errReadOnlyMode.Error() { + t.Errorf("expected readonly error message %q, got %q", errReadOnlyMode.Error(), got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } @@ -288,6 +291,9 @@ func TestTool_ConfigLabelsRemove_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + if got := resultToString(result); got != errReadOnlyMode.Error() { + t.Errorf("expected readonly error message %q, got %q", errReadOnlyMode.Error(), 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..b3475a3d4f 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 = errReadOnlyMode 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 = errReadOnlyMode 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..9623fdeacc 100644 --- a/pkg/mcp/tools_config_volumes_test.go +++ b/pkg/mcp/tools_config_volumes_test.go @@ -267,6 +267,9 @@ func TestTool_ConfigVolumesAdd_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + if got := resultToString(result); got != errReadOnlyMode.Error() { + t.Errorf("expected readonly error message %q, got %q", errReadOnlyMode.Error(), got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } @@ -292,6 +295,9 @@ func TestTool_ConfigVolumesRemove_Readonly(t *testing.T) { if !result.IsError { t.Fatal("expected error result in readonly mode, got success") } + if got := resultToString(result); got != errReadOnlyMode.Error() { + t.Errorf("expected readonly error message %q, got %q", errReadOnlyMode.Error(), got) + } if executor.ExecuteInvoked { t.Fatal("executor should not be invoked in readonly mode") } diff --git a/pkg/mcp/tools_delete.go b/pkg/mcp/tools_delete.go index 27760f864d..81d3ead88f 100644 --- a/pkg/mcp/tools_delete.go +++ b/pkg/mcp/tools_delete.go @@ -21,7 +21,7 @@ var deleteTool = &mcp.Tool{ func (s *Server) deleteHandler(ctx context.Context, r *mcp.CallToolRequest, input DeleteInput) (result *mcp.CallToolResult, output DeleteOutput, 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 = errReadOnlyMode return } diff --git a/pkg/mcp/tools_deploy.go b/pkg/mcp/tools_deploy.go index 09c7231656..fdb6a224b5 100644 --- a/pkg/mcp/tools_deploy.go +++ b/pkg/mcp/tools_deploy.go @@ -21,7 +21,7 @@ var deployTool = &mcp.Tool{ func (s *Server) deployHandler(ctx context.Context, r *mcp.CallToolRequest, input DeployInput) (result *mcp.CallToolResult, output DeployOutput, 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 = errReadOnlyMode return } out, err := s.executor.Execute(ctx, "deploy", input.Args()...)