server: skip duplicate response on CancelledError (#1153)

Co-authored-by: ihrpr <inna@anthropic.com>
This commit is contained in:
lukacf
2025-07-25 11:16:13 +02:00
committed by GitHub
parent 35777b9811
commit b34e7207ad
2 changed files with 117 additions and 0 deletions
+6
View File
@@ -647,6 +647,12 @@ class Server(Generic[LifespanResultT, RequestT]):
response = await handler(req)
except McpError as err:
response = err.error
except anyio.get_cancelled_exc_class():
logger.info(
"Request %s cancelled - duplicate response suppressed",
message.request_id,
)
return
except Exception as err:
if raise_exceptions:
raise err
+111
View File
@@ -0,0 +1,111 @@
"""Test that cancelled requests don't cause double responses."""
import anyio
import pytest
import mcp.types as types
from mcp.server.lowlevel.server import Server
from mcp.shared.exceptions import McpError
from mcp.shared.memory import create_connected_server_and_client_session
from mcp.types import (
CallToolRequest,
CallToolRequestParams,
CallToolResult,
CancelledNotification,
CancelledNotificationParams,
ClientNotification,
ClientRequest,
Tool,
)
@pytest.mark.anyio
async def test_server_remains_functional_after_cancel():
"""Verify server can handle new requests after a cancellation."""
server = Server("test-server")
# Track tool calls
call_count = 0
ev_first_call = anyio.Event()
first_request_id = None
@server.list_tools()
async def handle_list_tools() -> list[Tool]:
return [
Tool(
name="test_tool",
description="Tool for testing",
inputSchema={},
)
]
@server.call_tool()
async def handle_call_tool(name: str, arguments: dict | None) -> list:
nonlocal call_count, first_request_id
if name == "test_tool":
call_count += 1
if call_count == 1:
first_request_id = server.request_context.request_id
ev_first_call.set()
await anyio.sleep(5) # First call is slow
return [types.TextContent(type="text", text=f"Call number: {call_count}")]
raise ValueError(f"Unknown tool: {name}")
async with create_connected_server_and_client_session(server) as client:
# First request (will be cancelled)
async def first_request():
try:
await client.send_request(
ClientRequest(
CallToolRequest(
method="tools/call",
params=CallToolRequestParams(name="test_tool", arguments={}),
)
),
CallToolResult,
)
pytest.fail("First request should have been cancelled")
except McpError:
pass # Expected
# Start first request
async with anyio.create_task_group() as tg:
tg.start_soon(first_request)
# Wait for it to start
await ev_first_call.wait()
# Cancel it
assert first_request_id is not None
await client.send_notification(
ClientNotification(
CancelledNotification(
method="notifications/cancelled",
params=CancelledNotificationParams(
requestId=first_request_id,
reason="Testing server recovery",
),
)
)
)
# Second request (should work normally)
result = await client.send_request(
ClientRequest(
CallToolRequest(
method="tools/call",
params=CallToolRequestParams(name="test_tool", arguments={}),
)
),
CallToolResult,
)
# Verify second request completed successfully
assert len(result.content) == 1
# Type narrowing for pyright
content = result.content[0]
assert content.type == "text"
assert isinstance(content, types.TextContent)
assert content.text == "Call number: 2"
assert call_count == 2