FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

MCP tool response text-content block dropped when structuredContent is also present (runtime_get_value) · Issue #79944 · anthropics/claude-code · GitHub

Repository navigation

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

Open

Description

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)

Activity

  1. anombyte93 commented on Aug 5, 2026

    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.

  2. spencerbeggs commented on Sep 22, 2026

    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)
    structuredContent: {"marker":"STRUCTURED-MARKER-A"}
    "{\"marker\":\"STRUCTURED-MARKER-A\"}"
    both_no_schema (no outputSchema) content: CONTENT-MARKER-B (markdown rendering)
    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.

    Probe server (Node, no dependencies)
    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

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

  3. DdangJin commented on Sep 27, 2026

    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.

  4. XavierZhao1989 commented on Sep 28, 2026

    Another data point on scope: with the official Python SDK this happens by default for any tool that returns plain text.

    In mcp 2.2.0 (python-sdk), a bare @mcp.tool() on a -> str function advertises outputSchema: {"properties": {"result": {"type": "string"}}, ...} and sends structuredContent: {"result": "<the text>"} next to the text block. Because Claude Code passes only the serialized structuredContent to the model, the text reaches the model as an escaped JSON string, not as the markdown in content.

    Repro (Claude Code 2.1.283, macOS, stdio, claude -p --strict-mcp-config --output-format stream-json --verbose):

    from mcp.server.mcpserver import MCPServer
    
    mcp = MCPServer("echo")
    
    @mcp.tool()
    def echo_default(text: str) -> str:
        return text
    
    @mcp.tool(structured_output=False)
    def echo_plain(text: str) -> str:
        return text
    
    mcp.run()
    tool tool_result.content passed to the model
    echo_default "{\"result\":\"## Title\\n- item one\\n- item two\"}"
    echo_plain [{"type":"text","text":"## Title\n- item one\n- item two"}]

    Workaround for Python SDK servers: @mcp.tool(structured_output=False) on text tools. It works, but every server author has to discover it, so a client-side fix (use content when it is present, or pass both) would help every Python server at once.

  5. wstewartsmith99 commented on Oct 6, 2026

    +1, with a second server that hits this: the Microsoft Fabric IQ remote MCP server (https://fabriciq.svc.cloud.microsoft/v1/mcp/fabriciq, M365MCPServer 2026.9.4.31, protocol 2025-06-18), installed through the fabric-skills plugin from microsoft/skills-for-fabric. Server-side report: microsoft/skills-for-fabric#99.

    Versions: Claude Code CLI 2.1.290 and the VS Code extension (bundled 2.1.284).

    Repro: call ExecuteQuery with daxQueries=["EVALUATE ROW(\"x\", 1)"]. The server returns the rows in content[0].text and only an artifact_citation object in structuredContent. Claude Code sends the model JSON.stringify(structuredContent), so it sees the citation and not x = 1. DAX error messages are lost the same way. Results over about 10,000 characters come back as an embedded resource block and are kept, so only small results and errors disappear.

    Hooks can't work around it: a PostToolUse hook matched on mcp__plugin_fabric-skills_FabricIQ__.* gets tool_response as the already-converted citation string. The original text is not in the hook input or the transcript, so updatedMCPToolOutput has nothing to restore.

    Request: I realise dropping the text blocks has been deliberate since 2.0.21. When structuredContent does not mirror the text content (the spec says the text block SHOULD be the serialized JSON of structuredContent), could Claude Code keep the text blocks rather than drop them? Even a check for "text is non-empty and differs from the serialized structuredContent" would cover this case. Meanwhile we are documenting a workaround in the fabriciq skill, so there is no urgency from our side.

  6. gtech129 commented on Oct 8, 2026

    @wstewartsmith99 +1 confirming the same thing from Cowork (remote, 2.1.293) with the Fabric IQ connector. Small ExecuteQuery results come back as just the artifact_citation, and anything over ~10K characters comes back as a resource and works.

  7. JOWE-GG commented on Oct 13, 2026

    Another confirmation with the Microsoft Fabric IQ remote MCP server (custom connector via claude.ai, Team plan; server-side counterpart: microsoft/skills-for-fabric#99).

    Affected surfaces on 2026-10-12/13:

    • Claude Code CLI 2.1.296
    • Claude desktop app, Code tab
    • Cowork

    ExecuteQuery completes without error in ~1.3 s, but the model receives only {"artifact_citation": {...}}. The session transcript stores the tool_result and mcpMeta.structuredContent as the same citation object, and the text block with the result is not present anywhere in the transcript or debug log (--debug shows only call start and "completed successfully").

    The same connector, user and DAX return full results in claude.ai chat, so the difference is client-side handling of content vs structuredContent. Related report in claude-ai-mcp: #1132.

    +1 to the proposals above: keep non-empty text content that differs from the serialized structuredContent, or at minimum document which field is model-visible.

  8. chrisbernsdorf-scs commented on Oct 21, 2026

    +1 with the Microsoft Fabric IQ remote server (https://fabriciq.svc.cloud.microsoft/v1/mcp/fabriciq; server-side counterpart microsoft/skills-for-fabric#99), reproduced 2026-10-21.

    • Claude desktop app, Code tab, fabric-skills plugin: ExecuteQuery and GetSemanticModelSchema return only {"artifact_citation":{...}} on every model, for admin and non-admin users.
    • A business user on claude.ai with our skill loaded saw the same "names only" result.
    • A raw JSON-RPC call to the same server returns the rows in content[0].text alongside the citation-only structuredContent, so the data reaches the client and is dropped there.

    Request: when structuredContent is present, also pass the content text blocks to the model (or fall back to them when the tool declares no outputSchema covering the payload). Silently dropping text the server sent means an empty answer with no error, which users read as "no access".

  9. clouatre commented on Oct 22, 2026

    Still reproducible on 2.1.304 (macOS, stdio), in the main session and in subagents.

    Server: aptu-coder (Rust, rmcp 3). Its exec_command tool returns command output in content[].text and only metadata in structuredContent, matching its outputSchema. A successful call reaches the model as:

    {"exit_code":0,"output_collection_error":null,"output_truncated":false,"timed_out":false}
    

    The stdout is gone. When the command fails (isError: true), the text block comes through, so models have learned to append ; exit 1 to every command to see output. pi and goose show the same result correctly.

    The spec allows content and structuredContent to differ; its own list_users example pairs a human-readable text block with structured data. Passing content[] to the model alongside structuredContent, rather than in place of it, would fix this.

    Server-side workaround tracked in clouatre-labs/aptu-coder#1750.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL