Skip to content

Commit f745d69

Browse files
authored
fix: 204 is an acceptable response to DELETEing the session (#697)
1 parent 76919cf commit f745d69

File tree

2 files changed

+67
-1
lines changed

2 files changed

+67
-1
lines changed

src/mcp/client/streamable_http.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ async def terminate_session(self, client: httpx.AsyncClient) -> None:
410410

411411
if response.status_code == 405:
412412
logger.debug("Server does not allow session termination")
413-
elif response.status_code != 200:
413+
elif response.status_code not in (200, 204):
414414
logger.warning(f"Session termination failed: {response.status_code}")
415415
except Exception as exc:
416416
logger.warning(f"Session termination failed: {exc}")

tests/shared/test_streamable_http.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,72 @@ async def test_streamablehttp_client_session_termination(
960960
await session.list_tools()
961961

962962

963+
@pytest.mark.anyio
964+
async def test_streamablehttp_client_session_termination_204(
965+
basic_server, basic_server_url, monkeypatch
966+
):
967+
"""Test client session termination functionality with a 204 response.
968+
969+
This test patches the httpx client to return a 204 response for DELETEs.
970+
"""
971+
972+
# Save the original delete method to restore later
973+
original_delete = httpx.AsyncClient.delete
974+
975+
# Mock the client's delete method to return a 204
976+
async def mock_delete(self, *args, **kwargs):
977+
# Call the original method to get the real response
978+
response = await original_delete(self, *args, **kwargs)
979+
980+
# Create a new response with 204 status code but same headers
981+
mocked_response = httpx.Response(
982+
204,
983+
headers=response.headers,
984+
content=response.content,
985+
request=response.request,
986+
)
987+
return mocked_response
988+
989+
# Apply the patch to the httpx client
990+
monkeypatch.setattr(httpx.AsyncClient, "delete", mock_delete)
991+
992+
captured_session_id = None
993+
994+
# Create the streamablehttp_client with a custom httpx client to capture headers
995+
async with streamablehttp_client(f"{basic_server_url}/mcp") as (
996+
read_stream,
997+
write_stream,
998+
get_session_id,
999+
):
1000+
async with ClientSession(read_stream, write_stream) as session:
1001+
# Initialize the session
1002+
result = await session.initialize()
1003+
assert isinstance(result, InitializeResult)
1004+
captured_session_id = get_session_id()
1005+
assert captured_session_id is not None
1006+
1007+
# Make a request to confirm session is working
1008+
tools = await session.list_tools()
1009+
assert len(tools.tools) == 4
1010+
1011+
headers = {}
1012+
if captured_session_id:
1013+
headers[MCP_SESSION_ID_HEADER] = captured_session_id
1014+
1015+
async with streamablehttp_client(f"{basic_server_url}/mcp", headers=headers) as (
1016+
read_stream,
1017+
write_stream,
1018+
_,
1019+
):
1020+
async with ClientSession(read_stream, write_stream) as session:
1021+
# Attempt to make a request after termination
1022+
with pytest.raises(
1023+
McpError,
1024+
match="Session terminated",
1025+
):
1026+
await session.list_tools()
1027+
1028+
9631029
@pytest.mark.anyio
9641030
async def test_streamablehttp_client_resumption(event_server):
9651031
"""Test client session to resume a long running tool."""

0 commit comments

Comments
 (0)