Streamable HTTP server rejects a mixed-case Content-Type with 415
Maintainer antworten meist innerhalb von 1 Tag
@CRYPTONIKAV arbeitet bereits daran.
Seit 10.10.2026.
- #3675 von @CRYPTONIKAV — ohne Merge geschlossen
Bewertung
- Schwierigkeit
- 2/5
- Geschätzter Aufwand
- 1-3 Stunden
- Anfängerfreundlichkeit
- 75/100
Rechercherichtung
Beginne mit _check_content_type in src/mcp/server/streamable_http.py etwa in Zeile 534, wo der Medientyp case-sensitiv verglichen wird, und vergleiche es mit check_accept_headers in Zeile 96 derselben Datei. Reproduziere das Problem mit dem In-Process-Harness in tests/interaction/_connect.py und einem gemischt geschriebenen Content-Type. Fertig ist es, wenn Application/JSON eine 200-Antwort erhält und der Divergenzhinweis hosting:http:content-type-415 in tests/interaction/_requirements.py entsprechend aktualisiert ist.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Beschreibung
Release line
v2 (main, verified at 91941ed).
Description
StreamableHTTPServerTransport._check_content_type compares the request's Content-Type media type case-sensitively, so a POST with Content-Type: Application/JSON is answered 415 and the handshake never completes. Media types are case-insensitive per RFC 9110 §8.3.1.
Two things in the same request path already get this right, which is what makes the behaviour inconsistent rather than merely strict.
check_accept_headers (src/mcp/server/streamable_http.py:96) lowercases every Accept media type:
accept_types = [media_type.strip().split(";")[0].strip().lower() for media_type in accept_header.split(",")]
and TransportSecurityMiddleware._validate_content_type (src/mcp/server/transport_security.py:96) lowercases too:
return content_type is not None and content_type.lower().startswith("application/json")
The middleware runs first and is the lenient one, so a mixed-case header passes it and then hits the strict comparison in the transport (src/mcp/server/streamable_http.py:534):
return any(part == CONTENT_TYPE_JSON for part in content_type_parts)
This also makes a note in your own conformance table wrong. tests/interaction/_requirements.py:3136 records a divergence for hosting:http:content-type-415 saying
The transport-security middleware rejects a non-JSON Content-Type with 400 'Invalid Content-Type header' before the request reaches the transport, so the transport's own 415 path is unreachable through any public entry point.
and the call site at line 569 carries # pragma: no cover on that belief. A mixed-case Content-Type is the public entry point that reaches it.
Example Code
Against the repo's own in-process harness on main at 91941ed, after uv sync --frozen:
import pytest
from mcp_types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent
from mcp.server import Server, ServerRequestContext
from tests.interaction._connect import base_headers, initialize_body, mounted_app
pytestmark = pytest.mark.anyio
def _server() -> Server:
async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(tools=[])
async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
return CallToolResult(content=[TextContent(text="done")])
return Server("hosted", on_list_tools=list_tools, on_call_tool=call_tool)
async def test_content_type_casing() -> None:
async with mounted_app(_server()) as (http, _):
for value in ("application/json", "Application/JSON", "APPLICATION/JSON", "application/json; charset=utf-8"):
r = await http.post("/mcp", json=initialize_body(), headers=base_headers() | {"content-type": value})
print(f"{value!r:45} -> {r.status_code} {r.text[:60]!r}")
Output:
'application/json' -> 200 'event: message\r\ndata: {"jsonrpc":"2.0","id":1,"result":{"ca'
'Application/JSON' -> 415 '{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":'
'APPLICATION/JSON' -> 415 '{"jsonrpc":"2.0","id":null,"error":{"code":-32600,"message":'
'application/json; charset=utf-8' -> 200 'event: message\r\ndata: {"jsonrpc":"2.0","id":1,"result":{"ca'
The fix is to lowercase the parsed parts before comparing, the way the two siblings do.
Why it matters to me
Being straight about this, since you said that is what you use to prioritise: I do not have a client of my own that sends mixed-case, and I found this while checking the divergence notes in _requirements.py against the code. So the practical weight is not "my deployment is broken", it is that the server rejects a spec-conformant client at the handshake for a header-casing difference, and that your own conformance record currently states the opposite.
A one-line fix and a test are ready if you want a pull request. #2916 was the same fix against v1 and was closed in the backlog sweep, so I am filing the issue first as you asked there rather than opening another pull request unprompted.
Python & MCP Python SDK
Python 3.12 on macOS 27 arm64, repository main at 91941ed with uv sync --frozen.
- Vorherrschende Sprache
- Python
- Sterne
- 24.5k
- Forks
- 4k
- Ø Merge
- 15 Std. 19 Min.
- Gemergte PRs (30 T.)
- 32
Entwicklungsumgebung
- Kein Dockerfile und keine Docker-Compose-Datei
- Hat eine Pull-Request-Vorlage
- Beitragsleitfaden lesen
Erste Schritte
- Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
- Forken Sie das Repository und arbeiten Sie in einem Branch.
- Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.
Mehr aus modelcontextprotocol/python-sdk
-
enhancement
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 84/100
modelcontextprotocol/python-sdk#3673 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Bug in the MCP Apps documentation.Evtl. vergeben @Kludex hat das heute übernommen. Offendocumentation v2
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 82/100
modelcontextprotocol/python-sdk#3662 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Audio(data=b"") raises "Either path or data can be provided", while Image(data=b"") worksEvtl. vergeben @KaiyiQuan hat das vor 3 Tagen übernommen. Offenbug v1 v2
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 85/100
modelcontextprotocol/python-sdk#3656 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag
-
enhancement
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 66/100
modelcontextprotocol/python-sdk#3655 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Simple chatbot: unused Server.stdio_context attribute obscures transport ownershipEvtl. vergeben @Kludex hat das heute übernommen. Offenenhancement
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 86/100
modelcontextprotocol/python-sdk#3654 ·
Maintainer antworten meist innerhalb von 1 Tag
Alle Issues in modelcontextprotocol/python-sdk
Ähnliche Issues
-
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 83/100
PedestrianDynamics/pyFDS-Evac#766 ·
Maintainer antworten meist innerhalb von 1 Tag
-
Schwierigkeit 1/5 1-3 Stunden Anfängerfreundlichkeit 91/100
alchaincyf/nuwa-skill#86 ·
-
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 76/100
Maintainer antworten meist innerhalb von 2 Tagen
-
Docs Needs Triage
Schwierigkeit 1/5 Unter einer Stunde Anfängerfreundlichkeit 88/100
pandas-dev/pandas#71055 ·
Maintainer antworten meist innerhalb von 1 Tag
-
[Bug]: graphify reads files that git's global ignore file hidesEvtl. vergeben @smngvlkz hat das heute übernommen. Offen
Schwierigkeit 2/5 1-3 Stunden Anfängerfreundlichkeit 72/100
Graphify-Labs/graphify#4335 · 1 Kommentar ·
Maintainer antworten meist innerhalb von 1 Tag