claude-code - 💡(How to fix) Fix [BUG] MCP tool results: image content blocks dropped when `structuredContent` is also present [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#54737Fetched 2026-04-30 06:37:24
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Author
Timeline (top)
labeled ×3commented ×1

When an MCP tool returns a CallToolResult that contains both a content array (with ImageContent blocks) and a structuredContent field, Claude Code silently discards the entire content array. The model never sees the images; only the JSON-stringified structuredContent reaches the conversation.

The MCP spec permits both fields to coexist — structuredContent is the parsed object form, content carries the renderable blocks. Claude.ai desktop and MCP Inspector handle both correctly. Only Claude Code drops content.

Error Message

Error Messages/Logs

Root Cause

The bundled-JS function that parses CallToolResult into Claude Code's internal representation short-circuits on structuredContent. Decompiled from claude v2.1.123:

async function lF5(H, _, q, K) {
  if (H && typeof H === "object") {
    if ("toolResult" in H)
      return { content: String(H.toolResult), type: "toolResult" };
    if ("structuredContent" in H && H.structuredContent !== void 0)
      return {
        content: VH(H.structuredContent),  // VH = JSON.stringify
        type: "structuredContent",
        schema: v36(H.structuredContent)
      };
      // ↑ early-return: H.content (and any ImageContent inside it) is unreachable
    if ("content" in H && Array.isArray(H.content)) {
      let T = (await Promise.all(H.content.map((A) => cB7(A, q, K, true)))).flat();
      return { content: T, type: "contentArray", schema: v36(mD_(T)) };
    }
  }
  ...
}

Once structuredContent is present, the H.content branch — where image blocks are processed via cB7 into the model-bound ImageContent form — is never reached. There is no merge, no fallback; image bytes never enter the conversation.

(Function names from minification; the eng with the un-minified source can find this by behavior — it's the function that maps an MCP CallToolResult to the internal { content, type, schema } shape.)

Fix Action

Fix / Workaround

Workaround (server side)

Code Example



---

#!/usr/bin/env python3
# repro.py — minimal MCP server returning both content and structuredContent
import json, sys

RED_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="

def respond(rid, result):
    sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": result}) + "\n")
    sys.stdout.flush()

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    req = json.loads(line)
    method, rid = req.get("method"), req.get("id")
    if rid is None:  # notification, no response
        continue
    if method == "initialize":
        respond(rid, {
            "protocolVersion": "2025-06-18",
            "capabilities": {"tools": {}},
            "serverInfo": {"name": "image-bug-repro", "version": "0.1"},
        })
    elif method == "tools/list":
        respond(rid, {"tools": [{
            "name": "get_image",
            "description": "Returns text+image content AND structuredContent.",
            "inputSchema": {"type": "object", "properties": {}},
        }]})
    elif method == "tools/call":
        respond(rid, {
            "content": [
                {"type": "text", "text": '{"color":"red","size":"1x1"}'},
                {"type": "image", "data": RED_PNG_B64, "mimeType": "image/png"},
            ],
            "structuredContent": {"color": "red", "size": "1x1"},
        })

---

async function lF5(H, _, q, K) {
  if (H && typeof H === "object") {
    if ("toolResult" in H)
      return { content: String(H.toolResult), type: "toolResult" };
    if ("structuredContent" in H && H.structuredContent !== void 0)
      return {
        content: VH(H.structuredContent),  // VH = JSON.stringify
        type: "structuredContent",
        schema: v36(H.structuredContent)
      };
      // ↑ early-return: H.content (and any ImageContent inside it) is unreachable
    if ("content" in H && Array.isArray(H.content)) {
      let T = (await Promise.all(H.content.map((A) => cB7(A, q, K, true)))).flat();
      return { content: T, type: "contentArray", schema: v36(mD_(T)) };
    }
  }
  ...
}

---

async function lF5(H, _, q, K) {
  if (H && typeof H === "object") {
    if ("toolResult" in H)
      return { content: String(H.toolResult), type: "toolResult" };
    if ("content" in H && Array.isArray(H.content)) {
      let T = (await Promise.all(H.content.map((A) => cB7(A, q, K, true)))).flat();
      return { content: T, type: "contentArray", schema: v36(mD_(T)) };
    }
    if ("structuredContent" in H && H.structuredContent !== void 0)
      return {
        content: VH(H.structuredContent),
        type: "structuredContent",
        schema: v36(H.structuredContent)
      };
  }
  ...
}
RAW_BUFFERClick to expand / collapse

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • I am using the latest version of Claude Code

What's Wrong?

Summary

When an MCP tool returns a CallToolResult that contains both a content array (with ImageContent blocks) and a structuredContent field, Claude Code silently discards the entire content array. The model never sees the images; only the JSON-stringified structuredContent reaches the conversation.

The MCP spec permits both fields to coexist — structuredContent is the parsed object form, content carries the renderable blocks. Claude.ai desktop and MCP Inspector handle both correctly. Only Claude Code drops content.

Impact

Any MCP server that returns image content alongside structured output is broken in Claude Code. Servers built with the official Go MCP SDK trip this by default: its typed ToolHandlerFor[In, Out] wrapper unconditionally marshals the second return value into res.StructuredContent after the handler runs, with no opt-out hook ([email protected]/mcp/server.go:340-394). The official Python SDK's structured-output helpers behave similarly. So this is the common path, not an edge case.

What Should Happen?

The model receives image responses, and for the example the text block + the image. It describes a red pixel (or similar), confirming the image was forwarded.

Error Messages/Logs

Steps to Reproduce

Reproduction

A self-contained ~30-line Python stdio MCP server that emits both fields. No dependencies.

#!/usr/bin/env python3
# repro.py — minimal MCP server returning both content and structuredContent
import json, sys

RED_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=="

def respond(rid, result):
    sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": result}) + "\n")
    sys.stdout.flush()

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    req = json.loads(line)
    method, rid = req.get("method"), req.get("id")
    if rid is None:  # notification, no response
        continue
    if method == "initialize":
        respond(rid, {
            "protocolVersion": "2025-06-18",
            "capabilities": {"tools": {}},
            "serverInfo": {"name": "image-bug-repro", "version": "0.1"},
        })
    elif method == "tools/list":
        respond(rid, {"tools": [{
            "name": "get_image",
            "description": "Returns text+image content AND structuredContent.",
            "inputSchema": {"type": "object", "properties": {}},
        }]})
    elif method == "tools/call":
        respond(rid, {
            "content": [
                {"type": "text", "text": '{"color":"red","size":"1x1"}'},
                {"type": "image", "data": RED_PNG_B64, "mimeType": "image/png"},
            ],
            "structuredContent": {"color": "red", "size": "1x1"},
        })

Steps:

  1. Save as repro.py.
  2. Register it: claude mcp add image-bug -- python3 /absolute/path/to/repro.py
  3. Run claude and prompt: "Call the get_image tool and describe what image you see."

Expected

The model receives the text block + the image. It describes a red pixel (or similar), confirming the image was forwarded.

Actual

The model receives only the structured JSON. Asked about the image, it reports there is no image / only metadata.

Cross-check: configure the identical server with Claude.ai desktop or MCP Inspector — both surface the image correctly.

Claude Model

Opus

Is this a regression?

No, this never worked

Last Working Version

No response

Claude Code Version

2.1.123

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

Root cause

The bundled-JS function that parses CallToolResult into Claude Code's internal representation short-circuits on structuredContent. Decompiled from claude v2.1.123:

async function lF5(H, _, q, K) {
  if (H && typeof H === "object") {
    if ("toolResult" in H)
      return { content: String(H.toolResult), type: "toolResult" };
    if ("structuredContent" in H && H.structuredContent !== void 0)
      return {
        content: VH(H.structuredContent),  // VH = JSON.stringify
        type: "structuredContent",
        schema: v36(H.structuredContent)
      };
      // ↑ early-return: H.content (and any ImageContent inside it) is unreachable
    if ("content" in H && Array.isArray(H.content)) {
      let T = (await Promise.all(H.content.map((A) => cB7(A, q, K, true)))).flat();
      return { content: T, type: "contentArray", schema: v36(mD_(T)) };
    }
  }
  ...
}

Once structuredContent is present, the H.content branch — where image blocks are processed via cB7 into the model-bound ImageContent form — is never reached. There is no merge, no fallback; image bytes never enter the conversation.

(Function names from minification; the eng with the un-minified source can find this by behavior — it's the function that maps an MCP CallToolResult to the internal { content, type, schema } shape.)

Proposed fix

Swap the precedence: read content first, fall back to structuredContent only when content is absent.

async function lF5(H, _, q, K) {
  if (H && typeof H === "object") {
    if ("toolResult" in H)
      return { content: String(H.toolResult), type: "toolResult" };
    if ("content" in H && Array.isArray(H.content)) {
      let T = (await Promise.all(H.content.map((A) => cB7(A, q, K, true)))).flat();
      return { content: T, type: "contentArray", schema: v36(mD_(T)) };
    }
    if ("structuredContent" in H && H.structuredContent !== void 0)
      return {
        content: VH(H.structuredContent),
        type: "structuredContent",
        schema: v36(H.structuredContent)
      };
  }
  ...
}

This matches the MCP spec's intent — the spec recommends mirroring structuredContent into content as text precisely so clients that don't read structuredContent still get the data — and matches Claude.ai desktop's behavior.

A more conservative alternative: keep the current structuredContent path, but also extract image blocks from H.content when present and append them to the result, so the model still receives them. The straight precedence swap above is simpler and lossless.

Test plan

After the fix, run the repro above and verify:

  1. Claude Code describes the image content from get_image.
  2. The session transcript contains an "type":"image" block on the tool result.
  3. Existing tools that return structuredContent only (no content array) continue to work — the fallback path covers them.

A regression test would assert that for any MCP CallToolResult containing both content and structuredContent, every block in content (including images) is preserved on the conversation message that gets sent to the model.

Workaround (server side)

Until this lands, server authors can omit structuredContent whenever the result has images. With the Go MCP SDK this requires erasing the typed Out to any at the handler boundary so the SDK's auto-populate step is skipped.

This forces every server author to learn a Claude-Code-specific quirk and trade away outputSchema for image support — it shouldn't be the long-term answer.

extent analysis

TL;DR

The issue can be fixed by swapping the precedence of content and structuredContent in the lF5 function to prioritize content when both are present.

Guidance

  • Identify the lF5 function in the Claude Code source and modify it to check for content before structuredContent.
  • Verify that the modified function correctly handles cases where both content and structuredContent are present.
  • Test the fix using the provided repro.py script to ensure that image content is correctly forwarded to the model.
  • Consider adding regression tests to ensure that the fix does not break existing tools that return structuredContent only.

Example

async function lF5(H, _, q, K) {
  if (H && typeof H === "object") {
    if ("toolResult" in H)
      return { content: String(H.toolResult), type: "toolResult" };
    if ("content" in H && Array.isArray(H.content)) {
      let T = (await Promise.all(H.content.map((A) => cB7(A, q, K, true)))).flat();
      return { content: T, type: "contentArray", schema: v36(mD_(T)) };
    }
    if ("structuredContent" in H && H.structuredContent !== void 0)
      return {
        content: VH(H.structuredContent),
        type: "structuredContent",
        schema: v36(H.structuredContent)
      };
  }
  ...
}

Notes

The proposed fix assumes that the lF5 function is the correct location to make the change. Additional testing and verification may be necessary to ensure that the fix does not introduce unintended side effects.

Recommendation

Apply the proposed fix to the lF5 function to prioritize content over structuredContent when both are present. This fix is simpler and lossless

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix [BUG] MCP tool results: image content blocks dropped when `structuredContent` is also present [1 comments, 2 participants]