Skip to content

[BUG] Native mcp_tool_use dispatch skips runtime tool validation, so disabled MCP tools still execute #1646

Description

@DaubnerF

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions