Skip to content

fix(mcp): stop advertising code_execution while enable_code_execution is off - #1261

Merged
Dumbris merged 2 commits into
mainfrom
fix/1236-hide-disabled-code-execution
Sep 12, 2026
Merged

fix(mcp): stop advertising code_execution while enable_code_execution is off#1261
Dumbris merged 2 commits into
mainfrom
fix/1236-hide-disabled-code-execution

Conversation

@Dumbris

@Dumbris Dumbris commented Sep 12, 2026

Copy link
Copy Markdown
Member

Problem

With enable_code_execution: false (the default), tools/list on /mcp still advertised code_execution. Only the call handler refused it. Clients select tools from the list, not from descriptions (which harnesses routinely truncate), so agents kept calling it and burning a round trip per session before falling back to another path.

Closes #1236

Root cause

buildCodeExecutionTool (internal/server/mcp_routing.go) returned a "Code Execution (Disabled)" stub — a registered tool whose handler always refused — whenever the flag was off. Both routing-mode surfaces (/mcp in the default retrieve_tools mode, /mcp/call, /mcp/code) appended it unconditionally. The stub dates from PR #339, which treated a descriptive refusal as better than an unknown-tool error; in practice a disabled capability that is still advertised is indistinguishable from an available one.

A second, smaller problem: RefreshCallToolModeTools ran SetTools on every config.reloaded, so every unrelated config edit pushed notifications/tools/list_changed to every connected session on the default /mcp surface.

Fix

  • buildCodeExecutionTool returns nothing while the flag is off. The default surface (registerTools in mcp.go) now registers through the same builder, so there is one definition of the tool.
  • RefreshCodeExecutionAvailability (driven by config.reloaded) applies a runtime flip on every surface that carries the tool:
    • routing-mode surfaces rebuild through content-guarded refreshers (RefreshCallToolModeTools gains the same fingerprint guard RefreshCodeExecModeTools already had; both guards are seeded at init so the first reload does not re-announce an unchanged set);
    • the default surface is updated in place — AddTools on enable, DeleteTools on disable — and only when its state actually differs from the flag (mcp-go's AddTools notifies even for an empty batch).
    • Net effect: a toggle emits exactly one notifications/tools/list_changed per surface; an unrelated reload emits none.
  • The handler-level gate in mcp_code_execution.go is untouched: REST / CLI / CallToolDirect callers still get the "enable it" message. An MCP tools/call from a stale list now gets mcp-go's unknown-tool error (documented).
  • Docs: docs/features/routing-modes.md, docs/features/code-execution.md, docs/configuration.md, docs/code_execution/troubleshooting.md no longer say the tool "appears but returns an error" or that a restart is needed.

Golden tests (deliberate update)

Three frozen tool-surface gates exist. The frozen baselines (testdata/toolslist_goldens/pre099/*.json, testdata/tools_list_prefeature.golden.json) were captured with the flag off and therefore contain the stub — they are untouched. The current goldens toolslist_goldens/retrieve_tools_mode.json and code_execution_mode.json were regenerated via MCPPROXY_WRITE_TOOLSLIST_GOLDENS; the only delta is the removed code_execution entry (verified with jq diff). default_server.json is unchanged (that surface never carried the stub). The two enumerated-delta gates (toolsListAllowedRemovals in toolslist_snapshot_test.go, wantRemoved in mcp_menu_surface_test.go) now allow exactly that one removal per surface and still fail on any other addition, removal, or schema change. assertCodeExecutionDelta was deleted because it only ever compared the disabled stub; the live tool's script param / non-required code remain pinned by TestCodeExecutionRegistrations_ScriptParam on all three surfaces.

Testing

Failing tests were written first (internal/server/code_execution_hotreload_test.go, rewritten from the F16 stub tests):

  • TestBuildCodeExecutionTool_FollowsCurrentConfig — disabled → no tool; enabled → live tool; back off → none.
  • TestRoutingModeSurfaces_OmitDisabledCodeExecution — neither routing-mode builder lists it while disabled.
  • TestRefreshCodeExecutionAvailability_TogglesAdvertisedTool — hot enable adds it on all three surfaces, hot disable withdraws it.
  • TestRefreshCodeExecutionAvailability_EmitsListChangedOnlyOnAFlip — registers an initialized session per surface and asserts exactly one list_changed per flip and zero on unchanged reloads (this one also caught the pre-existing per-reload churn).
  • TestRefreshCodeExecutionAvailability_PreservesOtherTools — the in-place add/remove touches nothing else.
  • Updated: TestDisabledCodeExecutionNotRegistered, TestCodeExecutionDisabled_NotRegisteredButScriptCallStillExplained, the "wording matches" subtest of TestCodeExecution_DisabledGateCoversEveryDispatch.

Verification run locally:

  • go build ./... and go build -tags server ./cmd/mcpproxy
  • go test -race -count=1 ./internal/server/ (with the CI -skip regex) — ok
  • go test -race -count=1 ./internal/httpapi/ ./internal/runtime/ ./internal/config/ — ok
  • golangci-lint run --config .github/.golangci.yml ./internal/server/... — 0 issues
  • Cross-review with opencode (github-copilot/gpt-6-astra) in three file-scoped chunks: production code CLEAN, test/golden gates CLEAN, docs FINDINGS (1 — the "cached list still gets the enable hint" claim was wrong; fixed in the second commit), re-review CLEAN.

Not run here: ./scripts/test-api-e2e.sh (CI).

🤖 Generated with Claude Code

Dumbris and others added 2 commits September 12, 2026 08:07
… is off

A disabled code_execution was still listed in tools/list on every surface
that carries it (/mcp in the default retrieve_tools routing mode, /mcp/call,
/mcp/code) as a "Code Execution (Disabled)" stub whose handler refused every
call. Clients pick tools from the list, not from descriptions, so agents kept
calling it and burning a round trip per session before falling back (#1236).

- buildCodeExecutionTool returns nothing when the flag is off; the default
  surface now registers through the same builder (one definition).
- RefreshCodeExecutionAvailability adds/removes the tool in place on the
  default surface (AddTools / DeleteTools) and rebuilds the routing-mode
  surfaces through content-guarded refreshers, so a runtime flip emits
  notifications/tools/list_changed exactly once per surface and an
  unrelated config reload emits nothing. The call-tool surface gains the
  same fingerprint guard the code-exec surface already had; both guards are
  seeded at init so the first reload does not re-announce an unchanged set.
- The handler-level gate still refuses a call that arrives by name (REST,
  CallToolDirect, a cached listing) with the same wording.

Goldens: the frozen retrieve_tools_mode / code_execution_mode goldens were
captured with the flag off and carried the stub; they are regenerated with
that single entry removed, and the enumerated-delta gates (toolslist
snapshot + menu surface) now allow exactly that one removal per surface.
The pre099 / pre-feature baselines are untouched.

Closes #1236

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…enable hint

Review round 1 (astra): an MCP tools/call for the withdrawn name is refused
by mcp-go as an unknown tool; only REST/CLI callers reach the handler gate
and its "enable it" message. Also drop the restart step from the
"tool not found in tools list" troubleshooting entry — the flag is
hot-reloaded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 931646a
Status: ✅  Deploy successful!
Preview URL: https://acb18d05.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-1236-hide-disabled-code.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/1236-hide-disabled-code-execution

Available Artifacts

  • archive-darwin-amd64 (29 MB)
  • archive-darwin-arm64 (27 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (16 MB)
  • archive-windows-amd64 (29 MB)
  • archive-windows-arm64 (26 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (23 MB)
  • installer-dmg-darwin-arm64 (21 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 34675287129 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris
Dumbris merged commit fe3f4e3 into main Sep 12, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

code_execution tool is advertised in tools/list even when enable_code_execution is false

2 participants