MCP server never emits TurnComplete event - only threadId returned to caller
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
- Start Codex as an MCP server:
``bash``
codex mcp-server
- Call the
codextool with any prompt via an MCP client (e.g., Claude Code)
- Observe the response contains only:
``json``
{"threadId": "019bbed6-1e9e-7f31-984c-a05b65045719"}
- Check session logs at
~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl- theagent_messagewith the actual response is present, but noturn_completeevent 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
- 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;
}
- TurnComplete is only emitted from
Session::on_task_finished()incodex-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;
}
on_task_finishedshould be called fromspawn_task()closure aftertask_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
TurnCompletecan 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.
8 Comments
This is caused by merging https://github.com/openai/codex/pull/9192 yesterday, which added the
structuredContentfield alongsidecontent.Both Claude Code and Codex as MCP clients ignore the
contentfield ifstructuredContentis returned. The fix for this is to include the text content instructuredContent.See: https://github.com/openai/codex/pull/2594 and https://github.com/anthropics/claude-code/issues/9962.
@bolinfest pinging you since it was your PR :)
so there should be a code change and codex release then? got it.
Codex came up with this: https://github.com/openai/codex/issues/9251
@net hmm,
structuredContentshould havethreadIdandcontext, but nottext, I think?@bolinfest I don't think from the perspective of the MCP spec it matters what the
textfield instructuredContentis named; just give it a name that makes semantic sense. The main thing is thatcontentandstructuredContentshould 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
structuredContentfield as serialized JSON in thecontentfield:@bolinfest thank you!
thank you both.