MCP tool response text-content block dropped when structuredContent is also present (runtime_get_value)

Status Open
Maintainer reply None cached
Activity 7 comments · opened Jul 21, 2026

Description

When calling an MCP tool whose response includes both a structuredContent block (JSON metadata) and a content text block (the actual document body), Claude Code appears to surface only the structured block and silently drops the text block.

Repro

  • MCP server: custom internal runtime-mcp server (HTTP transport)
  • Tool: runtime_get_value(name="consumer-service", filepath="xx/xx.json")
  • Called identically across: fresh session, re-authenticated session, and after fully removing/re-adding the MCP server — same result every time.

Expected: tool returns the full document body (JSON text) plus metadata, per the tool's own description ("Returns the document body as text ... plus publication metadata ... as separate structured content").

Actual (in Claude Code):

{"namespace":"consumer-service","file":"xx/xx.json","cell":"cell-001","doc_type":"json","document_id":" etc}

Only the metadata wrapper — no document body text.

Confirmed not server-side: the identical tool call against the identical MCP server, run from Cursor, returns the full body correctly (JSON with 26 store IDs). This isolates the bug to Claude Code's handling of the MCP response, not the server.

Environment: Claude Code CLI, macOS (Darwin 25.5.0)

View original on GitHub ↗

3 Comments

anombyte93 · 2 months ago

Independent Linux reproduction with converter-level cause

Confirmed the same defect on Claude Code CLI 2.1.222 with a local stdio MCP server.

Wire and transcript proof

The server returns a valid mixed result:

{
  "content": [{"type":"text","text":"<real answer body>"}],
  "structuredContent": {"model":"sonar-pro","mode":"pro","citations":10,"provenance":"available","degraded":false}
}

The recorded Claude Code tool_result.content is instead the 91-character serialization of structuredContent; the answer text block is absent. A direct MCP stdio handshake against the same server returns the answer, so the loss occurs after the client receives the valid CallToolResult.

Converter cause

Read-only extraction from the installed 2.1.222 binary at byte offset 266806674 recovered function Ihd (mapToolResultToToolResultBlockParam). Its effective order is:

  1. if structuredContent exists, serialize it;
  2. if content contains non-text blocks, preserve only those blocks and append serialized metadata;
  3. otherwise return serialized metadata;
  4. map the complete content array only when structuredContent is absent.

Therefore an all-text content array is deterministically dropped whenever the result also contains structuredContent.

Calibrated synthetic probe

A minimal in-memory reproduction of that exact branch produced:

{"result":"PASS","mapped_type":"structuredContent","mapped_chars":91,"control_type":"contentArray","answer_survives_without_structuredContent":true}

The control direction matters: the same converter preserves the answer when structuredContent is omitted. This rules out the probe/parser itself as the source of the disappearance.

Suggested regression gate

  • text content plus structuredContent: preserve the text and retain metadata without replacement;
  • structured-only result: preserve current fallback;
  • mixed text plus image/resource blocks: preserve every content block;
  • error and large-output paths: unchanged;
  • assert that adding structuredContent cannot remove pre-existing text content.

This affects every MCP tool returning text plus structured metadata, not only the server used for this reproduction.

spencerbeggs · 22 days ago

Adding one more data point in case it's helpful. I'm not sure whether this is intended behavior, so I'm just sharing what I observed.

On Claude Code 2.1.280 (macOS, stdio transport, claude -p --strict-mcp-config --output-format stream-json --verbose), I used a small MCP server whose tools put different marker strings in content and structuredContent. Then I read the tool_result blocks Claude Code passed to the model:

| tool | server sent | tool_result passed to the model |
|---|---|---|
| both_with_schema (declares outputSchema) | content: CONTENT-MARKER-A (markdown rendering)<br>structuredContent: {"marker":"STRUCTURED-MARKER-A"} | "{\"marker\":\"STRUCTURED-MARKER-A\"}" |
| both_no_schema (no outputSchema) | content: CONTENT-MARKER-B (markdown rendering)<br>structuredContent: {"marker":"STRUCTURED-MARKER-B"} | "{\"marker\":\"STRUCTURED-MARKER-B\"}" |
| content_only | content: CONTENT-MARKER-C (markdown rendering) | [{"type":"text","text":"CONTENT-MARKER-C (markdown rendering)"}] |

What I noticed:

  • Whenever structuredContent is present, the model gets JSON.stringify(structuredContent), and the content text doesn't appear anywhere in the stream (including the tool_use_result record). That happens whether or not the tool declares an outputSchema.
  • Leaving out structuredContent is the only way I found to get the content text to the model.

For context, and without claiming this is a bug: the spec describes content as present alongside structuredContent, and its example keeps the two semantically identical. Servers that put different, model-oriented text in content are going past that guidance, which the MCP maintainers discussed and deferred in SEP-2200, with SEP-3279 as the proposed follow-up. Other clients currently handle this differently, for example Cursor and MCP Apps hosts reading content. So this may be a deliberate choice. Mostly it would help server authors to know which field Claude Code treats as model-visible, maybe through a note in the MCP docs.

<details>
<summary>Probe server (Node, no dependencies)</summary>

import { createInterface } from "node:readline"
const send = (m) => process.stdout.write(JSON.stringify(m) + "\n")
const schema = { type: "object", properties: { marker: { type: "string" } }, required: ["marker"] }
const tools = [
  { name: "both_with_schema", description: "Returns a marker.", inputSchema: { type: "object" }, outputSchema: schema },
  { name: "both_no_schema", description: "Returns a marker.", inputSchema: { type: "object" } },
  { name: "content_only", description: "Returns a marker.", inputSchema: { type: "object" } }
]
const results = {
  both_with_schema: { content: [{ type: "text", text: "CONTENT-MARKER-A (markdown rendering)" }], structuredContent: { marker: "STRUCTURED-MARKER-A" } },
  both_no_schema: { content: [{ type: "text", text: "CONTENT-MARKER-B (markdown rendering)" }], structuredContent: { marker: "STRUCTURED-MARKER-B" } },
  content_only: { content: [{ type: "text", text: "CONTENT-MARKER-C (markdown rendering)" }] }
}
createInterface({ input: process.stdin }).on("line", (line) => {
  const msg = JSON.parse(line)
  if (msg.id === undefined) return
  if (msg.method === "initialize")
    return send({ jsonrpc: "2.0", id: msg.id, result: { protocolVersion: msg.params.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "probe", version: "0.0.0" } } })
  if (msg.method === "tools/list") return send({ jsonrpc: "2.0", id: msg.id, result: { tools } })
  if (msg.method === "tools/call") return send({ jsonrpc: "2.0", id: msg.id, result: results[msg.params.name] })
  send({ jsonrpc: "2.0", id: msg.id, result: {} })
})

mcp.json: { "mcpServers": { "probe": { "command": "node", "args": ["/path/to/server.mjs"] } } }

claude -p --model haiku --strict-mcp-config --mcp-config mcp.json \
  --allowedTools "mcp__probe__both_with_schema,mcp__probe__both_no_schema,mcp__probe__content_only" \
  --output-format stream-json --verbose \
  "Call these three tools once each and quote the exact raw text of each tool result verbatim." < /dev/null

</details>

Thanks for all the work on Claude Code's MCP support.

DdangJin · 18 days ago

Still reproducible on Claude Code 2.1.283 (macOS, stdio, claude -p --strict-mcp-config --output-format stream-json --verbose).

A test server's tool returned two content text blocks plus structuredContent:

| server sent | tool_result passed to the model |
|---|---|
| content: [JSON body, trailing notice text] + structuredContent | only the structuredContent JSON; both text blocks dropped, including the first one |
| same two content blocks, no structuredContent | both text blocks |

So any extra text a server appends after the serialized result (warnings, pagination notes, disclosures) silently never reaches the model once structuredContent is present.

Workaround we shipped server-side: we also copy those trailing text blocks into structuredContent (an optional notices: string[] field, declared in outputSchema), and keep them in content for content-only clients.

If dropping content is intended, documenting it would help. It is easy to miss because the spec only says structured results SHOULD also be serialized into a text block, and says nothing about clients ignoring the rest of content.

Showing cached comments. Read the full discussion on GitHub ↗