Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ DESCRIPTION
func runMCPStart(cmd *cobra.Command, args []string, newClient ClientFactory) error {
// Configure write mode
writeEnabled := false
if val := os.Getenv("FUNC_ENABLE_MCP_WRITE"); val != "" {
if val := os.Getenv(mcp.EnvMCPWrite); val != "" {
parsed, err := strconv.ParseBool(val)
if err != nil {
return fmt.Errorf("FUNC_ENABLE_MCP_WRITE should be a boolean (true/false, 1/0, etc). Received %q", val)
return fmt.Errorf("%s should be a boolean (true/false, 1/0, etc). Received %q", mcp.EnvMCPWrite, val)
}
writeEnabled = parsed
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/mcp/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ const (
name = "func"
title = "func"
version = "0.1.0"

// EnvMCPWrite is the environment variable that enables write operations
// (build, deploy, delete) on the MCP server. Set to "true" to allow
// mutations; the server runs in read-only mode by default.
EnvMCPWrite = "FUNC_ENABLE_MCP_WRITE"
)

// NOTE: Invoking prompts in some interfaces (such as Claude Code) when all
Expand Down
5 changes: 2 additions & 3 deletions pkg/mcp/tools_build.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ 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")
if s.readonly.Load() && input.Push != nil && *input.Push {
err = fmt.Errorf("pushing images is not allowed in read-only mode; set %s=true to enable write operations", EnvMCPWrite)
return
}

out, err := s.executor.Execute(ctx, "build", input.Args()...)
if err != nil {
err = fmt.Errorf("%w\n%s", err, string(out))
Expand Down
49 changes: 49 additions & 0 deletions pkg/mcp/tools_build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,55 @@ import (
"knative.dev/func/pkg/mcp/mock"
)

// TestTool_Build_ReadonlyPushRejected ensures build with push=true is rejected in readonly mode.
func TestTool_Build_ReadonlyPushRejected(t *testing.T) {
client, _, err := newTestPairWithReadonly(t, true) // readonly = true
if err != nil {
t.Fatal(err)
}

result, err := client.CallTool(t.Context(), &mcp.CallToolParams{
Name: "build",
Arguments: map[string]any{"path": ".", "push": true},
})
if err != nil {
t.Fatal(err)
}
if !result.IsError {
t.Fatal("expected build --push to be rejected in readonly mode")
}
}

// TestTool_Build_ReadonlyWithoutPushAllowed ensures build without push is allowed in readonly mode.
func TestTool_Build_ReadonlyWithoutPushAllowed(t *testing.T) {
executor := mock.NewExecutor()
executor.ExecuteFn = func(_ context.Context, subcommand string, _ ...string) ([]byte, error) {
if subcommand != "build" {
t.Fatalf("expected subcommand 'build', got %q", subcommand)
}
return []byte("OK\n"), nil
}

client, _, err := newTestPair(t, WithReadonly(true), WithExecutor(executor))
if err != nil {
t.Fatal(err)
}

result, err := client.CallTool(t.Context(), &mcp.CallToolParams{
Name: "build",
Arguments: map[string]any{"path": "."},
})
if err != nil {
t.Fatal(err)
}
if result.IsError {
t.Fatalf("expected build without push to succeed in readonly mode, got error: %v", result)
}
if !executor.ExecuteInvoked {
t.Fatal("executor was not invoked")
}
}

// TestTool_Build_Args ensures the build tool executes with all arguments passed correctly.
func TestTool_Build_Args(t *testing.T) {
// Test data - defined once and used for both input and validation
Expand Down
2 changes: 1 addition & 1 deletion pkg/mcp/tools_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = fmt.Errorf("delete is not allowed in read-only mode; set %s=true to enable write operations", EnvMCPWrite)
return
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/mcp/tools_deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = fmt.Errorf("deploy is not allowed in read-only mode; set %s=true to enable write operations", EnvMCPWrite)
return
}
out, err := s.executor.Execute(ctx, "deploy", input.Args()...)
Expand Down
Loading