From 005926c10b79ea866c138ae7c4fce1c5fe25195e Mon Sep 17 00:00:00 2001 From: RJ Lopez Date: Tue, 14 Apr 2026 02:23:35 -0500 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20accept=20wildcard=20patterns=20in=20?= =?UTF-8?q?Accept=20header=20per=20RFC=207231=20=C2=A75.3.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mcp/server/streamable_http.py | 10 +++++++++- tests/shared/test_streamable_http.py | 14 ++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) 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( From a8f72c1afea8342f0b37ec360f14e90ed53cfa2b Mon Sep 17 00:00:00 2001 From: RJ Lopez Date: Tue, 14 Apr 2026 02:45:35 -0500 Subject: [PATCH 2/2] Fix #915: Resolve AnyIO cancellation scope RuntimeError in ClientSessionGroup on connection disconnects --- src/mcp/client/session_group.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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]: