Skip to content

Commit 546b25b

Browse files
committed
fix: accept single supported content type in SSE mode Accept header validation
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
1 parent fb2276b commit 546b25b

File tree

3 files changed

+34
-13
lines changed

3 files changed

+34
-13
lines changed

src/mcp/server/streamable_http.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,10 +426,10 @@ async def _validate_accept_header(self, request: Request, scope: Scope, send: Se
426426
)
427427
await response(scope, request.receive, send)
428428
return False
429-
# For SSE responses, require both content types
430-
elif not (has_json and has_sse):
429+
# For SSE responses, require at least one supported content type
430+
elif not (has_json or has_sse):
431431
response = self._create_error_response(
432-
"Not Acceptable: Client must accept both application/json and text/event-stream",
432+
"Not Acceptable: Client must accept application/json or text/event-stream",
433433
HTTPStatus.NOT_ACCEPTABLE,
434434
)
435435
await response(scope, request.receive, send)

tests/issues/test_1363_race_condition_streamable_http.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,35 +137,35 @@ async def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFi
137137

138138
# Suppress WARNING logs (expected validation errors) and capture ERROR logs
139139
with caplog.at_level(logging.ERROR):
140-
# Test with missing text/event-stream in Accept header
140+
# Test with only application/json in Accept header (valid — single supported type)
141141
async with httpx.AsyncClient(
142142
transport=httpx.ASGITransport(app=app), base_url="http://testserver", timeout=5.0
143143
) as client:
144144
response = await client.post(
145145
"/",
146146
json={"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}},
147147
headers={
148-
"Accept": "application/json", # Missing text/event-stream
148+
"Accept": "application/json",
149149
"Content-Type": "application/json",
150150
},
151151
)
152-
# Should get 406 Not Acceptable due to missing text/event-stream
153-
assert response.status_code == 406
152+
# Single supported Accept type is sufficient
153+
assert response.status_code == 200
154154

155-
# Test with missing application/json in Accept header
155+
# Test with only text/event-stream in Accept header (valid — single supported type)
156156
async with httpx.AsyncClient(
157157
transport=httpx.ASGITransport(app=app), base_url="http://testserver", timeout=5.0
158158
) as client:
159159
response = await client.post(
160160
"/",
161161
json={"jsonrpc": "2.0", "method": "initialize", "id": 1, "params": {}},
162162
headers={
163-
"Accept": "text/event-stream", # Missing application/json
163+
"Accept": "text/event-stream",
164164
"Content-Type": "application/json",
165165
},
166166
)
167-
# Should get 406 Not Acceptable due to missing application/json
168-
assert response.status_code == 406
167+
# Single supported Accept type is sufficient
168+
assert response.status_code == 200
169169

170170
# Test with completely invalid Accept header
171171
async with httpx.AsyncClient(

tests/shared/test_streamable_http.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,8 +612,7 @@ def test_accept_header_wildcard(basic_server: None, basic_server_url: str, accep
612612
"accept_header",
613613
[
614614
"text/html",
615-
"application/*",
616-
"text/*",
615+
"image/png",
617616
],
618617
)
619618
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
630629
assert "Not Acceptable" in response.text
631630

632631

632+
@pytest.mark.parametrize(
633+
"accept_header",
634+
[
635+
"text/event-stream",
636+
"application/json",
637+
"application/*",
638+
"text/*",
639+
],
640+
)
641+
def test_accept_header_single_type(basic_server: None, basic_server_url: str, accept_header: str):
642+
"""Test that a single supported Accept type is sufficient for SSE mode."""
643+
response = requests.post(
644+
f"{basic_server_url}/mcp",
645+
headers={
646+
"Accept": accept_header,
647+
"Content-Type": "application/json",
648+
},
649+
json=INIT_REQUEST,
650+
)
651+
assert response.status_code == 200
652+
653+
633654
def test_content_type_validation(basic_server: None, basic_server_url: str):
634655
"""Test that Content-Type header is properly validated."""
635656
# Test with incorrect Content-Type

0 commit comments

Comments
 (0)