MCP server never emits TurnComplete event - only threadId returned to caller

Resolved 💬 8 comments Opened Jan 15, 2026 by ryuan-cw Closed Jan 16, 2026
💡 Likely answer: A maintainer (bolinfest, collaborator) responded on this thread — see the highlighted reply below.

Bug Description

When using the Codex MCP server (codex mcp-server), tool calls return only a threadId in the response instead of the actual GPT response text. Session logs confirm GPT responds correctly with agent_message events, but the TurnComplete event that would surface this response is never emitted.

Steps to Reproduce

  1. Start Codex as an MCP server:

``bash
codex mcp-server
``

  1. Call the codex tool with any prompt via an MCP client (e.g., Claude Code)
  1. Observe the response contains only:

``json
{"threadId": "019bbed6-1e9e-7f31-984c-a05b65045719"}
``

  1. Check session logs at ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl - the agent_message with the actual response is present, but no turn_complete event follows.

Expected Behavior

The MCP tool call should return the GPT response text along with the threadId:

{
  "content": [{"type": "text", "text": "GPT's response here"}],
  "structured_content": {"threadId": "..."}
}

Actual Behavior

Only the threadId is returned. The response text is lost.

Technical Analysis

Event Flow

MCP Client → codex mcp-server → Codex Core → GPT API
                    ↓
         codex_tool_runner.rs waits for TurnComplete
                    ↓
         (event never arrives, only threadId returned)

Code Path

  1. MCP server correctly waits for TurnComplete in codex-rs/mcp-server/src/codex_tool_runner.rs:

``rust
EventMsg::TurnComplete(TurnCompleteEvent { last_agent_message }) => {
let text = match last_agent_message { ... };
let result = create_call_tool_result_with_thread_id(thread_id, text, None);
outgoing.send_response(request_id.clone(), result).await;
break;
}
``

  1. TurnComplete is only emitted from Session::on_task_finished() in codex-rs/core/src/tasks/mod.rs:

``rust
pub async fn on_task_finished(...) {
let event = EventMsg::TurnComplete(TurnCompleteEvent { last_agent_message });
self.send_event(turn_context.as_ref(), event).await;
}
``

  1. on_task_finished should be called from spawn_task() closure after task_for_run.run() completes

Session Log Evidence

The session log shows the conversation completes successfully but ends without turn_complete:

{"timestamp":"2026-01-14T17:18:29.861Z","event_msg":{"agent_message":"Hello from GPT"}}
{"timestamp":"2026-01-14T17:18:29.861Z","response_item":{"type":"message"}}
{"timestamp":"2026-01-14T17:18:29.908Z","event_msg":{"token_count":{...}}}
[END - no turn_complete event]

Hypothesis

The task lifecycle in MCP server mode does not properly trigger on_task_finished(). Possible causes:

  • The task isn't being registered via spawn_task() in MCP mode
  • The task completion callback is being skipped
  • There's a race condition where the event channel closes before TurnComplete can be emitted

Environment

  • OS: macOS Darwin 25.2.0
  • Codex version: 0.81.0
  • MCP client: Claude Code

Workaround

Currently, the only workaround is to manually parse session log files to extract agent_message content, which defeats the purpose of the MCP integration.

Impact

This bug makes the Codex MCP server unusable for any application expecting to receive GPT responses, as only session IDs are returned instead of actual content.

View original on GitHub ↗

8 Comments

net · 6 months ago

This is caused by merging https://github.com/openai/codex/pull/9192 yesterday, which added the structuredContent field alongside content.

{
  "content": [
    {
      "type": "text",
      "text": "`ls -lah` (or `ls -alh`) — long listing, includes dotfiles, human-readable sizes."
    }
  ],
  "structuredContent": {
    "threadId": "019bbb20-bff6-7130-83aa-bf45ab33250e"
  }
}

Both Claude Code and Codex as MCP clients ignore the content field if structuredContent is returned. The fix for this is to include the text content in structuredContent.

See: https://github.com/openai/codex/pull/2594 and https://github.com/anthropics/claude-code/issues/9962.

net · 6 months ago

@bolinfest pinging you since it was your PR :)

ryuan-cw · 6 months ago

so there should be a code change and codex release then? got it.

bolinfest collaborator · 6 months ago
bolinfest collaborator · 6 months ago

@net hmm, structuredContent should have threadId and context, but not text, I think?

net · 6 months ago

@bolinfest I don't think from the perspective of the MCP spec it matters what the text field in structuredContent is named; just give it a name that makes semantic sense. The main thing is that content and structuredContent should be treated as either one or the other is read, not both, and so should almost always have the same content (just in different forms).

For maximum backwards compatibility the spec suggests encoding the structuredContent field as serialized JSON in the content field:

  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"temperature\": 22.5, \"conditions\": \"Partly cloudy\", \"humidity\": 65}"
      }
    ],
    "structuredContent": {
      "temperature": 22.5,
      "conditions": "Partly cloudy",
      "humidity": 65
    }
  }
For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.
net · 6 months ago

@bolinfest thank you!

ryuan-cw · 6 months ago

thank you both.