Unparseable MCP tool input is replayed as a {raw,len} wrapper which the model imitates; wrapper is then forwarded to the MCP server without schema validation
Environment
- Claude Code 2.1.201, Windows 11 Pro (10.0.26200)
- Model: claude-fable-5
- MCP server:
mcp-knowledge-graph1.3.2 (stdio, spawned vianpx -y)
Summary
When the model emits tool input for an MCP tool that fails JSON parsing (e.g. a bracket glitch), Claude Code correctly rejects the call with InputValidationError. However, two follow-up behaviors combine into a session-poisoning loop:
- Replay format teaches the model a wrong schema. The rejected
tool_useblock is stored and replayed to the API as{"raw": "<original string>", "len": <n>}(the__unparsedToolInputserialization). On retry, the model sees its own previous attempt in this wrapped form and starts imitating it — emitting{"raw": "...", "len": ...}as the actual tool arguments. (Evidence that the model, not the harness, produced the later wrappers: thelenvalues are wrong — e.g.len: 172for a 188-char string,len: 170for 165 chars — a harness would count exactly.)
- No schema validation before dispatch to MCP. The wrapper is valid JSON, so Claude Code forwards
{"raw": ..., "len": ...}as the MCP tool arguments even though it matches nothing in the tool's declared inputSchema. The server receives arguments without its required fields and fails opaquely — in our casemcp-knowledge-graphcrashed withMCP error -32603: Cannot read properties of undefined (reading 'map')(observations.mapon undefined).
Once this happens, every subsequent call to that tool in the session repeats the pattern (the poisoned examples accumulate in context). In our transcript the loop persisted for 2 hours: a late retry contained perfectly valid inner JSON — but still wrapped in {"raw","len"}, so it still failed. Meanwhile a parallel session on the same machine, same Claude Code version and same MCP server wrote successfully, confirming the issue is session-context-bound, not server-side.
Reproduction (as observed; initial trigger is stochastic)
- Model calls an MCP tool with malformed JSON input (missing
]in our transcript) →InputValidationError(correct behavior). - Model retries; its previous attempt is now visible in context as
{"raw": "...", "len": N}. - Model emits
{"raw": "<its JSON as a string>", "len": <guessed>}as tool input. - Claude Code forwards this object to the MCP server → server error
-32603(e.g.Cannot read properties of undefined (reading 'map')). - Steps 3–4 repeat for the rest of the session; the tool is effectively unusable until a fresh session.
Observed timeline (from session transcript + MCP logs)
| attempt | recorded input | result |
|---|---|---|
| 1–2 | broken JSON (missing ]), identical retry | InputValidationError, never reached server |
| 3 | {"raw": "<broken JSON with stray '}'>", "len": 172} (actual 188 chars) | forwarded → server -32603 ... (reading 'map') |
| 4 | broken JSON again | InputValidationError |
| 5–6 | {"raw","len"} wrappers | forwarded → -32603 ('map' / 'filter') |
| +2h | {"raw": "<VALID inner JSON>", "len": 170} (actual 165) | forwarded → -32603 |
Suggested fixes
- Replay the original raw string (or an explicit marker like
<unparsed-input>...</unparsed-input>) instead of a JSON-shaped{"raw","len"}wrapper that looks like a learnable argument format. - Validate MCP tool arguments against the tool's declared
inputSchemabefore dispatch, and return a validation error to the model instead of forwarding non-conforming arguments to the server. Server-side-32603messages hide the real cause and read as server failure (we initially misdiagnosed this as MCP server degradation).