From 779bb992f49241b0277d83bfc86870a7ef15b54d Mon Sep 17 00:00:00 2001 From: Elvand Lie Nababan Date: Wed, 28 Oct 2026 19:48:33 +0700 Subject: [PATCH] fix(mcp): block config_git_set and config_git_remove in read-only mode Git configuration tools bypassed the MCP readonly policy and could modify Function settings, pipeline resources and remote webhooks. Reject both tools before validation or command execution in readonly mode. Assisted-by: Codex (gpt-6.1-sol) Signed-off-by: Elvand Lie Nababan --- pkg/mcp/instructions_warning.md | 1 + pkg/mcp/tools_config_git.go | 8 +++++ pkg/mcp/tools_config_git_test.go | 61 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/pkg/mcp/instructions_warning.md b/pkg/mcp/instructions_warning.md index 58e491adf0..ea4e89a24d 100644 --- a/pkg/mcp/instructions_warning.md +++ b/pkg/mcp/instructions_warning.md @@ -15,6 +15,7 @@ The Functions MCP server is currently running in **read-only mode**. **Disabled operations:** - Configure Functions (envs, labels, volumes add/remove) +- Configure Git source settings and pipeline resources (config_git_set, config_git_remove) - Deploy to cluster - Delete from cluster - Push a built image to a registry (`build` with `push` enabled) diff --git a/pkg/mcp/tools_config_git.go b/pkg/mcp/tools_config_git.go index 22783e5257..f6ee05182b 100644 --- a/pkg/mcp/tools_config_git.go +++ b/pkg/mcp/tools_config_git.go @@ -84,6 +84,10 @@ type ConfigGitSetOutput struct { } func (s *Server) configGitSetHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigGitSetInput) (result *mcp.CallToolResult, output ConfigGitSetOutput, err error) { + if s.readonly.Load() { + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) + return + } if input.Path == "" { err = fmt.Errorf("'path' is required") return @@ -153,6 +157,10 @@ type ConfigGitRemoveOutput struct { } func (s *Server) configGitRemoveHandler(ctx context.Context, r *mcp.CallToolRequest, input ConfigGitRemoveInput) (result *mcp.CallToolResult, output ConfigGitRemoveOutput, err error) { + if s.readonly.Load() { + err = fmt.Errorf("the server is currently in readonly mode. Please set %s and restart the MCP server", EnvMCPWrite) + return + } if input.Path == "" { err = fmt.Errorf("'path' is required") return diff --git a/pkg/mcp/tools_config_git_test.go b/pkg/mcp/tools_config_git_test.go index cfd2a07faf..af36b172d2 100644 --- a/pkg/mcp/tools_config_git_test.go +++ b/pkg/mcp/tools_config_git_test.go @@ -3,6 +3,7 @@ package mcp import ( "context" "errors" + "strings" "testing" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -348,3 +349,63 @@ func TestTool_ConfigGitRemove_Error(t *testing.T) { t.Fatal("expected error result, got success") } } + +func TestTool_ConfigGitSet_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_git_set", + Arguments: map[string]any{ + "path": ".", + "git_url": "https://github.com/user/repo", + "git_branch": "main", + }, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatalf("expected error result in readonly mode, got success (executor invoked: %t)", executor.ExecuteInvoked) + } + 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") + } +} + +func TestTool_ConfigGitRemove_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_git_remove", + Arguments: map[string]any{"path": ".", "delete_cluster": true}, + }) + if err != nil { + t.Fatal(err) + } + if !result.IsError { + t.Fatalf("expected error result in readonly mode, got success (executor invoked: %t)", executor.ExecuteInvoked) + } + 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") + } +}