openclaw - 💡(How to fix) Fix [Bug]: Inbound MCP bridge drops structuredContent (e.g. Codex threadId) — agent cannot multi-turn with codex-reply

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

When OpenClaw consumes an external MCP server's tool result, toAgentToolResult in src/agents/pi-bundle-mcp-materialize.ts exposes structuredContent only as a fallback when content[] is empty, so MCP servers (e.g. OpenAI's codex mcp-server) that return content AND structuredContent lose the structured fields entirely from the agent's view — including required identifiers like threadId that the agent needs to make subsequent tool calls.

Root Cause

When OpenClaw consumes an external MCP server's tool result, toAgentToolResult in src/agents/pi-bundle-mcp-materialize.ts exposes structuredContent only as a fallback when content[] is empty, so MCP servers (e.g. OpenAI's codex mcp-server) that return content AND structuredContent lose the structured fields entirely from the agent's view — including required identifiers like threadId that the agent needs to make subsequent tool calls.

Fix Action

Fix / Workaround

  1. OpenClaw 2026.5.5, configure the OpenAI Codex CLI (codex-cli 0.134.0) as an MCP server in ~/.openclaw/openclaw.json:
    "mcp": { "servers": { "codex": { "command": "/path/to/codex", "args": ["mcp-server"] } } }
  2. From an agent session, call the codex__codex tool with a simple prompt and sandbox: "read-only", approval-policy: "never".
  3. The MCP server's raw JSON-RPC response (verified by piping directly to codex mcp-server) contains:
    {
      "content": [{"type":"text","text":"pong"}],
      "structuredContent": {
        "threadId": "019e6cdb-8e7f-7cb2-891f-9edb689f6fc7",
        "content": "pong"
      }
    }
  4. In the OpenClaw agent transcript, the agent receives only "pong". The threadId is silently dropped.
  5. Calling codex__codex-reply (which requires threadId) is therefore impossible without an out-of-band workaround (e.g. scraping ~/.codex/sessions/YYYY/MM/DD/rollout-*-<uuid>.jsonl filenames).
  • openai/codex#13641"If a client loses a threadId... the entire conversation context is lost — forcing a costly cold start to rebuild in a new thread."
  • openai/codex#19937 — alternate mitigation request: expose CODEX_THREAD_ID env var to local stdio MCP servers.

Consequence: Multi-turn delegation to external MCP tooling requires brittle out-of-band workarounds (e.g. parsing Codex's on-disk session rollout filenames at ~/.codex/sessions/YYYY/MM/DD/rollout-*-<uuid>.jsonl). Each new MCP server with structured outputs hits the same wall.

Code Example

"mcp": { "servers": { "codex": { "command": "/path/to/codex", "args": ["mcp-server"] } } }

---

{
     "content": [{"type":"text","text":"pong"}],
     "structuredContent": {
       "threadId": "019e6cdb-8e7f-7cb2-891f-9edb689f6fc7",
       "content": "pong"
     }
   }

---

const normalizedContent = content.length > 0 ? content
  : params.result.structuredContent !== void 0
    ? [{ type: "text", text: JSON.stringify(params.result.structuredContent, null, 2) }]
    : [{ type: "text", text: JSON.stringify({ status: ..., server: ..., tool: ... }, null, 2) }];

---

{
  "name": "codex",
  "outputSchema": {
    "type": "object",
    "properties": { "threadId": {"type":"string"}, "content": {"type":"string"} },
    "required": ["threadId","content"]
  }
}

---

$ cat <<'EOF' | codex mcp-server
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"codex","arguments":{"prompt":"Say exactly: pong","sandbox":"read-only","approval-policy":"never"}}}
EOF

# final response:
{"jsonrpc":"2.0","id":2,"result":{
  "content":[{"type":"text","text":"pong"}],
  "structuredContent":{
    "threadId":"019e6cdb-8e7f-7cb2-891f-9edb689f6fc7",
    "content":"pong"
  }
}}

---

const blocks = [...content];
if (params.result.structuredContent !== undefined) {
  blocks.push({
    type: "text",
    text: "structuredContent:\n" + JSON.stringify(params.result.structuredContent, null, 2)
  });
}
const normalizedContent = blocks.length > 0 ? blocks : [/* existing empty-result fallback */];
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

When OpenClaw consumes an external MCP server's tool result, toAgentToolResult in src/agents/pi-bundle-mcp-materialize.ts exposes structuredContent only as a fallback when content[] is empty, so MCP servers (e.g. OpenAI's codex mcp-server) that return content AND structuredContent lose the structured fields entirely from the agent's view — including required identifiers like threadId that the agent needs to make subsequent tool calls.

Steps to reproduce

  1. OpenClaw 2026.5.5, configure the OpenAI Codex CLI (codex-cli 0.134.0) as an MCP server in ~/.openclaw/openclaw.json:
    "mcp": { "servers": { "codex": { "command": "/path/to/codex", "args": ["mcp-server"] } } }
  2. From an agent session, call the codex__codex tool with a simple prompt and sandbox: "read-only", approval-policy: "never".
  3. The MCP server's raw JSON-RPC response (verified by piping directly to codex mcp-server) contains:
    {
      "content": [{"type":"text","text":"pong"}],
      "structuredContent": {
        "threadId": "019e6cdb-8e7f-7cb2-891f-9edb689f6fc7",
        "content": "pong"
      }
    }
  4. In the OpenClaw agent transcript, the agent receives only "pong". The threadId is silently dropped.
  5. Calling codex__codex-reply (which requires threadId) is therefore impossible without an out-of-band workaround (e.g. scraping ~/.codex/sessions/YYYY/MM/DD/rollout-*-<uuid>.jsonl filenames).

Expected behavior

Per the Codex MCP interface docs: "For compatibility with MCP clients that prefer structuredContent, Codex mirrors the content blocks inside structuredContent alongside the threadId." The threadId is published in structuredContent specifically so MCP clients can use it to continue conversations.

OpenClaw already established this principle for its own outbound MCP tools in #77024 / #57461 (commits cf40284544, fa689295c6), citing: "upstream MCP 2025-06-18 tools spec says structured results should also return serialized JSON in a TextContent block for backwards compatibility." The same principle should apply on the inbound path: when consuming an external MCP server's structuredContent, the bridge should surface its data to the agent — either by appending a JSON text block when structuredContent has fields not present in content, or by always exposing structuredContent as a separate text block.

Actual behavior

In src/agents/pi-bundle-mcp-materialize.ts → toAgentToolResult:

const normalizedContent = content.length > 0 ? content
  : params.result.structuredContent !== void 0
    ? [{ type: "text", text: JSON.stringify(params.result.structuredContent, null, 2) }]
    : [{ type: "text", text: JSON.stringify({ status: ..., server: ..., tool: ... }, null, 2) }];

The structuredContent fallback only fires when content[] is empty. Codex always populates content[], so structuredContent.threadId is stored in details.structuredContent (visible to OpenClaw internals) but never reaches the agent model's tool result. The agent has no way to read it.

OpenClaw version

2026.5.5

Operating system

macOS 26.4.1 (Darwin 25.4.0, arm64)

Install method

npm global (/Users/hugosmith/.nvm/versions/node/v24.14.1/lib/node_modules/openclaw)

Model

anthropic/claude-opus-4-7 (consumer); openai/gpt-5.5 (Codex server side)

Provider / routing chain

OpenClaw agent → bundle-mcp materialize bridge → codex mcp-server (stdio JSON-RPC) → Codex App Server

Additional provider/model setup details

Codex MCP server tool schema declares outputSchema: { properties: { threadId: string, content: string }, required: ["threadId","content"] } for both codex and codex-reply — the threadId is contractually required output.

Raw tools/list output (relevant tools, abbreviated):

{
  "name": "codex",
  "outputSchema": {
    "type": "object",
    "properties": { "threadId": {"type":"string"}, "content": {"type":"string"} },
    "required": ["threadId","content"]
  }
}

Direct repro against the MCP server (no OpenClaw involvement) confirms structuredContent.threadId is in the response — issue is strictly in OpenClaw's translation layer.

Logs, screenshots, and evidence

Direct JSON-RPC probe (using only codex mcp-server, no OpenClaw):

$ cat <<'EOF' | codex mcp-server
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0.1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"codex","arguments":{"prompt":"Say exactly: pong","sandbox":"read-only","approval-policy":"never"}}}
EOF

# final response:
{"jsonrpc":"2.0","id":2,"result":{
  "content":[{"type":"text","text":"pong"}],
  "structuredContent":{
    "threadId":"019e6cdb-8e7f-7cb2-891f-9edb689f6fc7",
    "content":"pong"
  }
}}

OpenClaw agent view of the same call: only "pong". threadId is in details.structuredContent (per toAgentToolResult) but not in content[], so it never reaches the model.

Related upstream issues showing this is a known cross-client pain point:

  • openai/codex#13641"If a client loses a threadId... the entire conversation context is lost — forcing a costly cold start to rebuild in a new thread."
  • openai/codex#19937 — alternate mitigation request: expose CODEX_THREAD_ID env var to local stdio MCP servers.

OpenClaw precedent for surfacing structuredContent:

  • #77024 — fixed in cf40284544: "conversations_list and messages_read now include serialized conversation/message payloads in primary text content while preserving structuredContent for clients that consume it."
  • #57461 — same theme: "The full result data should be included in content (e.g. as a JSON text block) so that standard MCP clients can use it."

Impact and severity

Affected: any OpenClaw agent that wants multi-turn conversations with an external MCP server returning identifiers in structuredContent (Codex codex + codex-reply, and any other MCP server following the MCP 2025-06-18 structured-output pattern).

Severity: Blocks the intended use case. codex-reply requires threadId, which is unavailable. The agent can run one-shot Codex tasks but cannot iterate — defeating much of the value of having Codex available as a delegated coder.

Frequency: 100% reproducible — affects every call.

Consequence: Multi-turn delegation to external MCP tooling requires brittle out-of-band workarounds (e.g. parsing Codex's on-disk session rollout filenames at ~/.codex/sessions/YYYY/MM/DD/rollout-*-<uuid>.jsonl). Each new MCP server with structured outputs hits the same wall.

Additional information

Suggested fix (mirror of #77024 / #57461 on the inbound path): In toAgentToolResult, when structuredContent is present, append a serialized JSON text block to normalizedContent rather than only using it as a fallback. Pseudocode:

const blocks = [...content];
if (params.result.structuredContent !== undefined) {
  blocks.push({
    type: "text",
    text: "structuredContent:\n" + JSON.stringify(params.result.structuredContent, null, 2)
  });
}
const normalizedContent = blocks.length > 0 ? blocks : [/* existing empty-result fallback */];

This preserves details.structuredContent for internal consumers while making the structured fields readable by the agent model — matching the contract that MCP servers like Codex rely on.

Workaround until fixed: after calling codex__codex, parse the newest ~/.codex/sessions/YYYY/MM/DD/rollout-*-<uuid>.jsonl filename (Codex names rollout files using the threadId itself), then pass that UUID to codex__codex-reply. Verified working but fragile (assumes filesystem layout, races on concurrent sessions).

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Per the Codex MCP interface docs: "For compatibility with MCP clients that prefer structuredContent, Codex mirrors the content blocks inside structuredContent alongside the threadId." The threadId is published in structuredContent specifically so MCP clients can use it to continue conversations.

OpenClaw already established this principle for its own outbound MCP tools in #77024 / #57461 (commits cf40284544, fa689295c6), citing: "upstream MCP 2025-06-18 tools spec says structured results should also return serialized JSON in a TextContent block for backwards compatibility." The same principle should apply on the inbound path: when consuming an external MCP server's structuredContent, the bridge should surface its data to the agent — either by appending a JSON text block when structuredContent has fields not present in content, or by always exposing structuredContent as a separate text block.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING