From 546b25be88811e6c7c240345d18cb4ce5ce0eb13 Mon Sep 17 00:00:00 2001 From: 4444jPPP Date: Fri, 27 Mar 2026 14:29:26 -0400 Subject: [PATCH] fix: accept single supported content type in SSE mode Accept header validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relax the Accept header validation for SSE-mode POST requests from requiring both application/json AND text/event-stream to requiring at least one. This restores compatibility with clients that send only Accept: text/event-stream (e.g. Anthropic's MCP proxy used by Claude.ai for remote MCP integrations). The MCP spec uses SHOULD (not MUST) for clients accepting both content types. The server already negotiates the response format based on the message type — notifications/responses get JSON 202s, requests get SSE streams — so requiring both types in the Accept header is stricter than necessary. Closes #2349 --- src/mcp/server/streamable_http.py | 6 ++--- ...est_1363_race_condition_streamable_http.py | 16 ++++++------ tests/shared/test_streamable_http.py | 25 +++++++++++++++++-- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index aa99e7c88..43ec05fdd 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -426,10 +426,10 @@ async def _validate_accept_header(self, request: Request, scope: Scope, send: Se ) await response(scope, request.receive, send) return False - # For SSE responses, require both content types - elif not (has_json and has_sse): + # For SSE responses, require at least one supported content type + elif not (has_json or has_sse): response = self._create_error_response( - "Not Acceptable: Client must accept both application/json and text/event-stream", + "Not Acceptable: Client must accept application/json or text/event-stream", HTTPStatus.NOT_ACCEPTABLE, ) await response(scope, request.receive, send) diff --git a/tests/issues/test_1363_race_condition_streamable_http.py b/tests/issues/test_1363_race_condition_streamable_http.py index db2a82d07..4d2082ac4 100644 --- a/tests/issues/test_1363_race_condition_streamable_http.py +++ b/tests/issues/test_1363_race_condition_streamable_http.py @@ -137,7 +137,7 @@ async def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFi # Suppress WARNING logs (expected validation errors) and capture ERROR logs with caplog.at_level(logging.ERROR): - # Test with missing text/event-stream in Accept header + # Test with only application/json in Accept header (valid — single supported type) async with httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://testserver", timeout=5.0 ) as client: @@ -145,14 +145,14 @@ async def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFi "/", json={"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}}, headers={ - "Accept": "application/json", # Missing text/event-stream + "Accept": "application/json", "Content-Type": "application/json", }, ) - # Should get 406 Not Acceptable due to missing text/event-stream - assert response.status_code == 406 + # Single supported Accept type is sufficient + assert response.status_code == 200 - # Test with missing application/json in Accept header + # Test with only text/event-stream in Accept header (valid — single supported type) async with httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="http://testserver", timeout=5.0 ) as client: @@ -160,12 +160,12 @@ async def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFi "/", json={"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}}, headers={ - "Accept": "text/event-stream", # Missing application/json + "Accept": "text/event-stream", "Content-Type": "application/json", }, ) - # Should get 406 Not Acceptable due to missing application/json - assert response.status_code == 406 + # Single supported Accept type is sufficient + assert response.status_code == 200 # Test with completely invalid Accept header async with httpx.AsyncClient( diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index f8ca30441..a30d262c8 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -612,8 +612,7 @@ def test_accept_header_wildcard(basic_server: None, basic_server_url: str, accep "accept_header", [ "text/html", - "application/*", - "text/*", + "image/png", ], ) def test_accept_header_incompatible(basic_server: None, basic_server_url: str, accept_header: str): @@ -630,6 +629,28 @@ def test_accept_header_incompatible(basic_server: None, basic_server_url: str, a assert "Not Acceptable" in response.text +@pytest.mark.parametrize( + "accept_header", + [ + "text/event-stream", + "application/json", + "application/*", + "text/*", + ], +) +def test_accept_header_single_type(basic_server: None, basic_server_url: str, accept_header: str): + """Test that a single supported Accept type is sufficient for SSE mode.""" + response = requests.post( + f"{basic_server_url}/mcp", + headers={ + "Accept": accept_header, + "Content-Type": "application/json", + }, + json=INIT_REQUEST, + ) + assert response.status_code == 200 + + def test_content_type_validation(basic_server: None, basic_server_url: str): """Test that Content-Type header is properly validated.""" # Test with incorrect Content-Type