Skip to content

Commit 254e1b0

Browse files
committed
fix: send notifications/cancelled on request timeout and cancellation
Fixes #2507. BaseSession.send_request() never emits a notifications/cancelled message when its in-flight await is interrupted, whether by the SDK's own timeout or by external cancellation. The server never learns the request was abandoned, leaving coroutines suspended holding resources until the session ends. Add a _send_cancelled_notification helper that does best-effort delivery, and call it from both the TimeoutError and CancelledError handlers. The cancellation path uses anyio.CancelScope(shield=True) to ensure the notification is sent even during teardown.
1 parent 3d7b311 commit 254e1b0

1 file changed

Lines changed: 25 additions & 0 deletions

File tree

src/mcp/shared/session.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
INVALID_PARAMS,
2424
REQUEST_TIMEOUT,
2525
CancelledNotification,
26+
CancelledNotificationParams,
2627
ClientNotification,
2728
ClientRequest,
2829
ClientResult,
@@ -292,9 +293,14 @@ async def send_request(
292293
with anyio.fail_after(timeout):
293294
response_or_error = await response_stream_reader.receive()
294295
except TimeoutError:
296+
await self._send_cancelled_notification(request_id, "request timed out")
295297
class_name = request.__class__.__name__
296298
message = f"Timed out while waiting for response to {class_name}. Waited {timeout} seconds."
297299
raise MCPError(code=REQUEST_TIMEOUT, message=message)
300+
except anyio.get_cancelled_exc_class():
301+
with anyio.CancelScope(shield=True):
302+
await self._send_cancelled_notification(request_id, "request cancelled")
303+
raise
298304

299305
if isinstance(response_or_error, JSONRPCError):
300306
raise MCPError.from_jsonrpc_error(response_or_error)
@@ -325,6 +331,25 @@ async def send_notification(
325331
)
326332
await self._write_stream.send(session_message)
327333

334+
async def _send_cancelled_notification(
335+
self,
336+
request_id: RequestId,
337+
reason: str,
338+
) -> None:
339+
"""Best-effort delivery of a notifications/cancelled for an in-flight request."""
340+
try:
341+
notification = CancelledNotification(
342+
method="notifications/cancelled",
343+
params=CancelledNotificationParams(request_id=request_id, reason=reason),
344+
)
345+
jsonrpc_notification = JSONRPCNotification(
346+
jsonrpc="2.0",
347+
**notification.model_dump(by_alias=True, mode="json", exclude_none=True),
348+
)
349+
await self._write_stream.send(SessionMessage(message=jsonrpc_notification))
350+
except Exception:
351+
logging.debug("Failed to send cancellation notification for request %s", request_id)
352+
328353
async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
329354
if isinstance(response, ErrorData):
330355
jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response)

0 commit comments

Comments
 (0)