Skip to content

Streamable HTTP: Content-Type handling is order dependent and accepts invalid values #3691

Open

Description

@aabidk20

Initial Checks

Release line

2.x (current stable)

Description

The Content-Type header:

  • accepts values that are not JSON media types. application/json, text/plain is accepted and the request is executed, although a comma cannot appear inside a media type. On the modern transport application/jsonx is accepted as well
  • The answer depends on order. application/json, text/plain is accepted while text/plain, application/json is rejected, so the same invalid value gets opposite answers.

The first check is a prefix test in the middleware:

# src/mcp/server/transport_security.py
return content_type is not None and content_type.lower().startswith("application/json")

The second splits the value on ; and , and compares the result:

# src/mcp/server/streamable_http.py
content_type_parts = [part.strip() for part in content_type.split(";")[0].split(",")]
return any(part == CONTENT_TYPE_JSON for part in content_type_parts)

The second check rejects application/jsonx. The 2.x modern transport has no equivalent, so it accepts that value.

For reference, the TypeScript SDK answers 415 for application/json, text/plain and for application/jsonx, and mcp-go answers 400 for both.

Reproduction

Against a server started with stateless_http=True, so no session handshake is needed:

curl -s http://127.0.0.1:8901/mcp \
  -H 'Content-Type: application/json, text/plain' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ping","arguments":{}}}'

Observed: 200 with a normal JSON-RPC result, and the tool's handler runs.

Content-Type sent Expected handshake transport (1.x and 2.x) modern transport (2.x only)
application/json accepted 200 200
application/json; charset=utf-8 accepted 200 200
application/json, text/plain rejected 200, body executed 200, body executed
application/jsonx rejected 415 200, body executed
text/plain, application/json rejected 400 400
text/plain rejected 400 400

For modern transport calls were sent with MCP-Protocol-Version: 2026-07-28 plus the Mcp-Method and Mcp-Name headers.

Fix

Parse the media type, the segment before the first ;, and compare it exactly to application/json. Parameters keep working, so application/json; charset=utf-8 is unaffected, while the values above are rejected.

Python & MCP Python SDK

Reproduced on the 2.x line (`2.2.0`, `2.3.0`) and the 1.x line (`1.30.0`)

Activity

  1. added
    bugSomething isn't working
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Oct 12, 2026
  2. jayzuccarelli commented on Oct 12, 2026

    @jayzuccarelli

    Reproduced on main: the POST Content-Type check lets application/json, text/plain and application/jsonx through, while text/plain, application/json gets a 400. The fix I'd make follows the issue: compare only the media type before the first ; against application/json in both checks, keeping the case-insensitive match the middleware already has, so application/json; charset=utf-8 still works. Regression test adds application/jsonx and both comma orderings to the existing Content-Type cases in tests/server/test_transport_security.py. Disclosure: drafted with Claude Code. Could a maintainer assign this to me?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions