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

Open
#37,228 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
rust
Domain
api, networking

Research direction

Start with rust-v0.1.0/codex-rs/rmcp-client/src/http_client_adapter.rs, especially StreamableHttpClientAdapter::post_message and the response body handling, then inspect codex-rs/http-client/Cargo.toml. Run the provided Brotli MCP reproduction and verify that compressed responses are decoded before JSON/SSE parsing, or that unsupported encodings produce a specific error.

Written by the indexing model from the issue text.

Description

bug CLI mcp

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.

Dominant language
Rust
Stars
125k
Forks
19.5k
Avg merge
1m
Merged PRs (30d)
1k

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from openai/codex

All issues in openai/codex

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.