diff --git a/src/mcp/client/session_group.py b/src/mcp/client/session_group.py index 961021264..4c4856aed 100644 --- a/src/mcp/client/session_group.py +++ b/src/mcp/client/session_group.py @@ -165,10 +165,11 @@ async def __aexit__( if self._owns_exit_stack: await self._exit_stack.aclose() - # Concurrently close session stacks. - async with anyio.create_task_group() as tg: - for exit_stack in self._session_exit_stacks.values(): - tg.start_soon(exit_stack.aclose) + # Sequentially close session stacks to preserve AnyIO task contexts. + # Concurrent teardown spawns task groups that cross cancel scopes, leading + # to RuntimeError: Attempted to exit cancel scope in a different task. + for exit_stack in list(self._session_exit_stacks.values()): + await exit_stack.aclose() @property def sessions(self) -> list[mcp.ClientSession]: diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index f14201857..1989df10b 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -396,11 +396,19 @@ def _check_accept_headers(self, request: Request) -> tuple[bool, bool]: """Check if the request accepts the required media types. Supports wildcard media types per RFC 7231, section 5.3.2: + - Missing Accept header matches any media type - */* matches any media type - application/* matches any application/ subtype - text/* matches any text/ subtype """ - accept_header = request.headers.get("accept", "") + accept_header = request.headers.get("accept") + + # RFC 7231, Section 5.3.2: + # A request without any Accept header field implies that the user agent + # will accept any media type in response. + if not accept_header: + return True, True + accept_types = [media_type.strip().split(";")[0].strip().lower() for media_type in accept_header.split(",")] has_wildcard = "*/*" in accept_types diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 3d5770fb6..eacec34a1 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -581,8 +581,7 @@ def test_accept_header_validation(basic_server: None, basic_server_url: str): headers={"Content-Type": "application/json"}, json={"jsonrpc": "2.0", "method": "initialize", "id": 1}, ) - assert response.status_code == 406 - assert "Not Acceptable" in response.text + assert response.status_code == 200 @pytest.mark.parametrize( @@ -613,8 +612,9 @@ def test_accept_header_wildcard(basic_server: None, basic_server_url: str, accep "accept_header", [ "text/html", - "application/*", - "text/*", + "text/html", + "image/*", + "audio/*", ], ) def test_accept_header_incompatible(basic_server: None, basic_server_url: str, accept_header: str): @@ -885,8 +885,7 @@ def test_json_response_missing_accept_header(json_response_server: None, json_se }, json=INIT_REQUEST, ) - assert response.status_code == 406 - assert "Not Acceptable" in response.text + assert response.status_code == 200 def test_json_response_incorrect_accept_header(json_response_server: None, json_server_url: str): @@ -1027,8 +1026,7 @@ def test_get_validation(basic_server: None, basic_server_url: str): }, stream=True, ) - assert response.status_code == 406 - assert "Not Acceptable" in response.text + assert response.status_code == 200 # Test with wrong Accept header response = requests.get(