[BUG] tools/list_changed doesn't refresh the deferred-tool / ToolSearch index in interactive sessions (still repros on 2.1.165; carve-out from #4118 / #60626)

Status Open
Reported on v2.1.86
Maintainer reply None cached
Activity 11 comments · opened Jun 7, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When an MCP server registers a new tool at runtime and emits notifications/tools/list_changed, the new tool never becomes invocable in the active interactive session. Specifically, it does not appear in the deferred-tool index that ToolSearch queries, so the model cannot load its schema and cannot call it. A full /mcp reconnect or session restart is the only workaround.

This is the same root cause raised in #60626, which was closed as not_planned (duplicate) and consolidated into #4118 — but #4118 was then closed as completed on the strength of the v2.1.0 handler work. The v2.1.0 fix wired up the notification handler for the /mcp capability list, but did not update the deferred-tool / ToolSearch surface used during an active conversation. So the specific #60626 bug is effectively untracked: folded into an issue that was simultaneously marked done. Multiple post-closure comments on #4118 report the same thing on 2.1.86 and 2.1.146; this report confirms it still repros on 2.1.165.

What Should Happen?

After tools/list_changed, the deferred-tool index backing ToolSearch should refresh before the next tool invocation (or the model should be able to force a refresh), so runtime-registered tools become loadable without a reconnect.

Error Messages/Logs

Steps to Reproduce

  1. Connect an MCP server that supports runtime tool registration (reproduced here with a WebMCP bridge on ws://localhost:4797).
  2. In an active interactive session, trigger the server to register a new tool. Server logs confirm registration and the server emits notifications/tools/list_changed:
  3. Registered channel: /localhost_8080
  4. Received message: registerTool on /localhost_8080
  5. Tool registered: localhost_8080-get_current_mel
  6. Ask the model to search for / use the new tool. ToolSearch returns no match for the tool by exact name (select:mcp__webmcp___localhost_8080-get_current_mel), partial name (localhost_8080), or semantic query.
  7. The tool stays uncallable until /mcp reconnect or a session restart.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.165 (native binary, built 2026-06-04)

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

The deferred-tool index is populated at server connect/init time and is not refreshed mid-session. ToolSearch searches a stale snapshot; the runtime-registered tool is invisible. The two WebMCP tools present at connect (get-token, define-mcp-tool) are searchable; the one registered after connect is not — which points to a connect-time snapshot rather than a live/list_changed-driven refresh of the deferred surface.

Why this isn't covered by existing issues
#4118 — closed completed; the v2.1.0 fix covered the /mcp handler, not the deferred-tool / ToolSearch surface. Post-closure comments (2.1.86, 2.1.146) report ToolSearch still doesn't pick up newly promoted tools.
#60626 — closed not_planned as a duplicate of #4118; this was the deferred-tool case, and it got absorbed into an issue that was then closed as done.
#62844 — open, but scoped to headless --print --output-format stream-json mode only.
#65569 / #64909 — adjacent (skill-backing deferred tools / sub-agent empty registry), not the interactive runtime-registration case.

View original on GitHub ↗

3 Comments

LudaThomas · 2 months ago

if I do kill the session and start a new one the deferred-tool index is up to date and claude can use the tools.

Austin519 · 2 months ago

+1 — confirming this reproduces with a different MCP and across two transports, which rules out WebMCP-specific and transport-specific causes. The trigger isn't "new tool registered at runtime" — it's "tools published after the deferred-tool snapshot is taken at connect/init time," which the OP's last paragraph already nails. Adding our repro for the case-coverage record:

Setup (Linux, Claude Code 2.1.x interactive session):

mcp-toggle is a runtime MCP-proxy MCP. Its mcp_enable("foo") tool spawns a child MCP, dynamically publishes the child's tools under the prefix foo__, and emits notifications/tools/list_changed via session.send_tool_list_changed(). mcp_disable does the reverse.

Symptom (identical to OP):

mcp_enable("context7")
  → { tools_loaded: 2, tool_prefix: "context7__" }            ✓ proxy did its job
context7__resolve-library-id(libraryName: "Astro middleware")
  → "No such tool available: context7__resolve-library-id"   ✗
ToolSearch(select:context7__resolve-library-id)
  → "No matching deferred tools found"                        ✗

Same failure for an HTTP-transport child (playwright, 23 tools loaded per enable result; playwright__browser_navigateNo such tool available). So the bug is transport-agnostic on the client side — both stdio child MCPs and Streamable-HTTP child MCPs fail identically.

Confirmed mcp-toggle is doing its end correctly:

Driven directly as a stdio child via a fresh ClientSession in a Python test (tests/test_proxy_live.py in the repo), the exact same mcp_enable call followed by list_tools returns the newly-published tools, and forwarding a call returns real upstream data. So the JSON-RPC notification is emitted and the MCP-SDK side honors it correctly — the gap is on the Claude Code host, exactly as OP describes.

Why this matters for catalog-style on-demand MCPs. The whole value prop of mcp-toggle and similar runtime-management tools (context-window discipline by spawning MCPs only when needed) is dead-on-arrival in Claude Code interactive sessions until this is fixed. Today the pragmatic workaround is to register everything claude mcp add --scope user, paying the context cost of all tools at session start — which is what mcp-toggle v2 was built to avoid.

Happy to provide additional repro detail (mcp-toggle source, session transcript, SDK versions) if it helps narrow down the deferred-tool-snapshot path on the host side.

cesarlai-alt · 1 month ago

Can confirm the split you're describing from the other side. I've been driving notifications/tools/list_changed at runtime from a gateway (mcp-salad — Unix socket, enable/disable mutates the server pool and fires the notification), and the behavior tracks exactly what you wrote: the /mcp capability list reflects the change, but the tool doesn't become invocable in an active interactive session until a reconnect.

In a ToolSearch/deferred-tool setup the failure is even quieter — the newly-registered tool isn't in the searchable index, so the model can't even surface its schema to load it, let alone call it. So from the user's seat it looks like the notification silently did nothing, when really the capability side updated and the deferred-tool surface didn't. Enabling then disabling a server round-trips fine on the /mcp list but the call path stays stale until /mcp reconnect.

Net: the v2.1.0 handler wired the notification to the capability list but not to the deferred-tool index the conversation actually dispatches through, which is the carve-out you're pointing at. Happy to share repro logs from the gateway side if a minimal reproduction helps — it exercises this path on every enable/disable.

Showing cached comments. Read the full discussion on GitHub ↗