Problem
The disabledTools setting promises execution-time enforcement: "Tools in this list will be excluded from prompt generation and rejected at execution time" (packages/types/src/global-settings.ts:288). For MCP tools that promise holds only while the model calls through the use_mcp_tool wrapper. Each MCP tool can also be advertised as its own native function declaration (mcp--serverName--toolName dynamic tools); calls to those arrive as mcp_tool_use blocks, and that dispatch path never consults the tool-policy layer that rejects disabled tools. A user who disables MCP calling can still have MCP tools executed.
Observed on main at commit ba46d1f34 (2026-09-13, Zoo Code v3.82.1).
Evidence
- The
tool_use arm of the dispatcher validates before executing: it reads current settings (src/core/assistant-message/presentAssistantMessage.ts:346-347), converts disabledTools into a requirements map (presentAssistantMessage.ts:608) and calls validateToolUse (presentAssistantMessage.ts:618), where explicit disabling takes priority over every other allow rule (src/core/tools/validateToolUse.ts:133-146).
- The
mcp_tool_use arm (presentAssistantMessage.ts:130) consults none of that. It resolves the server name, synthesizes a use_mcp_tool block (presentAssistantMessage.ts:275-290) and calls the handler directly: await useMcpToolTool.handle(cline, syntheticToolUse, { (presentAssistantMessage.ts:292). No settings read, no requirements map, no validateToolUse call.
- Inside the handler, the only execution-time policy check is the mode's server allow-list (
ensureMcpServerAllowed at src/core/tools/UseMcpToolTool.ts:63); its own doc comment separates that check from "the shared validateToolUse path" (src/core/tools/UseMcpToolTool.ts:24).
- The model-side exclusion is declaration-layer input:
modelInfo.excludedTools is consumed by the prompt filter (src/core/prompts/tools/filter-tools-for-mode.ts:165-170) and never reaches the mcp_tool_use arm.
Consequence
With use_mcp_tool listed in disabledTools, or excluded through excludedTools, a connected MCP server's tools can still run: every call the model makes through a dynamic native declaration is executed without a policy check. The documented execution-time rejection applies only to the wrapper form.
Desired behavior
Executing a native mcp_tool_use block should consult the same policy the tool_use arm validates against, so a disabled or excluded use_mcp_tool also rejects dynamic MCP tool calls at execution time. This is a new property: on the observed commit only the wrapper path validates.
Proposed approach
At the point where the tool_use arm validates, route the synthesized call through the existing validation layer and return a structured error tool_result on rejection. Mirror that layer's policy resolution rather than bespoke MCP logic.
Acceptance criteria
- A
disabledTools entry naming use_mcp_tool makes a native mcp_tool_use call fail at dispatch, with a structured error tool_result; the server method is never invoked.
- An
excludedTools entry naming use_mcp_tool produces the same rejection.
- The decision comes from the shared validation layer, matching what the wrapper path returns for the same policy.
- With
use_mcp_tool enabled, native MCP calls behave as before.
- Tests: dynamic native call rejected under each disabling input, accepted when enabled.
Problem
The
disabledToolssetting promises execution-time enforcement: "Tools in this list will be excluded from prompt generation and rejected at execution time" (packages/types/src/global-settings.ts:288). For MCP tools that promise holds only while the model calls through theuse_mcp_toolwrapper. Each MCP tool can also be advertised as its own native function declaration (mcp--serverName--toolNamedynamic tools); calls to those arrive asmcp_tool_useblocks, and that dispatch path never consults the tool-policy layer that rejects disabled tools. A user who disables MCP calling can still have MCP tools executed.Observed on
mainat commitba46d1f34(2026-09-13, Zoo Code v3.82.1).Evidence
tool_usearm of the dispatcher validates before executing: it reads current settings (src/core/assistant-message/presentAssistantMessage.ts:346-347), convertsdisabledToolsinto a requirements map (presentAssistantMessage.ts:608) and callsvalidateToolUse(presentAssistantMessage.ts:618), where explicit disabling takes priority over every other allow rule (src/core/tools/validateToolUse.ts:133-146).mcp_tool_usearm (presentAssistantMessage.ts:130) consults none of that. It resolves the server name, synthesizes ause_mcp_toolblock (presentAssistantMessage.ts:275-290) and calls the handler directly:await useMcpToolTool.handle(cline, syntheticToolUse, {(presentAssistantMessage.ts:292). No settings read, no requirements map, novalidateToolUsecall.ensureMcpServerAllowedatsrc/core/tools/UseMcpToolTool.ts:63); its own doc comment separates that check from "the shared validateToolUse path" (src/core/tools/UseMcpToolTool.ts:24).modelInfo.excludedToolsis consumed by the prompt filter (src/core/prompts/tools/filter-tools-for-mode.ts:165-170) and never reaches themcp_tool_usearm.Consequence
With
use_mcp_toollisted indisabledTools, or excluded throughexcludedTools, a connected MCP server's tools can still run: every call the model makes through a dynamic native declaration is executed without a policy check. The documented execution-time rejection applies only to the wrapper form.Desired behavior
Executing a native
mcp_tool_useblock should consult the same policy thetool_usearm validates against, so a disabled or excludeduse_mcp_toolalso rejects dynamic MCP tool calls at execution time. This is a new property: on the observed commit only the wrapper path validates.Proposed approach
At the point where the
tool_usearm validates, route the synthesized call through the existing validation layer and return a structured errortool_resulton rejection. Mirror that layer's policy resolution rather than bespoke MCP logic.Acceptance criteria
disabledToolsentry naminguse_mcp_toolmakes a nativemcp_tool_usecall fail at dispatch, with a structured errortool_result; the server method is never invoked.excludedToolsentry naminguse_mcp_toolproduces the same rejection.use_mcp_toolenabled, native MCP calls behave as before.