[BUG] MCP tool results: undocumented ~50,000-character persist-to-disk threshold (character-based, not lifted by MAX_MCP_OUTPUT_TOKENS, message says "tokens")

Status Open
Reported on v2.1.283
Maintainer reply None cached
Activity 1 comment · opened Sep 27, 2026

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".

  1. The boundary is between 50,000 and 50,500 characters, identical for Latin and Greek text and for single- vs multi-line text.
  2. 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.
  3. It is not byte-based: Greek at 50,000 characters is ~92 KB of UTF-8 and still passes.
  4. MAX_MCP_OUTPUT_TOKENS=100000 does not raise it; only the wording of the notice changes.
  5. anthropic/maxResultSizeChars does 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_TOKENS does not affect it;
  • make the notice accurate (it reports a token limit for a character-based cutoff);
  • or let MAX_MCP_OUTPUT_TOKENS govern 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

  1. Save the minimal server below as synth_server.py (MCP Python SDK / FastMCP 1.28.1).
  2. mcp.json: {"mcpServers":{"synth":{"type":"stdio","command":"python","args":["synth_server.py"]}}}
  3. 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
  4. Inspect tool_result in 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗