Skip to content

MCP tool results bypass the context-size guard, silently blowing the context window #14

Description

@ScrewTSW

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:

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions