MCP: tool result content[].text dropped from model when structuredContent is also present

Resolved 💬 5 comments Opened May 2, 2026 by sidis405 Closed May 6, 2026

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/mcp v0

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:

  1. Tool: find_in_stash_picker (search-and-pick over a personal stash).
  2. 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."
}
}
``

  1. Server-side log of the wire response confirms BOTH content[0].text and structuredContent are sent. (Verified via the server's tool-call log table.)
  2. In Claude Code:
  • ctrl+o to expand the tool result shows ONLY the structuredContent JSON. The content[0].text is not visible in the expanded transcript.
  • The model paraphrases from structuredContent only ("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.
  1. 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:

  1. The host filters out content[].text whenever the tool's tools/list entry declares _meta.ui.resourceUri (i.e. has a RendersApp annotation). We saw the filter persist even after we removed RendersApp and reconnected, so it may be tools/list cache stickiness OR a broader filter.
  2. The host always prefers structuredContent over content[].text when both are present and only forwards the structured part, regardless of RendersApp.

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

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

View original on GitHub ↗

This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗