MCP tool results silently replaced by an unreadable <<ccr:...,html,NNNB>> placeholder (two unrelated servers)
Summary
Some MCP tool results are replaced in the model's context by a placeholder of the form <<ccr:<hash>,html,<size>B>> instead of the tool's actual output. The model cannot read the result at all, so it either reports the tool as broken or, worse, guesses at what it contained.
This happens across two unrelated MCP servers (Supabase and Slack) in a single session, so it does not look like a server-side fault.
Repro
Smallest case, using the Supabase MCP's query_logs:
query_logs(project_id: "<project-ref>", sql: "select 'ping' as a, 1 as b")
Result delivered to the model:
{"result":"<<ccr:66b6eb0734dd,html,530B>>"}
Deterministic — two identical calls both returned html,530B with different hashes.
What rules out the obvious explanations
| Test | Result |
|---|---|
| query_logs, trivial 2-value query | <<ccr:…,html,530B>> |
| Same call repeated | <<ccr:…,html,530B>> |
| query_logs against a different Supabase project | <<ccr:…,html,524B>> |
| execute_sql, same project, same credential, same minute | renders normally |
| slack_list_user_channels format=full | same placeholder |
| slack_list_user_channels format=names_only | renders normally |
- Not auth — a sibling tool on the same server and credential works in the same minute.
- Not payload size —
{"a":"ping","b":1}is ~30 bytes; the placeholder reports 530B. - Not one server — Slack shows the identical marker.
- Not permanent — earlier calls in the same session to the same
query_logstool returned readable JSON, then it degraded.
The html type in the marker is the interesting part: the payload being captured appears to be HTML rather than the JSON the tool normally returns, and every HTML-typed result seems to get stored as a reference rather than passed through.
Expected
Either the tool output reaches the model, or the model is told the output was withheld and why (too large, wrong content type, fetch failed). A bare opaque reference with no retrieval path is the worst case: there is no tool to dereference <<ccr:…>>, so the content is simply lost mid-conversation.
Impact
The model cannot distinguish "this tool is broken" from "I cannot see this tool's reply." In my case it spent several turns misattributing the failure — first to result size, then to the vendor's backend being down — and told the user to go read the data manually in a dashboard. A one-line "result withheld: unsupported content type" would have avoided all of it.
Environment
- Claude Code 2.1.274
- macOS 27.0, arm64
- MCP servers: Supabase (
query_logs,execute_sql), Slack (slack_list_user_channels)
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Two additions that narrow this.
1. Every affected tool is a claude.ai connector, not a local MCP server.
All of them are namespaced
mcp__claude_ai_*— Supabase, Slack and Linear are cloud connectors routed through Anthropic's connector layer rather than local stdio servers. Three independent vendors breaking the same way at the same moment is implausible; one shared proxy is not. That also fitshtmlas the captured content type: an HTML error page from a proxy, where JSON was expected.Possibly related, though I can't confirm the naming is the same mechanism: #61044 describes MCP calls routed through
api.anthropic.com/v2/ccr-sessions/{id}/mcp, so "ccr" appears to name an Anthropic-side session proxy rather than a generic content-reference scheme.2. It is per-tool, not per-server, and not payload size.
| Call | Result |
|---|---|
| Supabase
query_logs,select 'ping' as a, 1 as b(~30B of data) |<<ccr:…,html,530B>>|| Supabase
execute_sql, same project, same credential, same minute | renders || Slack
slack_list_user_channelsformat=full| placeholder || Slack
slack_list_user_channelsformat=names_only| renders || Linear
list_issues, 25 issues, ~4KB | renders |So a 4KB Linear payload comes through while a 30-byte Supabase one does not, which rules out size. Within a single connector one tool fails while its sibling works, which rules out per-server auth or connection state.
query_logsreturned readable JSON earlier in the same session and then degraded to the placeholder permanently, so whatever flips is stateful rather than a fixed property of the tool./logindoes not fix it.Re-authenticated the claude.ai session, then immediately:
| Call | Result |
|---|---|
|
query_logs,select 'ping' as a, 1 as b|<<ccr:0d4b7c7e86d4,html,530B>>|| same, again |
<<ccr:f34456dbffe0,html,530B>>||
execute_sql, same project, seconds later | renders normally |Identical byte count before and after re-auth, and the sibling tool on the same connector still works. So this is not connector hydration or a stale session — the
/loginworkaround that resolves the "connector tools missing entirely" reports (#55914, #69422) has no effect here, which is further evidence this is a distinct bug rather than a variant of those.The same mechanism also corrupts data silently, which is worse than withholding it
The placeholder is the visible half of this. The invisible half is that when the same compressor shortens a result in place rather than replacing it, it rewrites field values into plausible but fabricated text, and marks only the response as a whole — not the affected fields.
Reproduced on the Linear connector. Same tool, same 5 records, seconds apart; the only difference is that the first call requests more fields and so crosses the compression threshold.
Call A —
list_projects(limit: 5), footer reads[271 words compressed to 68 (from 1 source line). Retrieve more: hash=e47b304d675eb41c3d710690]:Call B —
list_projects(limit: 5, fields: ["id","name"]), no compression:Call A's values are not truncations of Call B's.
"MVP cross-check description)"shares no substring with"MVP - Reporting"beyondMVP; the trailingdescription)looks spliced from a neighbouring field name. The record's ownurlin call A is.../project/mvp-reporting-519e0f88820c, so the corrupted name sits directly beside evidence contradicting it.Why this is the more serious half
<<ccr:…>>placeholder is fail-safe: the model can see something is missing and say so.In my case the corrupted values were project names. The same connectors in this session were also being used to read production drug names, dosing fields and error logs for a clinical application. A silently rewritten drug name or lab value that still parses as valid is a materially different class of risk from a missing one.
Also affects Bash output, not just MCP
Earlier in the same session,
grepoutput through the Bash tool carried[264 words compressed to 201 (from 22 source lines)]and arrived with source lines spliced together, which is what sent me to theReadtool instead. So this is not connector-specific — it is whatever shortens large tool results generally.Suggested minimum