Streamable HTTP server rejects a mixed-case Content-Type with 415
メンテナーはふだん 1 日以内に返信
評価
調査の方向性
src/mcp/server/streamable_http.py の約534行目にある _check_content_type から始めてください。ここではメディアタイプが大文字小文字を区別して比較されています。同じファイルの96行目にある check_accept_headers と比較してください。大文字と小文字が混在する Content-Type を使い、tests/interaction/_connect.py のインプロセスハーネスで再現してください。Application/JSON が200レスポンスを受け取り、tests/interaction/_requirements.py の hosting:http:content-type-415 の差異メモが一致するように更新されたら完了です。
索引モデルが issue の本文から書いたものです。
説明
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.
- 主要言語
- Python
- スター
- 24.5k
- フォーク
- 4k
- 平均マージ
- 15時間 19分
- マージ済み PR(30日)
- 32
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
modelcontextprotocol/python-sdk のほかの issue
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
modelcontextprotocol/python-sdk#3673 ·
メンテナーはふだん 1 日以内に返信
-
Bug in the MCP Apps documentation.対応中かも @Kludex が今日担当しました。 オープンdocumentation v2
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
modelcontextprotocol/python-sdk#3662 ·
メンテナーはふだん 1 日以内に返信
-
Audio(data=b"") raises "Either path or data can be provided", while Image(data=b"") works対応中かも @KaiyiQuan が 3 日前に担当しました。 オープンbug v1 v2
難易度 1/5 1時間未満 初心者へのやさしさ 85/100
modelcontextprotocol/python-sdk#3656 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信
-
enhancement
難易度 2/5 1〜3時間 初心者へのやさしさ 66/100
modelcontextprotocol/python-sdk#3655 ·
メンテナーはふだん 1 日以内に返信
-
Simple chatbot: unused Server.stdio_context attribute obscures transport ownership対応中かも @Kludex が今日担当しました。 オープンenhancement
難易度 1/5 1時間未満 初心者へのやさしさ 86/100
modelcontextprotocol/python-sdk#3654 ·
メンテナーはふだん 1 日以内に返信
modelcontextprotocol/python-sdk の issue をすべて見る
似ている issue
-
needs triage
難易度 2/5 1〜3時間 初心者へのやさしさ 83/100
darnitdevorg/darnit#587 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 67/100
メンテナーはふだん 1 日以内に返信
-
accessibility good first issue
難易度 1/5 1〜3時間 初心者へのやさしさ 92/100
メンテナーはふだん 1 日以内に返信
-
llm translation
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
メンテナーはふだん 1 日以内に返信
-
bug bug-hunt
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
メンテナーはふだん 1 日以内に返信