[BUG] MCP tool results: undocumented ~50,000-character persist-to-disk threshold (character-based, not lifted by MAX_MCP_OUTPUT_TOKENS, message says "tokens")
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?
A text result from an MCP tool without anthropic/maxResultSizeChars is saved to a file once it exceeds roughly 50,000 characters, a cutoff that is unrelated to the documented 25,000-token default. The threshold is not documented, it is character-based, MAX_MCP_OUTPUT_TOKENS does not lift it, and the message the model receives says the result "exceeds maximum allowed tokens".
- The boundary is between 50,000 and 50,500 characters, identical for Latin and Greek text and for single- vs multi-line text.
- It is not token-based: by input-token accounting a 50,000-character inline result was ~34k tokens (Latin) and ~48k tokens (Greek, +39%). A token-based limit would have moved the Greek boundary down by ~14k characters; it did not. Both are also above the documented 25,000-token default.
- It is not byte-based: Greek at 50,000 characters is ~92 KB of UTF-8 and still passes.
MAX_MCP_OUTPUT_TOKENS=100000does not raise it; only the wording of the notice changes.anthropic/maxResultSizeCharsdoes raise it, as documented.
What Should Happen?
Any of:
- document the default character threshold in "MCP output limits and warnings" (the docs mention "the default persist-to-disk threshold" but give no number) and state that
MAX_MCP_OUTPUT_TOKENSdoes not affect it; - make the notice accurate (it reports a token limit for a character-based cutoff);
- or let
MAX_MCP_OUTPUT_TOKENSgovern this threshold, as the docs suggest.
Error Messages/Logs
Error: result (50,500 characters across 1 line) exceeds maximum allowed tokens. Output has been saved to <path>/tool-results/….txt
# with MAX_MCP_OUTPUT_TOKENS=100000, 60,000 characters:
<persisted-output> Output too large (58.6KB). Full output saved to: <path>…
Steps to Reproduce
- Save the minimal server below as
synth_server.py(MCP Python SDK / FastMCP 1.28.1). mcp.json:{"mcpServers":{"synth":{"type":"stdio","command":"python","args":["synth_server.py"]}}}- Run one tool call per case:
claude -p "<call the tool with n=…>" --strict-mcp-config --mcp-config mcp.json --output-format stream-json --verbose - Inspect
tool_resultin the stream-json log.
| Case | n (chars) | Result in the model's context |
|---|---|---|
| latin, single line | 45,000 / 49,000 / 50,000 | full text inline |
| latin, single line | 50,500 / 51,000 / 60,000 / 70,000 / 80,000 | Error: result (50,500 characters across 1 line) exceeds maximum allowed tokens. Output has been saved to <path>/tool-results/….txt |
| greek, single line | 50,000 | full text inline |
| greek, single line | 50,500 | saved to file, same message |
| latin, newline every 100 chars | 50,000 | full text inline |
| latin, newline every 100 chars | 50,500 … 75,000 | saved to file (… characters across 505 lines) exceeds maximum allowed tokens …) |
| greek, MAX_MCP_OUTPUT_TOKENS=100000 | 60,000 | still saved to file; message changes to <persisted-output> Output too large (58.6KB). Full output saved to: <path>… |
| latin, tool with anthropic/maxResultSizeChars: 250000 (echo_text_meta) | 120,000 / 240,000 | full text inline |
"""Minimal MCP server to reproduce Claude Code tool-result size limits.
echo_text(n, alphabet) -> success result of exactly n characters
raise_error(n) -> error result whose text is exactly n characters
*_meta variants -> same, with _meta["anthropic/maxResultSizeChars"] = 250000
"""
import random
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("synth")
ALPHABETS = {
"latin": "abcdefghijklmnopqrstuvwxyz",
"greek": "αβγδεζηθικλμνξοπρστυφχψω",
}
META = {"anthropic/maxResultSizeChars": 250000}
def make_text(n: int, alphabet: str, line_len: int = 0) -> str:
"""Deterministic random words, exactly n characters, ending with a marker.
line_len > 0: a newline replaces the space roughly every line_len characters."""
letters = ALPHABETS[alphabet]
rnd = random.Random(n)
marker = " END-OF-TEXT"
parts, size = [], 0
while size < n - len(marker):
w = "".join(rnd.choice(letters) for _ in range(rnd.randint(2, 9))) + " "
parts.append(w)
size += len(w)
body = "".join(parts)[: n - len(marker)]
if line_len > 0:
chars = list(body)
for i in range(line_len, len(chars), line_len):
chars[i] = "\n"
body = "".join(chars)
text = body + marker
assert len(text) == n
return text
def error_text(n: int, tool: str, alphabet: str = "latin") -> str:
"""Message such that the client-visible text 'Error executing tool <tool>: <msg>' is exactly n chars."""
prefix = f"Error executing tool {tool}: "
return make_text(n - len(prefix), alphabet)
@mcp.tool(structured_output=False)
def echo_text(n: int, alphabet: str = "latin", line_len: int = 0) -> str:
"""Return a text of exactly n characters (alphabet: latin | greek; line_len > 0 - multi-line)."""
return make_text(n, alphabet, line_len)
@mcp.tool(structured_output=False, meta=META)
def echo_text_meta(n: int, alphabet: str = "latin", line_len: int = 0) -> str:
"""Same as echo_text, tool carries anthropic/maxResultSizeChars=250000."""
return make_text(n, alphabet, line_len)
@mcp.tool(structured_output=False)
def raise_error(n: int, alphabet: str = "latin") -> str:
"""Fail with an error whose client-visible text is exactly n characters."""
raise ValueError(error_text(n, "raise_error", alphabet))
@mcp.tool(structured_output=False, meta=META)
def raise_error_meta(n: int, alphabet: str = "latin") -> str:
"""Same as raise_error, tool carries anthropic/maxResultSizeChars=250000."""
raise ValueError(error_text(n, "raise_error_meta", alphabet))
if __name__ == "__main__":
mcp.run()
Claude Model
Opus
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.283 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Non-interactive/CI environment
Additional Information
Impact: server authors size responses against the documented 25,000-token limit and find results silently moved out of the model's context at a different, unstated point. Without Read/Grep/Bash the model cannot recover the content. Because the cutoff is per character, the token cost of what does get through varies widely by script.
Only Latin and Greek were tested; other scripts (e.g. CJK) were not. A related limit on error results is filed separately.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗