MCP: tool result content[].text dropped from model when structuredContent is also present
Summary
When an MCP tool returns BOTH a content[].text part and structuredContent, Claude Code (CLI) appears to forward only structuredContent to the model. The text content is observable in the iframe-host machinery (so the host receives it) but the model never sees it.
This breaks tools that intentionally use the text part to give the model a human-presentable rendering of the data — e.g. a numbered candidate list with markdown links — while keeping structuredContent as a typed payload.
The same response shape works on Claude.ai web and on ChatGPT.
Environment
- Claude Code v2.1.126 (Opus 4.7, claude-opus-4-7)
- macOS 25.4.0
- Streamable HTTP MCP transport
- Server: Laravel 13 /
laravel/mcpv0
Reproduction
I have a working MCP server that emits a tool result with both content blocks. The server's behavior is verifiable from the wire side:
- Tool:
find_in_stash_picker(search-and-pick over a personal stash). - The tool returns:
``json``
{
"content": [
{ "type": "text", "text": "Found 6 candidates ...\n1. [link] 3ff8t — [The Unwritten Laws of Software Engineering](https://...) ...\n2. ..." }
],
"structuredContent": {
"status": "candidates_ready",
"hit_count": 6,
"fallback_used": false,
"query": "engineering",
"type": null,
"hits": [...],
"agent_instructions": "Present this list to the user ... Do not auto-pick."
}
}
- Server-side log of the wire response confirms BOTH
content[0].textandstructuredContentare sent. (Verified via the server's tool-call log table.) - In Claude Code:
ctrl+oto expand the tool result shows ONLY thestructuredContentJSON. Thecontent[0].textis not visible in the expanded transcript.- The model paraphrases from
structuredContentonly ("The picker returned X hits, but the numbered list isn't showing in the response I got back — only the structured status payload"). The model literally tells the user it didn't receive the text part.
- Same response from the same server, same authenticated session, viewed in:
- Claude.ai (web): the model receives both parts and presents the markdown-linked numbered list. ✅
- ChatGPT (with MCP enabled): the model receives both parts and presents the list. ✅
- Claude Code CLI: only structuredContent makes it to the model. ❌
Why it matters
Several MCP App / picker patterns put the user-facing prose in content[].text and the typed payload in structuredContent (per the MCP Apps spec on visibility: content and structuredContent are model-visible; _meta is iframe-only). When a host filters one of the two model-visible channels, tools have to duplicate everything into structuredContent. That works around it but is a real spec deviation.
In our case, after observing this, we ended up duplicating hits into structuredContent.hits and adding an agent_instructions string so models on Claude Code can still present the list. Worked, but the text content is the natural channel for that.
Possible cause
Inferred only — would need confirmation from the Claude Code team. Two hypotheses:
- The host filters out
content[].textwhenever the tool'stools/listentry declares_meta.ui.resourceUri(i.e. has aRendersAppannotation). We saw the filter persist even after we removedRendersAppand reconnected, so it may be tools/list cache stickiness OR a broader filter. - The host always prefers
structuredContentovercontent[].textwhen both are present and only forwards the structured part, regardless ofRendersApp.
The behavior is consistent across Disconnect/Reconnect of the MCP connector and across new ccc sessions on a given install.
Expected
Per MCP spec / ext-apps#380, both content[].text and structuredContent should reach the model. Hosts may render content as the user-visible channel and structuredContent as the typed channel, but neither should be silently dropped from the model's transcript when both are present.
Cross-references
- modelcontextprotocol/ext-apps#380 — Properly clarify content model visibility - content vs structuredContent vs _meta
- modelcontextprotocol/ext-apps#481 — MCP Apps: No shared session identity between AppBridge and Model sub-connections
- Related design discussion in our repo: https://github.com/sidis405/stashed.live/issues/116
Workaround
Duplicate model-targeted prose into structuredContent (e.g. as an agent_instructions string) until the filter is fixed. Live in our PR: https://github.com/sidis405/stashed.live/pull/111
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗