Large MCP image results are truncated into text in rollout events, leaking unusable partial base64 into Copy as Markdown
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
Summary
When an MCP tool returns an image whose serialized CallToolResult exceeds the MCP event result size limit, Codex changes the event / rollout representation of the result from structured image content into a truncated text item.
For an MCP image result such as:
{
"content": [
{
"type": "image",
"data": "<base64 PNG>",
"mimeType": "image/png"
}
]
}
the persisted/completed MCP event can instead become approximately:
{
"content": [
{
"type": "text",
"text": "{\"content\":[{\"type\":\"image\",\"data\":\"iVBOR...<truncated>...\"}]}"
}
]
}
This loses the image content type in the event and preserves roughly 1 MiB of incomplete base64 as ordinary text.
The original MCP result still appears to be used by the actual tool/model path, so this seems primarily to be an event persistence / observability / export issue rather than an MCP execution failure.
Environment
Observed with:
- Codex Desktop
- Bundled Codex CLI:
0.153.4 - Windows
- MCP tool returning PNG screenshots
- Reproduced with
blender_official.get_screenshot_of_window_as_image
The specific MCP implementation should not be required to reproduce this; any MCP tool returning sufficiently large image content should trigger the same Codex event path.
Steps to reproduce
-
Configure an MCP server with a tool that returns an image using normal MCP image content:
{ "content": [ { "type": "image", "data": "<base64>", "mimeType": "image/png" } ] } -
Make the image large enough that the serialized
CallToolResultexceeds Codex's MCP event result byte limit.In my real-world case, screenshot calls used size limits such as:
size_limit_in_bytes = 1500000and:
size_limit_in_bytes = 1200000 -
Run the MCP tool from a Codex Desktop task.
-
Inspect the rollout JSONL / completed
McpToolCallevent. -
Optionally use Copy as Markdown on the same conversation.
Actual behavior
For oversized MCP image results, the completed MCP event no longer contains an image content item.
Instead, the complete CallToolResult is serialized to JSON, truncated, and placed inside a text content item.
In my captured trajectory I observed three such records. Each event contained approximately 1,048,600 characters of text and included a truncation marker such as:
…100355 chars truncated…
The text begins with content equivalent to:
{"content":[{"type":"image","data":"iVBOR...
Because the base64 is truncated, it is no longer a recoverable image.
However, it still consumes roughly 1 MiB in the rollout event.
There is a second side effect in Codex Desktop:
Copy as Markdown treats this event as ordinary text, because the outer content type is now text.
As a result, the copied Markdown contains the huge, incomplete base64 preview instead of a compact image placeholder.
In one real session:
- Copy-as-Markdown size: ~3.2 MB
- approximately 3.15 MB came from three of these truncated image-as-text event records
- two of those records represented the same screenshot
Expected behavior
Codex should continue limiting the size of persisted MCP event results, but truncating an image should preserve its semantic type.
For example, an oversized image event could be represented as a small structured placeholder such as:
{
"type": "image",
"mimeType": "image/png",
"omitted": true,
"reason": "event_size_limit"
}
or an equivalent event-specific representation.
Useful metadata could optionally include:
{
"mimeType": "image/png",
"originalEncodedBytes": 1148852,
"omitted": true
}
The important properties are:
- do not preserve a large partial base64 string;
- do not change an image into ordinary text;
- retain enough information for the UI/exporter to know that the MCP result contained an image;
- allow Copy as Markdown to render a compact placeholder such as
Image output omitted: image/png.
Source-level cause
The behavior appears to come from:
codex-rs/core/src/mcp_tool_call.rs
specifically:
truncate_mcp_tool_result_for_event()
MCP_TOOL_CALL_EVENT_RESULT_MAX_BYTES is based on DEFAULT_OUTPUT_BYTES_CAP.
When the serialized CallToolResult exceeds that limit, the function:
- serializes the entire structured result;
- truncates the serialized string;
- replaces the result content with one
type: "text"item; - removes
structured_contentand metadata.
The truncated copy is then passed to:
notify_mcp_tool_call_completed(...)
while the subsequent HandledMcpToolCall still returns the original result.
So the size protection itself makes sense, but for multimodal results the fallback destroys the content-type information in the event.
There is also an existing app-server test:
mcp_tool_call_completion_notification_contains_truncated_large_result
which explicitly expects a large MCP completion result to be represented as text.
The current test appears to cover generic large output, but not the semantic consequences for MCP image content.
Why this matters
This causes several observable problems even if the original image is still delivered correctly to the agent:
-
Rollout fidelity
The persisted trajectory says the MCP tool returned text even though the actual MCP result contained an image.
-
Storage inefficiency
Codex keeps close to 1 MiB of partial base64 which is no longer usable as an image.
-
Copy as Markdown pollution
Since the event is typed as text, the exporter copies the partial base64 literally.
-
Trajectory analysis
Tools consuming Codex JSONL must parse JSON embedded inside text to infer that the original result was an image.
-
Replay/debugging ambiguity
A persisted event no longer accurately represents the modality returned by the MCP server.
Suggested fix
Special-case multimodal content when constructing an oversized MCP event preview.
For type: "image" or other binary/multimodal blocks:
- omit the payload itself;
- preserve the content type and MIME type;
- optionally record the original size and/or a digest;
- use text truncation only for genuinely textual content.
For mixed results, Codex could preserve small text blocks and replace only oversized binary/image blocks with structured omission markers.
It would also be useful to add a test specifically for an oversized MCP image result, verifying that:
- the original tool result remains available to the agent;
- the completion event remains bounded in size;
- no partial base64 is persisted;
- image modality remains identifiable;
- Copy as Markdown does not emit the partial image payload.
Related issues
- #22894 — Copy as Markdown embeds image/base64 content and can produce extremely large Markdown.
- #14466 — MCP tool results are truncated inside Codex even when the upstream MCP server returned the complete payload.
This issue is more specific: it concerns the current oversized MCP event fallback changing structured image content into truncated text, and the resulting persistence/export behavior.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in codex-rs/core/src/mcp_tool_call.rs at truncate_mcp_tool_result_for_event(), then read the app-server test mcp_tool_call_completion_notification_contains_truncated_large_result. Reproduce the oversized image-result path and preserve image modality without partial base64 in the bounded completion event. Done means the original result remains available, the event stays bounded, and Copy as Markdown does not emit the truncated payload.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100