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") + } +}