Streamable HTTP MCP client parses Brotli-compressed JSON without decoding it

Open 💬 5 comments Opened Aug 6, 2026 by jimmyyyeh

What issue are you seeing?

The Streamable HTTP MCP client can receive a JSON response with Content-Encoding: br, pass the compressed bytes directly to Serde, and fail with a misleading JSON error:

MCP client failed to start: MCP startup failed: Transport send error: Transport
[rmcp::transport::worker::WorkerTransport<rmcp::transport::streamable_http_client::StreamableHttpClientWorker<...>>]
error: Deserialize error: expected value at line 1 column 2

This occurs when initialize is small and uncompressed, but a larger tools/list response is Brotli-compressed by an ingress or reverse proxy.

Observed with Codex CLI 0.146.1 on macOS. Decompressing the same response externally produces valid JSON, while Codex passes the encoded body to the JSON parser.

What steps can reproduce the bug?

Run a minimal stateless MCP HTTP server that returns a Brotli-compressed tools/list response:

# pip install brotli
import brotli
import json
from http.server import BaseHTTPRequestHandler, HTTPServer


class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("Content-Length", "0"))
        request = json.loads(self.rfile.read(length))
        method = request.get("method")

        if method == "notifications/initialized":
            self.send_response(202)
            self.end_headers()
            return

        if method == "initialize":
            result = {
                "protocolVersion": request["params"]["protocolVersion"],
                "capabilities": {},
                "serverInfo": {"name": "brotli-repro", "version": "1.0.0"},
            }
        elif method == "tools/list":
            result = {
                "tools": [
                    {
                        "name": f"tool_{index}",
                        "description": "x" * 1000,
                        "inputSchema": {"type": "object", "properties": {}},
                    }
                    for index in range(100)
                ]
            }
        else:
            result = {}

        body = json.dumps(
            {"jsonrpc": "2.0", "id": request.get("id"), "result": result}
        ).encode()
        encoded = brotli.compress(body) if method == "tools/list" else body

        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        if method == "tools/list":
            self.send_header("Content-Encoding", "br")
        self.send_header("Content-Length", str(len(encoded)))
        self.end_headers()
        self.wfile.write(encoded)


HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

Configure Codex:

[mcp_servers.brotli_repro]
url = "http://127.0.0.1:8765/mcp"

Start Codex. MCP initialization reaches tools/list and then fails with the deserialize error.

What is the expected behavior?

Codex should do one of the following:

  1. Advertise only supported response codings, for example Accept-Encoding: identity, when response decompression is unavailable; or
  2. Enable Brotli support in the shared Reqwest client and decode the response before passing it to the JSON/SSE parser.

If an unsupported Content-Encoding is received, the error should identify the unsupported encoding instead of reporting malformed JSON.

Additional information

Root-cause analysis

In 0.146.1, StreamableHttpClientAdapter::post_message sets Accept and Content-Type, but does not set Accept-Encoding:

https://github.com/openai/codex/blob/rust-v0.146.1/codex-rs/rmcp-client/src/http_client_adapter.rs#L97-L111

The shared HTTP client enables Reqwest features json, rustls-tls-native-roots, and stream, but not brotli:

https://github.com/openai/codex/blob/rust-v0.146.1/codex-rs/http-client/Cargo.toml#L7-L14

For an application/json response, the adapter collects the body stream and immediately calls serde_json::from_slice without inspecting Content-Encoding:

https://github.com/openai/codex/blob/rust-v0.146.1/codex-rs/rmcp-client/src/http_client_adapter.rs#L211-L220

The same behavior is still present on current main at commit 7a0e974e08c798d1e8d59d407aeb6e24db1313af.

RFC 9110 section 12.5.3 says that when Accept-Encoding is absent, the user agent considers any content coding acceptable. Therefore a server or ingress can validly choose Brotli, even though this Codex client cannot decode it:

https://datatracker.ietf.org/doc/html/rfc9110#section-12.5.3

Reqwest's Brotli response decoding is gated by its optional brotli feature:

https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.brotli

Workarounds

Client-side configuration:

[mcp_servers.example.http_headers]
Accept-Encoding = "identity"

Server-side: disable response compression for the MCP route.

View original on GitHub ↗

5 Comments

ded-furby · 22 days ago

test from automation

ded-furby · 22 days ago

Thanks for the clear repro. This looks like a transport decode gap in streamable HTTP MCP.

Could you share one low-volume capture with headers (Content-Encoding, Content-Type) plus a short raw-body excerpt, and confirm whether the endpoint is behind a proxy/CDN that could inject compression? Also include codex --version and MCP server/transport details so we can confirm whether to decode br only when explicitly encoded JSON is received.

JayYarlagadda · 22 days ago

Looking into this — root cause is that the Streamable HTTP MCP client never inspects Content-Encoding on application/json responses and hands the still-compressed bytes straight to serde_json, while the shared HTTP client (reqwest) is built without any decoding features. Planning a small, scoped fix:

  • Send Accept-Encoding: identity on MCP HTTP requests so compliant servers won't compress responses.
  • If a server ignores that and returns a Content-Encoding anyway, fail with a clear "unsupported Content-Encoding" error instead of the misleading JSON parse error.

Will open a PR shortly.

jimmyyyeh · 22 days ago

Thanks. Here is a sanitized low-volume capture from the failing tools/list response.

Codex version:

codex-cli 0.146.1

MCP server details:

  • Python FastMCP
  • Streamable HTTP transport
  • Stateless HTTP enabled
  • JSON responses enabled (json_response=True)
  • Endpoint path: POST /mcp
  • The failure occurs on tools/list; the smaller initialize response succeeds

Relevant response headers:

HTTP/2 200
Content-Type: application/json
Content-Encoding: br

I captured the wire body without automatic decompression. Its first bytes are:

5b 19 1e c1 8c d4 ...

After Brotli decompression, the body is valid JSON with the expected JSON-RPC shape:

{"jsonrpc":"2.0","id":"...","result":{"tools":[...]}}

As a control, sending:

Accept-Encoding: identity

returns the same valid JSON without Content-Encoding: br, and Codex can parse it successfully.

The production endpoint is behind a Kubernetes ingress/reverse proxy that can apply response compression. The FastMCP application itself does not configure Brotli compression, so the encoding appears to be added by the ingress layer. I have not included the endpoint or authorization headers here, but can provide further sanitized transport details if useful.

Given that the response explicitly includes both Content-Type: application/json and Content-Encoding: br, decoding could be conditional on the declared Content-Encoding before passing the body to the JSON parser. Alternatively, if the client does not support response decompression, sending Accept-Encoding: identity would prevent the server/proxy from selecting Brotli.

JayYarlagadda · 22 days ago

Dug into this and got it fixed and verified locally.

Root cause: the streamable HTTP MCP client never sends an Accept-Encoding header and never checks Content-Encoding on the response before handing the body to serde_json. The shared HTTP client also doesn't have Brotli/gzip decoding enabled, so a compressed response from a proxy/ingress goes straight to the JSON parser as raw bytes — that's why you get the confusing "expected value at line 1 column 2" error instead of anything pointing at the real issue. Your capture above lines up exactly with this (Content-Type: application/json + Content-Encoding: br, and Accept-Encoding: identity as a workaround that sidesteps it entirely).

What I changed: the client now sends Accept-Encoding: identity on MCP requests, so a compliant server/proxy won't compress the response in the first place — this is the same behavior you confirmed manually. On top of that, if a server ignores that and compresses anyway, the client now catches it immediately and fails with a clear "unsupported Content-Encoding: br" error instead of silently trying to parse garbage as JSON.

End result: tools/list (and any other MCP response) behind a compressing proxy now either works correctly, or fails with an error that actually tells you what went wrong — no more mystery JSON parse errors. Wrote a test that mocks a Brotli-compressed tools/list response and confirms the client rejects it cleanly. Ran the full test suite plus clippy for the crate, everything passes, no regressions.

Happy to open a PR for this if someone from the team wants to invite one — it's ready to go.