openai / openai/codex

Large MCP image results are truncated into text in rollout events, leaking unusable partial base64 into Copy as Markdown

Open
#44,126 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app app-server bug mcp
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

  1. Configure an MCP server with a tool that returns an image using normal MCP image content:

    {
      "content": [
        {
          "type": "image",
          "data": "<base64>",
          "mimeType": "image/png"
        }
      ]
    }
    
  2. Make the image large enough that the serialized CallToolResult exceeds Codex's MCP event result byte limit.

    In my real-world case, screenshot calls used size limits such as:

    size_limit_in_bytes = 1500000
    

    and:

    size_limit_in_bytes = 1200000
    
  3. Run the MCP tool from a Codex Desktop task.

  4. Inspect the rollout JSONL / completed McpToolCall event.

  5. 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:

  1. serializes the entire structured result;
  2. truncates the serialized string;
  3. replaces the result content with one type: "text" item;
  4. removes structured_content and 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:

  1. Rollout fidelity

    The persisted trajectory says the MCP tool returned text even though the actual MCP result contained an image.

  2. Storage inefficiency

    Codex keeps close to 1 MiB of partial base64 which is no longer usable as an image.

  3. Copy as Markdown pollution

    Since the event is typed as text, the exporter copies the partial base64 literally.

  4. Trajectory analysis

    Tools consuming Codex JSONL must parse JSON embedded inside text to infer that the original result was an image.

  5. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.