MCP tools/call fails with Unexpected response type when valid tool result decodes as CustomResult
What version of Codex CLI is running?
codex-cli 0.141.0
What subscription do you have?
ChatGPT Pro. This local repro is running Codex CLI with an Amazon Bedrock model provider.
Which model were you using?
gpt-5.5
What platform is your computer?
Linux 6.12.90-120.164.amzn2023.x86_64 x86_64 x86_64
What terminal emulator and version are you using (if applicable)?
zsh under zellij 0.44.2 over SSH. The noninteractive repro environment reports TERM=dumb.
Codex doctor report
Redacted summary from codex doctor --json:
{
"overallStatus": "fail",
"codexVersion": "0.141.0",
"checks": {
"config.load": {
"status": "ok",
"summary": "config loaded",
"details": {
"mcp servers": "3",
"model": "openai.gpt-5.5",
"model provider": "amazon-bedrock"
}
},
"installation": {
"status": "ok",
"summary": "installation looks consistent"
},
"mcp.config": {
"status": "ok",
"summary": "MCP configuration is locally consistent",
"details": {
"configured servers": "3",
"disabled servers": "0",
"stdio servers": "3"
}
},
"runtime.provenance": {
"status": "ok",
"summary": "running standalone on linux-x86_64",
"details": {
"platform": "linux-x86_64",
"version": "0.141.0"
}
},
"terminal.env": {
"status": "fail",
"summary": "TERM=dumb - colors and cursor control are disabled"
},
"updates.status": {
"status": "warning",
"summary": "update configuration is locally consistent"
}
}
}
What issue are you seeing?
Codex can initialize a stdio MCP server and discover tools, but every tools/call fails with:
Unexpected response type
This was observed with an internal stdio MCP server. The same MCP server and same tools work through another MCP client, which suggests the server response is valid MCP and this is a Codex client-side result handling issue. If this were a tool-level auth, permissions, or application error, the server would return a normal MCP tool result containing that error payload. Instead Codex fails below tool logic while decoding/dispatching the JSON-RPC result.
Tracing showed that rmcp successfully parsed the JSON-RPC response, but represented the tools/call result as ServerResult::CustomResult(...) rather than ServerResult::CallToolResult(...):
PeerMessage(Response(... result: CustomResult(... redacted valid tools/call payload ...)))
...
Unexpected response type
The redacted custom result payload has the expected MCP CallToolResult shape, including a content array of text content blocks. Codex currently accepts only ServerResult::CallToolResult in rmcp-client, so a valid tool-call payload can be rejected solely because rmcp decoded it through the extensibility/custom-result variant.
What steps can reproduce the bug?
- Configure Codex CLI 0.141.0 with a stdio MCP server that returns a normal
tools/callJSON-RPC result shape but is decoded byrmcpasServerResult::CustomResult. - Call any tool exposed by that server.
- Observe that initialization and discovery succeed, but the tool call fails with
Unexpected response type.
Observed repro details:
- Transport: stdio MCP
- Server details: redacted internal MCP server
initialize,tools/list, and other discovery paths succeedtools/callfails identically across tools- Raw trace shows the response decoded as
ServerResult::CustomResult(... redacted valid tools/call payload ...)
What is the expected behavior?
For a tools/call request, Codex should accept a ServerResult::CustomResult if and only if its payload cleanly deserializes as an MCP CallToolResult.
It should still reject unrelated custom result shapes for tools/call.
Additional information
I have a tested local fix that keeps this narrow:
- Preserve the existing
ServerResult::CallToolResultpath. - Add a coercion path for
ServerResult::CustomResultthat attempts to deserialize the custom payload intoCallToolResult. - Return
UnexpectedResponsefor non-tool custom shapes or any other response variant. - Add unit coverage for:
- Custom result with a valid
CallToolResultshape - Normal
CallToolResult - Invalid custom result that should still be rejected
Patch branch is available here:
https://github.com/ytsssun/codex/tree/fix/mcp-call-tool-custom-result
Local commit:
4a010370335e0ce30100164c6dff616bd74639c2 Handle custom MCP tool call results
Verification run against the patch:
cargo test -p codex-rmcp-client coerce_call_tool_result -- --nocapture
# 3 passed
cargo test -p codex-rmcp-client --no-fail-fast
# all passed
I also validated the patched Codex CLI against the same raw internal MCP binary with no compatibility wrapper. The previously failing tool call returned a normal redacted JSON tool result successfully.
This is related to MCP parse/interop issues, but it appears distinct from #25451: this case is stdio tools/call, and the JSON-RPC frame is already decoded into an rmcp::model::ServerResult; Codex then rejects the CustomResult variant as an unexpected response type.
6 Comments
I'm also running into possibly related challenges:
The Angular MCP server works: a direct SDK call to get_best_practices returns the best-practices text successfully.
Codex’s MCP adapter rejects the response with Unexpected response type.
The likely incompatibility is MCP content[].annotations: the rejected Angular tools include annotations, while list_projects does not and works.
[ChatGPT Business]
I have had this issue for some time now too, the get_best_practices do not work inside codex, but works if using for example
@modelcontextprotocol/inspector. It is kinda annoying, since every time I use codex inside my angular repos, it tries to use this and reports a failure.I can confirm a very similar repro on a newer Codex CLI version with a different internal stdio MCP server.
Environment:
0.144.1.266 (stable)Observed through Codex CLI:
Unexpected response typeThe health-check failure is useful because it suggests this is not caused by a large Slack message payload.
Cross-check outside Codex:
I called the same stdio MCP server directly over JSON-RPC with:
initializenotifications/initializedtools/callDirect
tools/callsucceeds for all tested tools, including the health check.Sanitized direct response shape summary:
For one of the message-related tools, the direct response had:
This looks consistent with the MCP tool result shape:
contentcontains a text content item,structuredContentmay be present, and content annotations appear to be allowed by the MCP spec.One additional observation: tool error paths from the same server can return through Codex as structured JSON in some cases, so the server is reachable and this seems specific to successful
tools/callresults.This aligns with the hypothesis in this issue: Codex/rmcp appears to reject a valid-looking
tools/callresult because it is decoded through a response variant that Codex does not currently accept. It does not look like a model-provider or inference issue, since the failure happens before the model can consume the tool output.Leaving this comment in case it helps anyone
As this issue is a month old and yet to be resolved, I asked Codex to author a proxy Slack MCP server that transforms the upstream MCP server's response to a format acceptable to codex https://gist.github.com/nisanthchunduru/e9a462230c682d4bfee7e9b5c61b1b1e
Independent reproduction, with what I think is a decisive data point: the same response bytes succeed on one path and fail on the other.
Setup
A third-party MCP server (Streamable HTTP, protocol
2025-06-18, JSON response mode) exposing Outlook/OneDrive tools. Reached two ways:clientInfo.name=claude-code,Anthropic/ClaudeAI) — works.openai-mcp/openai-mcp (Codex)and are dispatched asmcp__codex_apps__<connector>_<tool>— fails withUnexpected response typeon every tool call.The decisive comparison
Same mailbox, same tool, same arguments, same server revision, ~12 minutes apart. Byte counts are from the server's own outbound logging:
| Client | Path | Response bytes | Keys on the result | Outcome |
|---|---|---|---|---|
|
claude-code| direct | 13179 |content,isError,_meta| success, full result rendered ||
openai-mcp (Codex)| viacodex_apps| 13179 |content,isError,_meta|Unexpected response type|Identical byte count, identical key set, identical content-block count and types. Both logged as a well-formed
CallToolResultagainst the correct request id, with no JSON-RPC error.Ruled out server-side
Each of these was tested against a live failing call, not reasoned about:
ServerResultis an untagged union ending inCustomResult(Value), so an unrecognised result silently deserialises as the wrong variant. We shipped all four recognisedCallToolResultfields (content,structuredContent,isError,_meta) — still failed.JSON.stringifyemits a lone surrogate as a\udXXXescape thatserde_jsonrejects outright. We instrumented a counter:0on every failing call.call_toolpath:ServerResult variant deserialized: CallToolResult, parsed fine.application/jsonfail identically.Why this points at the aggregator
codex_appsis the only element present on the failing path and absent on the working one. Since it re-serialises connector results before the client parses them, a result that leaves our socket as a validCallToolResultcan reach rmcp as something else — and rmcp's untagged union turns that into the opaqueUnexpected response typerather than a parse error naming the problem.What would help
codex_appslog what it received vs. what it forwarded? That single diff would settle this.Unexpected response typeis indistinguishable across "field dropped", "shadowing key added", and "content block rewritten". Logging the variant actually deserialised, or the first ~200 bytes of the unparsed result, would make this self-service to debug.Worth noting for anyone else landing here: no server-side change fixes this. We shipped three, on three plausible mechanisms, and all three were disproven by retest. If your server is emitting a valid
CallToolResult— verify that by feeding your real bytes to rmcp directly — the problem is not on your side.Fixed on our side. Root cause is
annotationson tool-result content blocks.Posting the mechanism and the fix since this issue is still open and the workaround above deserves an explanation of why it works.
TL;DR
If your MCP server sets
annotationson content blocks, every tool call through the ChatGPT connector fails withUnexpected response type. Strip them and it works immediately.That's the whole fix. @nisanthchunduru found this empirically a month ago with a proxy — it's the correct fix, and here is the reason.
Why it breaks
annotations.audiencedeserializes into a closed role enum in rmcp. A value outside that enum doesn't degrade gracefully: the entireCallToolResultfails to deserialize and falls through toServerResult's catch-allCustomResult(Value)variant, which codex reports asUnexpected response type.Verified against rmcp 3.1.3:
| Result on the wire | rmcp variant | Outcome |
|---|---|---|
|
annotations: {audience:["user","assistant"]}|CallToolResult| OK ||
annotations: {audience:["assistant"],priority:1.0}|CallToolResult| OK ||
annotations: {audience:["system"]}(unknown role) |CustomResult|Unexpected response type|| no
annotations|CallToolResult| OK |The catch: valid annotations parse fine as you emit them. We confirmed that our exact annotations deserialize correctly in rmcp — the corruption happens in transit. ChatGPT doesn't connect to a connector directly; it routes tool calls through an OpenAI-hosted aggregator MCP server (
codex_apps), which re-serializes the result before their client parses it. Something in that hop reshapes annotations into a form rmcp rejects.A server that sets no annotations has nothing to mangle, which is why this affects so few people — and why it looks like a mystery when it hits you.
How to tell if this is you
annotationson content blocks? Grep for it. If yes, this is almost certainly your problem.Evidence
Same tool, same server, same data. The only difference is the strip:
| Response bytes | annotations stripped | Outcome |
|---|---|---|
| 7501 | no |
Unexpected response type|| 13179 | no |
Unexpected response type|| 20215 | no |
Unexpected response type|| 7399 | yes (2 blocks) | success |
| 12607 | yes (2 blocks) | success |
Note 7,501 bytes failing against 7,399 succeeding — near-identical sizes, opposite outcomes.
We also confirmed, before finding this, that a byte-identical response succeeds when delivered directly and fails through
codex_apps: 13,179 bytes both times, same server, same revision, minutes apart. The payload leaving the server is a validCallToolResult— we fed our real production bytes (68 KB) to a real rmcp 3.1.3 client running the samecall_toolpath and it parsed cleanly.Don't waste time on these
We shipped three fixes before finding it. All wrong, all disproven by instrumenting a live failing call:
CallToolResult." We shipped all four recognized fields (content,structuredContent,isError,_meta). Still failed. Worth knowing: redundant fields only help ifcontentis absent — ifcontentis present but unparseable, the deserializer fails as a whole regardless.JSON.stringifyemits\udXXX, whichserde_jsonrejects) but not this bug — we instrumented a counter and it read0on every failing call.If you're debugging this: feed your actual response bytes to a real rmcp client before changing anything. If they parse, the problem isn't your payload and no server-side reshaping will help — except removing annotations.
Ask for maintainers
codex_appslog what it received vs. what it forwarded? That diff would confirm the reshaping directly.Unexpected response typeis indistinguishable across "field dropped", "shadowing key added", and "annotation rewritten", because the union terminates in a catch-all. Logging the variant actually deserialized — or the first ~200 bytes of the unparsed result — would make this self-service instead of a multi-day hunt.