MCP tool results silently replaced by an unreadable <<ccr:...,html,NNNB>> placeholder (two unrelated servers)

Status Closed — not planned
Reported on v2.1.274
Maintainer reply None cached
Activity 4 comments · opened Sep 26, 2026 · closed Oct 6, 2026

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_logs tool 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)

View original on GitHub ↗

3 Comments

zlotnika · 19 days ago

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 fits html as 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_channels format=full | placeholder |
| Slack slack_list_user_channels format=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_logs returned 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.

zlotnika · 19 days ago

/login does 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 /login workaround 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.

zlotnika · 19 days ago

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]:

P-INF2-13  name: "MVP cross-check description)"
P-INF2-12  name: "MVP: description)"
P-INF2-11  name: "MVP #4: Note"

Call B — list_projects(limit: 5, fields: ["id","name"]), no compression:

P-INF2-13  name: "MVP - Reporting"
P-INF2-12  name: "MVP: Insurance Verification, Revenue Cycle, Billing"
P-INF2-11  name: "MVP - Customer Demos #4: Clinical Documentation & Nurse's Note"

Call A's values are not truncations of Call B's. "MVP cross-check description)" shares no substring with "MVP - Reporting" beyond MVP; the trailing description) looks spliced from a neighbouring field name. The record's own url in call A is .../project/mvp-reporting-519e0f88820c, so the corrupted name sits directly beside evidence contradicting it.

Why this is the more serious half

  • The <<ccr:…>> placeholder is fail-safe: the model can see something is missing and say so.
  • In-place compression is fail-silent: the output is well-formed, plausible, and wrong. A model has no way to tell a compressed field from a real one, and will relay it as fact.

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, grep output 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 the Read tool instead. So this is not connector-specific — it is whatever shortens large tool results generally.

Suggested minimum

  1. Never rewrite the interior of a structured payload. Drop whole records, or truncate with an explicit marker at the truncation point — never interpolate.
  2. If a value was altered, mark that value, not just the response.
  3. For JSON results, prefer dropping trailing records and saying how many were dropped, since that is verifiable by the consumer.

Showing cached comments. Read the full discussion on GitHub ↗