Repository navigation
fix(mcp): don't let an MCP tool replace or remove a built-in (#1853) - #1863
diyaavirmani wants to merge 1 commit into
Conversation
The tool registry is a plain name-keyed map, and MCP entries were registered after the built-ins, so an MCP tool named like a built-in (the filesystem server's write_file and list_directory, the git server's git_commit) silently replaced it: handler, schema and approval policy. git_commit stopped prompting in auto-accept. The unhealthy and disconnect paths then removed MCP tools by name, which deleted the built-in for the rest of the session. ToolManager now tracks the names the MCP client registered. An MCP tool whose name is already taken by a built-in, custom or skill tool is skipped, the same way a colliding custom tool is, and is reported on the server's init result. The TUI, --plain and the daemon print a startup warning for it. The unhealthy path, the recovery to connected and disconnectMCP only add or remove names MCP holds. The MCP lookups behind /tools, the confirmation prompt and plan mode's read-only filter also only apply to those names, so a skipped tool's annotation can't hide or relabel the built-in. Two existing specs planted "MCP" tools with registerSkillTool and set the private mcpClient field; they now connect a fake client through initializeMCP instead. Closes Nano-Collective#1853.
nc-review: comments — 1 nit@diyaavirmani — a few things worth a look, none blocking. Closes #1853 cleanly. ToolManager now tracks names registered by MCP and treats any name collision with a built-in/custom/skill tool as an MCP skip (parallel to the existing custom-tool collision handling). Unhealthy/recovery/disconnect paths now mutate only MCP-owned names; the MCP lookups ( ⚪ nit · The recovery test covers 🔴 blocking · 🟠 a reviewer would ask for a change · ⚪ optional Automated code review — correctness, security, design, tests, plus duplicates and scope. A human still decides; this is not a substitute for review and is not exhaustive. The required status checks separately cover lint, formatting, types, unused dependencies, the test suite and the build. This bot never merges. Maintainers can rerun with |
|
Hi @diyaavirmani, thanks for this PR! It looks like a maintainer has left feedback Whenever you get a chance, could you take a look at the open comments? |
Description
Closes #1853.
The tool registry is a plain name-keyed map, and MCP tools were registered after the built-ins. So an MCP tool with a built-in's name silently replaced it: handler, schema and approval policy. The filesystem server's
write_fileandlist_directory, and the git server'sgit_commit, are common examples.git_committhen ran without a prompt in auto-accept. When the server later went unhealthy or disconnected, its tools were removed by name, which deleted the built-in for the rest of the session.As agreed in the issue, built-ins always win.
Change (
source/tools/tool-manager.ts):ToolManagertracks the names the MCP client registered.MCPInitResult.skippedTools).connected, anddisconnectMCPonly add or remove names that MCP itself registered. A built-in can no longer be replaced on recovery or removed on disconnect.getMCPToolInfo, used by/toolsand the confirmation prompt, and plan mode's read-only filter. So a skipped tool can't relabel the built-in as an MCP tool, and its missingreadOnlyHintcan't hide the built-in in plan mode. I found the plan-mode case while working on this: an unannotated MCPlist_directorywas hiding the built-inlist_directoryin plan mode.Warning:
describeSkippedMCPTools()builds one message, and the TUI (aWarningMessage),--plain(writeStatus) and the daemon (console.error) all print it at startup:Docs: a short "Tool Name Collisions" section in
docs/configuration/mcp-configuration.md.Type of Change
Changeset
pnpm changeset) describing this change for the changelogTesting
Automated Tests
.spec.ts/tsxfilespnpm test:allcompletes successfully): see below; the only failures also happen without this changeNew tests in
source/tools/tool-manager.spec.ts. They use a fake client through the existingcreateMCPClient()seam:skippedToolslists it, and the server's other tool still registersskippedToolsis unset when nothing collidesconnecteddoesn't replace the built-in, and its own tool comes back (the maintainer's extra case)disconnectMCPremoves only MCP tools (the maintainer's extra case)git_commitstill needs approval in auto-accept with a server offeringgit_commitdescribeSkippedMCPToolsformattingWith only
tool-manager.tsreverted (plus a stub for the new formatter), 7 of these fail. The other two, the "nothing collides" case and the formatter, are expected to pass there.Two existing tests created their "MCP" tools with
registerSkillTooland set the privatemcpClientfield, so as far asToolManageris concerned those tools were never registered by MCP. They now connect a fake client throughinitializeMCP, with the same assertions:tool-manager.spec.ts)custom-tools/end-to-end.spec.ts)Format, lint, types and knip pass. On macOS the full AVA suite passed 9,711 tests (20 skipped). The only 2 failures are the
gh-pr-worktreetests that fail the same way on upstream main (/varvs/private/vartmpdir paths on macOS).Manual Testing
End to end through the real CLI, on macOS with Node 22.23.3:
--plain run --json, auto-accept,@modelcontextprotocol/sdkthat logs each call and never writes to disk.These are the cases from the issue. The source of truth is
notes.txton disk, the MCP server's own call log, and the CLI's JSON report.9a244b89257b95e2)write_file(control)write_filenotes.txt, MCP never calledecho_tool+crash(control)list_directoryworkslist_directory+crashNot connectedUnknown tool: list_directory, no longer offeredlist_directoryworks and stays offeredgit_commit(control)git_commitIn the interactive TUI, I started in a project whose server offers
write_fileandecho_tool, accepted the trust prompt, and ran/tools:/toolslistswrite_file (fixture)./toolslistsecho_tool (fixture), andwrite_fileis listed with the built-ins.The fix cherry-picks cleanly onto the v1.32.0 release PR, and the branch merges cleanly with current main.
Checklist
WarningMessage,writeStatusandconsole.error, the same as MCP connection failures)