Skip to content

Commit 6681cf0

Browse files
committed
fix(server): match Content-Type case-insensitively in StreamableHTTP
Media types are case-insensitive (RFC 9110, section 8.3.1), and StreamableHTTPServerTransport._check_accept_headers already lowercases the Accept media types before comparing. _check_content_type did not, so a spec-valid request with a mixed/upper-case Content-Type (e.g. "Application/JSON") was rejected with 415 Unsupported Media Type. Lowercase the parsed Content-Type media type before comparing to CONTENT_TYPE_JSON, consistent with _check_accept_headers. Adds a unit test for case-insensitive matching (the _check_content_type path was previously no-cover).
1 parent 1cec2d6 commit 6681cf0

2 files changed

Lines changed: 27 additions & 1 deletion

File tree

src/mcp/server/streamable_http.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,9 @@ def _check_accept_headers(self, request: Request) -> tuple[bool, bool]:
411411
def _check_content_type(self, request: Request) -> bool:
412412
"""Check if the request has the correct Content-Type."""
413413
content_type = request.headers.get("content-type", "")
414-
content_type_parts = [part.strip() for part in content_type.split(";")[0].split(",")]
414+
# Media types are case-insensitive (RFC 9110, section 8.3.1), so normalize
415+
# to lower case before comparing — consistent with _check_accept_headers.
416+
content_type_parts = [part.strip().lower() for part in content_type.split(";")[0].split(",")]
415417

416418
return any(part == CONTENT_TYPE_JSON for part in content_type_parts)
417419

tests/shared/test_streamable_http.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,30 @@ def test_streamable_http_transport_init_validation() -> None:
597597
StreamableHTTPServerTransport(mcp_session_id="test\n")
598598

599599

600+
def test_check_content_type_is_case_insensitive() -> None:
601+
"""Content-Type media types are case-insensitive (RFC 9110, section 8.3.1).
602+
603+
A spec-valid request such as ``Content-Type: Application/JSON`` must be
604+
accepted, consistent with ``_check_accept_headers`` (which already lowercases).
605+
"""
606+
transport = StreamableHTTPServerTransport(mcp_session_id=None)
607+
608+
def request_with(content_type: str) -> Request:
609+
return Request({"type": "http", "headers": [(b"content-type", content_type.encode())]})
610+
611+
for value in (
612+
"application/json",
613+
"Application/JSON",
614+
"APPLICATION/JSON",
615+
"application/json; charset=utf-8",
616+
"Application/Json; charset=utf-8",
617+
):
618+
assert transport._check_content_type(request_with(value)) is True, value
619+
620+
# A genuinely different media type is still rejected.
621+
assert transport._check_content_type(request_with("text/plain")) is False
622+
623+
600624
@pytest.mark.anyio
601625
async def test_session_termination(basic_app: Starlette) -> None:
602626
"""DELETE terminates the session, after which requests for it return 404."""

0 commit comments

Comments
 (0)