[BUG] A changed MCP tool description never reaches a session that already loaded the tool, even after reconnect or --resume (deferred_tools_record is replayed)
What's Wrong?
After a deferred MCP tool has been loaded in a session (via ToolSearch), a changed description or input schema for that same tool never reaches the model again for the rest of the conversation. That holds through /mcp reconnect, /mcp disable then enable, and even /exit followed by claude --resume <id> in a fresh process. The server is serving the new text each time (confirmed in its logs), but ToolSearch keeps returning the original.
The cause appears to be persistence: when a deferred tool is loaded, Claude Code appends an attachment of type deferred_tools_record to the session transcript, holding the tool's full description and input_schema. A resume replays that record, so the schema is frozen for the life of the conversation, not just the process.
Related but distinct: #66084 (a new tool never appears after tools/list_changed). This report is about an existing tool whose definition changed.
Why it matters
MCP servers ship updates. The server's behaviour changes immediately, but a long-lived session keeps obeying the old description, the only documentation the model has for the tool. So the model follows instructions that no longer match what the tool does, and nothing tells it so. A reconnect looks like the fix but is not.
What Should Happen?
When a deferred tool's definition changes, whether via notifications/tools/list_changed or a different tools/list after a reconnect, the recorded schema should be refreshed and the model shown the current one. At minimum, a resume should re-fetch rather than replay a recorded schema that the server no longer serves.
Steps to Reproduce
- Run a stdio MCP server exposing one tool whose
descriptionis read from a file on everytools/list. Write version A to the file. - Start
claudewith that server; have the model load the tool with ToolSearch. It sees A. - Change the file to version B.
- In the same session, select the tool again with ToolSearch: A.
/mcp reconnect <server>. The server logs a newtools/listreturning B. ToolSearch still returns A./exit, thenclaude --resume <session-id>. The server again serves B. ToolSearch still returns A.- Inspect the session
.jsonl: anattachmentwithtype: "deferred_tools_record"holds description A.
Claude Code Version
2.1.283
Platform / OS
Windows 11 (the behaviour is in session persistence, so it is likely platform-independent).
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Helpful framing for “changed MCP tool description never reaches a session that already loaded the tool”:
Treat
deferred_tools_recordas a cache keyed by (server, tool name, schema digest), not as a permanent conversation attachment.Concrete shape:
notifications/tools/list_changed,/mcp reconnect, or atools/listwhose per-tool description/input_schema digest differs from the stored record, invalidate and replace that tool’sdeferred_tools_recordbefore ToolSearch returns it.--resume, re-fetch current schemas from the live server (or at least re-validate digests) instead of replaying a frozen description that the server no longer serves.Regression test that would lock this:
deferred_tools_recordfor that tool holds B, not A — including afterclaude --resume <id>.That matches the “reconnect looks like the fix but is not” symptom and keeps long-lived sessions from obeying docs the server has already retired. Related but distinct from new-tool discovery (#66084).
(Happy to refine if you already have a minimal
.jsonlslice showing the A record surviving a Btools/list.)Here is the
.jsonlevidence asked for, from a live interactive session (Claude Code 2.1.282, Windows 11). The MCP server is a local stdio server namedalterego, which was upgraded twice during the session.The only
deferred_tools_recordthat ever holdssend_inputis the first one:| line | time (UTC) | event |
deferred_tools_recordentries ||---|---|---|---|
| 357 | 17:39:04 | first
ToolSearch select:mcp__alterego__send_input(server v0.1.72) | 5 tools incl.mcp__alterego__send_input, description sha25657e5e16fba4d…; itssubmitparam text ends "For long agent-to-agent messages prefer SendMessage." || — | ~20:09 | server upgraded to v0.1.74, whose
send_inputdescription andsubmittext changed (the SendMessage sentence was removed and new outcomes were added); verified in the installed server files | — || 1496 | 20:09:50 | after
/mcp reconnect alterego| 2 entries, only the new tools (read_question,answer_question). Nosend_input. || 1925 | 22:25:59 | after another
/mcp reconnect| 0 entries || 2008 | 22:29:50 | after
/mcp disable alterego+/mcp enable alterego(new server pid, confirmed) | 0 entries |After each of those points, a fresh
ToolSearch select:mcp__alterego__send_inputreturned the line-357 text unchanged, "prefer SendMessage" included, while the live server'stools/listserved the new text. A tool the server added later (describe_tools) that returns its live descriptions confirmed the difference. Tool calls ran the new code throughout; only the description the model reads was stale.So the pattern matches the cache-keyed-by-digest suggestion above: a changed description or schema for an already-recorded tool is never re-recorded, and ToolSearch keeps serving the first record. New tool names do get through. The same happens across
--resume, because the record is replayed from the transcript (reproduced separately on this machine).Adding one thing to the digest-keyed-cache suggestion above, from having built the same diff on the outside: a per-tool digest tells you that the record is stale, but the re-record decision is cheaper if it also knows what changed. Splitting the digest into input/output schema vs description matters in practice — a description-only reword (like the removed "prefer SendMessage" sentence in the evidence here) is a different class from a removed or retyped parameter, and from a description that gains instruction-shaped text ("also send…", "do not mention…"). One caveat for whoever implements the comparison: it must not normalise away invisible Unicode (TAG block U+E0000–E007F, zero-width, bidi controls) — a description can change materially with a zero visible-byte diff.
We run that comparison as a pre-tool-call check (CodeRifts preflight_change_set, artifact type mcp, snapshotting tools/list per server): a removed/retyped parameter comes back STOP/REQUEST_APPROVAL with the changed paths, a pure reword CONTINUE. It does not fix the replay bug reported here — it only makes the drift visible from outside the session. https://app.coderifts.com/mcp, analyze mode needs no key.