MCP transport silently swallows non-2xx HTTP errors as JSON-RPC deserialization failures

Open 💬 0 comments Opened Apr 30, 2026 by AtharvaPatil-at

What version of Codex CLI is running?

0.125.0

What subscription do you have?

Enterprise

Which model were you using?

gpt-5.5

What platform is your computer?

Darwin 25.4.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

_No response_

What issue are you seeing?

When an MCP server returns a non-2xx HTTP response with Content-Type: application/json (e.g., a 403 Forbidden for insufficient OAuth scope), Codex reports a confusing deserialization error instead of the actual HTTP error:

Transport[rmcp::transport::worker::WorkerTransport<
   rmcp::transport::streamable_http_client::StreamableHttpClientWorker<
      codex_rmcp_client::http_client_adapter::StreamableHttpClientAdapter
   >
>] error: Deserialize error: data did not match any variant of untagged enum JsonRpcMessage

The actual server response was:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error":"insufficient_scope","error_description":"Insufficient scope"}

What steps can reproduce the bug?

  1. Configure an MCP server that returns 403 with a JSON body for unauthorized tool calls (e.g., an OAuth-protected server where the token lacks the required scope).
  2. Invoke any tool via Codex that triggers the 403.
  3. Observe that the error message mentions a JSON-RPC deserialization failure rather than the HTTP 403.

What is the expected behavior?

Codex should detect the non-2xx status code before attempting to deserialize the body as a JsonRpcMessage, and surface a clear error like:

MCP tool call failed: HTTP 403 Forbidden - {"error":"insufficient_scope","error_description":"Insufficient scope"}

Additional information

The root cause is in codex-rs/rmcp-client/src/http_client_adapter.rs in the post_message method. After handling 401, 404, 202, and 204 explicitly, the code branches directly on Content-Type without a general non-2xx guard:

https://github.com/openai/codex/blob/8426edf71e4a5b754467749ce16090515e2c13c9/codex-rs/rmcp-client/src/http_client_adapter.rs#L158-L163
(A 403 with Content-Type: application/json falls into this branch.)

The fix would be to add a non-2xx guard before the content-type branch - the same pattern already used in get_stream in the same file:

if !status.is_success() {
   let body = collect_body(&mut body_stream).await?;
   return Err(StreamableHttpError::UnexpectedServerResponse(
      format!("HTTP {}: {}", status, String::from_utf8_lossy(&body)),
   ));
}

View original on GitHub ↗