Skip to content

Commit d7f87aa

Browse files
shared/session: silent cancel on CancelledNotification; locally cancel pending requester when emitting CancelledNotification (spec SHOULD-not response)
1 parent 61399b3 commit d7f87aa

File tree

1 file changed

+34
-1
lines changed

1 file changed

+34
-1
lines changed

src/mcp/shared/session.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,20 @@ async def cancel(self) -> None:
147147
response=ErrorData(code=0, message="Request cancelled", data=None),
148148
)
149149

150+
async def mark_cancelled_without_response(self) -> None:
151+
"""Cancel this request and mark it as completed without sending a response.
152+
153+
This is used when cancellation is initiated by a cancellation notification,
154+
where the receiver SHOULD NOT send a response per the MCP spec.
155+
"""
156+
if not self._entered:
157+
raise RuntimeError("RequestResponder must be used as a context manager")
158+
if not self._cancel_scope:
159+
raise RuntimeError("No active cancel scope")
160+
161+
self._cancel_scope.cancel()
162+
self._completed = True
163+
150164
@property
151165
def in_flight(self) -> bool:
152166
return not self._completed and not self.cancelled
@@ -314,6 +328,24 @@ async def send_notification(
314328
)
315329
await self._write_stream.send(session_message)
316330

331+
# If we are emitting a cancellation notification for a request that we
332+
# originally sent, proactively cancel the local waiter so callers of
333+
# send_request() are unblocked without relying on a peer response.
334+
try:
335+
from mcp.types import CancelledNotification as _CancelledNotification # local import to avoid cycle
336+
337+
root = getattr(notification, "root", None)
338+
if isinstance(root, _CancelledNotification):
339+
cancelled_id = root.params.requestId
340+
stream = self._response_streams.pop(cancelled_id, None)
341+
if stream is not None:
342+
error = ErrorData(code=0, message="Request cancelled", data=None)
343+
await stream.send(JSONRPCError(jsonrpc="2.0", id=cancelled_id, error=error))
344+
await stream.aclose()
345+
except Exception:
346+
# Never let local cancellation propagation break notification sending
347+
pass
348+
317349
async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:
318350
if isinstance(response, ErrorData):
319351
jsonrpc_error = JSONRPCError(jsonrpc="2.0", id=request_id, error=response)
@@ -383,7 +415,8 @@ async def _receive_loop(self) -> None:
383415
if isinstance(notification.root, CancelledNotification):
384416
cancelled_id = notification.root.params.requestId
385417
if cancelled_id in self._in_flight:
386-
await self._in_flight[cancelled_id].cancel()
418+
# Silent cancellation in response to a cancellation notification
419+
await self._in_flight[cancelled_id].mark_cancelled_without_response()
387420
else:
388421
# Handle progress notifications callback
389422
if isinstance(notification.root, ProgressNotification):

0 commit comments

Comments
 (0)