Summary
MCP tool results are inserted into chat history with no size check whatsoever. A single oversized result silently consumes the entire context window, the request never returns, and the session becomes unrecoverable.
The guard for this already exists and is already used by the built-in file tools. callTool.ts simply never calls it.
The asymmetry
throwIfFileExceedsHalfOfContext (core/tools/implementations/readFileLimit.ts) counts tokens before content enters context and throws a typed error the model can act on:
throw new ContinueError(
ContinueErrorReason.FileTooLarge,
`File ${filepath} is too large (${tokens} tokens vs ${tokenLimit} token limit). Try another approach`,
);
Three built-ins call it: readFile, readCurrentlyOpenFile, readFileRange.
core/tools/callTool.ts calls it zero times. The MCP branch pushes text straight through (callTool.ts:146-152):
(response.content as any).forEach((item: any) => {
if (item.type === "text") {
contextItems.push({
name: extras.tool.displayTitle,
description: "Tool output",
content: item.text, // <-- unbounded
icon: extras.tool.faviconUrl,
});
grep -n "contextLength\|countTokens\|maxChars\|MAX_" core/tools/callTool.ts → no matches.
So the same operation on the same file succeeds or destroys the session depending purely on which server answered. read_file refuses; mcp__filesystem__read_file does not.
Reproduction
Config: @modelcontextprotocol/server-filesystem over /home/tdancs. Model context 40,960.
filesystem_read_file {
"path": ".../anthropic.claude-code-2.1.220-linux-x64/extension.js",
"head": 300
}
Target is an esbuild bundle: 2,650,012 bytes, 907 lines, longest line 156,328 chars.
Result: tool call reaches status generated and never resolves. No output is ever rendered, so from the user's side the model appears to hang while "generating something invisible". Roughly 660k tokens against a 40,960 window.
Note head: 300 is not a defence — that server counts lines, and for a minified bundle 300 lines is nearly the whole file. Any line-based limit is meaningless on minified JS. The client cannot rely on server-side limiting.
Retry loop
The same call appears twice in the session — first canceled, then re-issued with byte-identical arguments four turns later. Because the cancellation produced no result, nothing recorded why it died, so the model had no reason not to repeat it.
Expected behaviour
Oversized results must never enter context. Throw before insertion so the model receives a tool failure with an actionable message — cheaper than inserting-then-evicting, and it leaves a durable record that prevents the identical retry.
The message must explain why, not just that it was too big. For minified bundles specifically, it should state that line-based head/limit params won't help, or the model just retries with a smaller head and hits the same wall.
Proposed fix
Hoist the check from the built-ins into callToolFromUri so it covers every tool result regardless of origin.
Two design questions worth settling first:
- Threshold. Half-context suits a single
read_file, but MCP tools fire in sequence — three results at 49% each still overflow. A per-call fraction that accounts for remaining context would be stricter, at the cost of more machinery.
- Truncate vs. reject. Built-ins reject;
grepSearch/globSearch truncate and announce it (DEFAULT_GREP_SEARCH_CHAR_LIMIT, "the number of results exceeded 100"). Rejection is right for a minified bundle where the first N lines are worthless; truncation is better for a large log. Rejection is the safer default and matches existing file-tool behaviour.
Relationship to #12
Related symptom, different mechanism. #12 is about failures the model cannot see. This is about successes too large to hold. Both produce identical-retry loops, and both are fixed by ensuring every tool call leaves a legible result — but the code paths are unrelated.
Summary
MCP tool results are inserted into chat history with no size check whatsoever. A single oversized result silently consumes the entire context window, the request never returns, and the session becomes unrecoverable.
The guard for this already exists and is already used by the built-in file tools.
callTool.tssimply never calls it.The asymmetry
throwIfFileExceedsHalfOfContext(core/tools/implementations/readFileLimit.ts) counts tokens before content enters context and throws a typed error the model can act on:Three built-ins call it:
readFile,readCurrentlyOpenFile,readFileRange.core/tools/callTool.tscalls it zero times. The MCP branch pushes text straight through (callTool.ts:146-152):grep -n "contextLength\|countTokens\|maxChars\|MAX_" core/tools/callTool.ts→ no matches.So the same operation on the same file succeeds or destroys the session depending purely on which server answered.
read_filerefuses;mcp__filesystem__read_filedoes not.Reproduction
Config:
@modelcontextprotocol/server-filesystemover/home/tdancs. Model context 40,960.Target is an esbuild bundle: 2,650,012 bytes, 907 lines, longest line 156,328 chars.
Result: tool call reaches status
generatedand never resolves. No output is ever rendered, so from the user's side the model appears to hang while "generating something invisible". Roughly 660k tokens against a 40,960 window.Note
head: 300is not a defence — that server counts lines, and for a minified bundle 300 lines is nearly the whole file. Any line-based limit is meaningless on minified JS. The client cannot rely on server-side limiting.Retry loop
The same call appears twice in the session — first
canceled, then re-issued with byte-identical arguments four turns later. Because the cancellation produced no result, nothing recorded why it died, so the model had no reason not to repeat it.Expected behaviour
Oversized results must never enter context. Throw before insertion so the model receives a tool failure with an actionable message — cheaper than inserting-then-evicting, and it leaves a durable record that prevents the identical retry.
The message must explain why, not just that it was too big. For minified bundles specifically, it should state that line-based
head/limitparams won't help, or the model just retries with a smallerheadand hits the same wall.Proposed fix
Hoist the check from the built-ins into
callToolFromUriso it covers every tool result regardless of origin.Two design questions worth settling first:
read_file, but MCP tools fire in sequence — three results at 49% each still overflow. A per-call fraction that accounts for remaining context would be stricter, at the cost of more machinery.grepSearch/globSearchtruncate and announce it (DEFAULT_GREP_SEARCH_CHAR_LIMIT, "the number of results exceeded 100"). Rejection is right for a minified bundle where the first N lines are worthless; truncation is better for a large log. Rejection is the safer default and matches existing file-tool behaviour.Relationship to #12
Related symptom, different mechanism. #12 is about failures the model cannot see. This is about successes too large to hold. Both produce identical-retry loops, and both are fixed by ensuring every tool call leaves a legible result — but the code paths are unrelated.