[BUG] MCP ImageContent returned as text in tool results instead of native image blocks (10-20x token waste)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
When an MCP server returns ImageContent (e.g., a matplotlib chart from the Jupyter MCP server), Claude Code does not convert it into a native image content block for the Anthropic API. Instead, the base64 data appears to be treated as text in the tool result, consuming ~15,000-25,000 tokens per image.
The same image, when attached directly as a user message (e.g., pasting a screenshot), is processed as a native image and costs only ~1,600 tokens — roughly 10-20x less.
This makes iterative notebook workflows (where multiple charts are produced) impractical, as a notebook with 10 charts can consume 150K-250K tokens just on image data that Claude can't even interpret (it's just base64 text characters).
What Should Happen?
When an MCP tool returns ImageContent per the MCP spec:
{
"type": "image",
"data": "<base64-encoded-data>",
"mimeType": "image/png"
}
Claude Code should convert this into a native image content block in the API request:
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "<base64-encoded-data>"
}
}
This way the model receives the image as an actual image (~1,600 tokens for a typical chart) and can visually interpret it, rather than receiving a wall of base64 text (~20,000 tokens) that it cannot interpret.
Observed Behavior
In the Claude Code terminal, large MCP image results show messages like:
Error: result (62,162 characters) exceeds maximum allowed tokens. Output has been saved to ~/.claude/projects/.../tool-results/mcp-jupyter-execute_cell-XXX.txt
The base64 image data is being saved as a text file and treated as text content. Claude sees the raw base64 characters but cannot interpret them as an image.
Reproduction Steps
- Set up the Jupyter MCP server with Claude Code
- Connect to a notebook and execute a cell that produces a matplotlib chart
- Observe that the result is treated as text (base64 string), not as a native image
Impact
This affects any MCP server that returns images: Jupyter (charts/plots), Playwright (screenshots), Figma, etc. For data science workflows in particular, this makes Claude Code impractical for iterative notebook development with visualizations.
Related Closed Issues
- #14150 — Same core issue (base64 saved to JSON file instead of rendered as image). Closed by inactivity bot, not by a fix.
- #9152 — Token limit exceeded for MCP image responses. Closed as duplicate of #4002.
- #4002 — File content exceeds 25K token limit. Closed.
Claude Code Version
Latest
Platform
macOS
Additional Context
The MCP server (datalayer/jupyter-mcp-server) correctly returns ImageContent objects with type="image", mimeType="image/png", and base64 data. The issue is entirely in how Claude Code handles these objects when constructing the API request.
11 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is not a duplicate. The three issues flagged were all closed by inactivity bots, not by fixes:
ImageContenthandling was not)The underlying bug remains: MCP
ImageContentobjects are not converted into native image content blocks for the Anthropic API. Instead, the base64 data is treated as text, either dumped into the context as a string or saved to a.txtfile. This means:This is blocking a key workflow for data scientists. Without this fix, Claude Code cannot create a notebook, visually analyze the charts it produces, and iteratively adjust — which is the core data science loop. Instead, users have to manually screenshot each chart, paste it back into Claude Code, describe what's wrong, and repeat. It turns what should be an autonomous end-to-end workflow into a tedious manual process that defeats the purpose of using an AI coding assistant.
This affects MCP servers that return images (Jupyter, and based on prior issues, Playwright and Figma as well) and has been reported multiple times over several months under different issue numbers. Please don't auto-close this again.
Having a similar (probably the exact same) issue: my custom MCP server is returning, to claude code CLI inside a terminal:
{"result":[{"type":"image","data":"/9j/4AAQSkZJRgABAQAA...uaTaTRcLH/2Q==","mimeType":"image/jpeg" ... etc ...I'm not getting token limit errors (only a warning about the size consuming context), but claude hallucinates the description when I ask it to describe what it sees. It's clearly not 'seeing' the image correctly.
For comparison, gemini cli does NOT have this issue - it correctly interprets the resulting images. OpenAI's codex has a similar, but different problem: depending on the llm harness/mcp client, it might truncate the result.
Yeah are you seeing just letters and numbers for the image data? base46 encoding...
Claude Code should be able to read that and convert it to an image. But it's not, so it's a bug :/
Yeah, the most disturbing thing is that claude just wholesale makes something up.
It seems like this has to do with whether or not the tool has an output schema. With a schema, the image isn't rendered. Without a schema, it is.
Minimal working example:
<img width="860" height="689" alt="Image" src="https://github.com/user-attachments/assets/3b32c1f3-a52a-4520-8fa1-1ae9b4019e7d" />
Wow, good catch!
I don't know why an ImageContent would ever _not_ be run through image understanding.
Root Cause Analysis Update
Building on @alipatti's excellent diagnostic above, I've traced the full root cause through the MCP Python SDK source code. Sharing here so the fix can be scoped precisely.
What's happening on the wire
When a tool has a return type annotation (e.g.,
-> list[str | ImageContent]), FastMCP auto-enables structured output. The SDK then sends both fields in theCallToolResult:content:[TextContent, ImageContent]— proper content blocks with images intactstructuredContent:{"result": [{"type": "image", "data": "..."}]}— a JSON dict whereImageContenthas been flattened viamodel_dump(mode="json")The image data is already correct and available as a proper
ImageContentblock in thecontentarray. Claude Code appears to prioritizestructuredContentwhen present, discarding thecontentarray entirely — which loses the type distinction betweenImageContentand plain text.Confirmed: bug is still present
As of Claude Code v2.1.79, the bug persists. Running a matplotlib chart through the Jupyter MCP server returns:
...as a JSON dict in the tool result, not as a native image content block. Claude receives ~20,000 tokens of base64 text instead of a ~1,600 token native image. Issues #15412 and #14150 are closed but the underlying behavior has not changed.
This is a cross-client pattern
VS Code has the same bug: microsoft/vscode#290063 — "MCP: structuredContent in tool result overrides content[].text sent to model." The issue is that MCP clients are treating
structuredContentandcontentas mutually exclusive when they should be complementary —contentfor model-facing display (including images),structuredContentfor programmatic access.The fix
When processing MCP
CallToolResult, Claude Code should always extractImageContentblocks from thecontentarray for display to the model, regardless of whetherstructuredContentis also present. Thecontentarray is the model-oriented output;structuredContentis the machine-oriented output. Images should never be downgraded to text.Why this matters: data science workflows
This bug blocks a core use case: data scientists using Claude Code with Jupyter notebooks to iteratively create, view, and refine charts and analyses. The workflow should be: run a cell → Claude sees the chart → Claude suggests improvements → repeat. Instead, Claude either can't see the chart at all (base64 saved to a temp file) or hallucinates a description of an image it never actually processed. With 10 charts in a notebook, that's 150K-250K tokens of unusable base64 text.
A server-side workaround exists (
structured_output=Falseon MCP tool decorators), but the real fix belongs here in the client — the data is already correct on the wire.If you're using the Jupyter MCP Server, if you use the commit from this change here https://github.com/datalayer/jupyter-mcp-server/pull/217 rather than the released version, you'll be able to have Claude Code see the charts as images!
It's a workaround and this issue should still be fixed.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.