Skip to content

Commit 31ae5f4

Browse files
authored
Relax Accept header requirement for JSON-only responses (modelcontextprotocol#1500)
1 parent 6c26d08 commit 31ae5f4

File tree

2 files changed

+69
-8
lines changed

2 files changed

+69
-8
lines changed

src/mcp/server/streamable_http.py

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -306,20 +306,36 @@ def _check_content_type(self, request: Request) -> bool:
306306

307307
return any(part == CONTENT_TYPE_JSON for part in content_type_parts)
308308

309+
async def _validate_accept_header(self, request: Request, scope: Scope, send: Send) -> bool:
310+
"""Validate Accept header based on response mode. Returns True if valid."""
311+
has_json, has_sse = self._check_accept_headers(request)
312+
if self.is_json_response_enabled:
313+
# For JSON-only responses, only require application/json
314+
if not has_json:
315+
response = self._create_error_response(
316+
"Not Acceptable: Client must accept application/json",
317+
HTTPStatus.NOT_ACCEPTABLE,
318+
)
319+
await response(scope, request.receive, send)
320+
return False
321+
# For SSE responses, require both content types
322+
elif not (has_json and has_sse):
323+
response = self._create_error_response(
324+
"Not Acceptable: Client must accept both application/json and text/event-stream",
325+
HTTPStatus.NOT_ACCEPTABLE,
326+
)
327+
await response(scope, request.receive, send)
328+
return False
329+
return True
330+
309331
async def _handle_post_request(self, scope: Scope, request: Request, receive: Receive, send: Send) -> None:
310332
"""Handle POST requests containing JSON-RPC messages."""
311333
writer = self._read_stream_writer
312334
if writer is None:
313335
raise ValueError("No read stream writer available. Ensure connect() is called first.")
314336
try:
315-
# Check Accept headers
316-
has_json, has_sse = self._check_accept_headers(request)
317-
if not (has_json and has_sse):
318-
response = self._create_error_response(
319-
("Not Acceptable: Client must accept both application/json and text/event-stream"),
320-
HTTPStatus.NOT_ACCEPTABLE,
321-
)
322-
await response(scope, receive, send)
337+
# Validate Accept header
338+
if not await self._validate_accept_header(request, scope, send):
323339
return
324340

325341
# Validate Content-Type

tests/shared/test_streamable_http.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,51 @@ def test_json_response(json_response_server: None, json_server_url: str):
693693
assert response.headers.get("Content-Type") == "application/json"
694694

695695

696+
def test_json_response_accept_json_only(json_response_server: None, json_server_url: str):
697+
"""Test that json_response servers only require application/json in Accept header."""
698+
mcp_url = f"{json_server_url}/mcp"
699+
response = requests.post(
700+
mcp_url,
701+
headers={
702+
"Accept": "application/json",
703+
"Content-Type": "application/json",
704+
},
705+
json=INIT_REQUEST,
706+
)
707+
assert response.status_code == 200
708+
assert response.headers.get("Content-Type") == "application/json"
709+
710+
711+
def test_json_response_missing_accept_header(json_response_server: None, json_server_url: str):
712+
"""Test that json_response servers reject requests without Accept header."""
713+
mcp_url = f"{json_server_url}/mcp"
714+
response = requests.post(
715+
mcp_url,
716+
headers={
717+
"Content-Type": "application/json",
718+
},
719+
json=INIT_REQUEST,
720+
)
721+
assert response.status_code == 406
722+
assert "Not Acceptable" in response.text
723+
724+
725+
def test_json_response_incorrect_accept_header(json_response_server: None, json_server_url: str):
726+
"""Test that json_response servers reject requests with incorrect Accept header."""
727+
mcp_url = f"{json_server_url}/mcp"
728+
# Test with only text/event-stream (wrong for JSON server)
729+
response = requests.post(
730+
mcp_url,
731+
headers={
732+
"Accept": "text/event-stream",
733+
"Content-Type": "application/json",
734+
},
735+
json=INIT_REQUEST,
736+
)
737+
assert response.status_code == 406
738+
assert "Not Acceptable" in response.text
739+
740+
696741
def test_get_sse_stream(basic_server: None, basic_server_url: str):
697742
"""Test establishing an SSE stream via GET request."""
698743
# First, we need to initialize a session

0 commit comments

Comments
 (0)