"""JSON-RPC `Dispatcher` over the `SessionMessage` stream contract all transports speak. Owns request-id correlation, the receive loop, per-request task isolation, cancellation/progress wiring, and the single exception-to-wire boundary; methods and params are otherwise opaque strings and dicts. """ from __future__ import annotations import contextvars import logging from collections.abc import Awaitable, Callable, Mapping from dataclasses import dataclass, field from functools import partial from typing import Any, Generic, Literal, cast import anyio import anyio.abc import anyio.lowlevel from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from opentelemetry.trace import SpanKind from pydantic import ValidationError from typing_extensions import TypeVar from mcp.shared._compat import resync_tracer from mcp.shared._otel import inject_trace_context, otel_span from mcp.shared._stream_protocols import ReadStream, WriteStream from mcp.shared.dispatcher import CallOptions, DispatchContext, Dispatcher, OnNotify, OnRequest, ProgressFnT from mcp.shared.exceptions import MCPError, NoBackChannelError from mcp.shared.message import ( ClientMessageMetadata, MessageMetadata, ServerMessageMetadata, SessionMessage, ) from mcp.shared.transport_context import TransportContext from mcp.types import ( CONNECTION_CLOSED, INTERNAL_ERROR, INVALID_PARAMS, REQUEST_TIMEOUT, ErrorData, JSONRPCError, JSONRPCMessage, JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, ProgressToken, RequestId, ) __all__ = ["JSONRPCDispatcher", "handler_exception_to_error_data"] logger = logging.getLogger(__name__) _ABANDON_WRITE_TIMEOUT: float = 5 """Bound for courtesy-cancel writes on the abandon paths; the caller-cancel arm shields its write, so a wedged transport would otherwise hang it uncancellably.""" _SHUTDOWN_WRITE_TIMEOUT: float = 1 """Tighter bound for the shutdown-arm error write so a wedged transport can't hold session close.""" TransportT = TypeVar("TransportT", bound=TransportContext, default=TransportContext) PeerCancelMode = Literal["interrupt", "signal"] """How `notifications/cancelled` is applied: `"interrupt"` (default) cancels the handler's scope; `"signal"` only sets `ctx.cancel_requested`.""" def handler_exception_to_error_data(exc: BaseException) -> ErrorData | None: """Map a handler-raised exception to its wire `ErrorData`. The two rungs every dispatcher shares: an `MCPError` carries its own `ErrorData`; a pydantic `ValidationError` is the spec's INVALID_PARAMS with empty ``data`` (no pydantic text on the wire). Returns ``None`` for any other exception so each caller applies its own catch-all - `JSONRPCDispatcher` currently pins ``code=0`` for v1 compat, `to_jsonrpc_response` uses `INTERNAL_ERROR`. """ if isinstance(exc, MCPError): return exc.error if isinstance(exc, ValidationError): return ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="") return None def _coerce_id(request_id: RequestId) -> RequestId: """Coerce a stringified int request ID back to int so a peer-echoed ID still correlates (matches the TS SDK).""" if isinstance(request_id, str): try: return int(request_id) except ValueError: pass return request_id @dataclass(slots=True) class _Pending: """An outbound request awaiting its response.""" send: MemoryObjectSendStream[dict[str, Any] | ErrorData] receive: MemoryObjectReceiveStream[dict[str, Any] | ErrorData] on_progress: ProgressFnT | None = None @dataclass(slots=True) class _InFlight(Generic[TransportT]): """An inbound request currently being handled.""" scope: anyio.CancelScope dctx: _JSONRPCDispatchContext[TransportT] @dataclass class _JSONRPCDispatchContext(Generic[TransportT]): """Concrete `DispatchContext` produced for each inbound JSON-RPC message.""" transport: TransportT _dispatcher: JSONRPCDispatcher[TransportT] _request_id: RequestId | None message_metadata: MessageMetadata = None # TODO(maxisbey): remove for Context rework """Transport-attached `SessionMessage.metadata` that the server lifts onto its request context.""" _progress_token: ProgressToken | None = None _closed: bool = False cancel_requested: anyio.Event = field(default_factory=anyio.Event) @property def request_id(self) -> RequestId | None: return self._request_id @property def can_send_request(self) -> bool: return self.transport.can_send_request and not self._closed async def notify(self, method: str, params: Mapping[str, Any] | None) -> None: if self._closed: logger.debug("dropped %s: dispatch context closed", method) return await self._dispatcher.notify(method, params, _related_request_id=self._request_id) async def send_raw_request( self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None, ) -> dict[str, Any]: if not self.can_send_request: raise NoBackChannelError(method) return await self._dispatcher.send_raw_request(method, params, opts, _related_request_id=self._request_id) async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: if self._progress_token is None: return params: dict[str, Any] = {"progressToken": self._progress_token, "progress": progress} if total is not None: params["total"] = total if message is not None: params["message"] = message await self.notify("notifications/progress", params) def close(self) -> None: self._closed = True def _default_transport_builder(_meta: MessageMetadata) -> TransportContext: return TransportContext(kind="jsonrpc", can_send_request=True) def _shielded_progress(fn: ProgressFnT) -> ProgressFnT: """Wrap a user progress callback so an exception can't cancel the dispatcher's task group.""" async def _wrapped(progress: float, total: float | None, message: str | None) -> None: try: await fn(progress, total, message) except Exception: logger.exception("progress callback raised") return _wrapped def _contained_notify(fn: OnNotify) -> OnNotify: """Wrap a notification handler so it can't crash the dispatcher (same boundary as `_shielded_progress`).""" async def _wrapped(dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None: try: await fn(dctx, method, params) except Exception: logger.exception("notification handler for %r raised", method) return _wrapped @dataclass(slots=True, frozen=True) class _OutboundPlan: """Outbound metadata plus whether abandoning the request sends a courtesy `notifications/cancelled`.""" metadata: MessageMetadata cancel_on_abandon: bool def _plan_outbound(related_request_id: RequestId | None, opts: CallOptions | None) -> _OutboundPlan: """Choose the outbound `SessionMessage.metadata` and the abandon-cancellation policy. `related_request_id` wins over resumption hints (they are dropped). Only hints that actually reach the transport suppress the courtesy cancel - a request that is neither resumable nor cancelled would leak the peer's work. """ opts = opts or {} cancel_on_abandon = opts.get("cancel_on_abandon", True) token = opts.get("resumption_token") on_token = opts.get("on_resumption_token") if related_request_id is not None: if token is not None or on_token is not None: logger.debug( "dropping resumption hints: related_request_id %r takes precedence on metadata", related_request_id ) return _OutboundPlan(ServerMessageMetadata(related_request_id=related_request_id), cancel_on_abandon) if token is not None or on_token is not None: return _OutboundPlan( ClientMessageMetadata(resumption_token=token, on_resumption_token_update=on_token), cancel_on_abandon=False, ) return _OutboundPlan(None, cancel_on_abandon) class JSONRPCDispatcher(Dispatcher[TransportT]): """`Dispatcher` over the `SessionMessage` stream contract. Explicit Protocol base so pyright checks conformance at the class definition. """ def __init__( self, read_stream: ReadStream[SessionMessage | Exception], write_stream: WriteStream[SessionMessage], *, transport_builder: Callable[[MessageMetadata], TransportT] | None = None, peer_cancel_mode: PeerCancelMode = "interrupt", raise_handler_exceptions: bool = False, inline_methods: frozenset[str] = frozenset(), on_stream_exception: Callable[[Exception], Awaitable[None]] | None = None, ) -> None: """Wire a dispatcher over a transport's `SessionMessage` stream pair. Args: transport_builder: Builds each message's `TransportContext` from its `SessionMessage.metadata`. raise_handler_exceptions: Re-raise handler exceptions out of `run()` after the error response is written. inline_methods: Methods awaited in the read loop before the next message is dequeued (e.g. `initialize`); an inline handler that awaits the peer deadlocks the parked loop. on_stream_exception: Observer for `Exception` items on the read stream; without it they are debug-logged and dropped. Awaited inline in the read loop, so a slow observer stalls dispatch. """ self._read_stream = read_stream self._write_stream = write_stream # With transport_builder omitted, TransportT defaults to # TransportContext; pyright can't connect the two, hence the cast. self._transport_builder = cast( "Callable[[MessageMetadata], TransportT]", transport_builder or _default_transport_builder, ) self._peer_cancel_mode: PeerCancelMode = peer_cancel_mode self._raise_handler_exceptions = raise_handler_exceptions self._inline_methods = inline_methods self._on_stream_exception = on_stream_exception self._next_id = 0 self._pending: dict[RequestId, _Pending] = {} self._in_flight: dict[RequestId, _InFlight[TransportT]] = {} self._tg: anyio.abc.TaskGroup | None = None self._running = False self._closed = False async def send_raw_request( self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None, *, _related_request_id: RequestId | None = None, ) -> dict[str, Any]: """Send a JSON-RPC request and await its response. `_related_request_id` is set only by `_JSONRPCDispatchContext` so that mid-handler requests route onto the inbound request's SSE stream. Raises: MCPError: Peer error response; `REQUEST_TIMEOUT` if `opts["timeout"]` elapsed; `CONNECTION_CLOSED` if the transport closed or the dispatcher shut down. RuntimeError: Called before `run()`. """ # Post-close sends get the same CONNECTION_CLOSED contract as in-flight waiters. if self._closed: raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") if not self._running: raise RuntimeError("JSONRPCDispatcher.send_raw_request called before run()") opts = opts or {} request_id = self._allocate_id() out_params = dict(params) if params is not None else {} out_meta = dict(out_params.get("_meta") or {}) on_progress = opts.get("on_progress") if on_progress is not None: # The request id doubles as the progress token, so `_pending[token]` finds `on_progress` directly. out_meta["progressToken"] = request_id out_params["_meta"] = out_meta # buffer=1: a close signal can arrive before the waiter parks in receive(); # a WouldBlock later just means the waiter already has its one outcome. send, receive = anyio.create_memory_object_stream[dict[str, Any] | ErrorData](1) pending = _Pending(send=send, receive=receive, on_progress=on_progress) self._pending[request_id] = pending plan = _plan_outbound(_related_request_id, opts) # Spec MUST: only previously-issued requests may be cancelled. A write # interrupted by cancellation may still have delivered (a memory-stream # send can hand its item to the receiver and still raise), so a started # write counts as issued: the peer ignores a cancel for an id it never # saw, while skipping it would leak a delivered request's handler. request_write_started = False timeout_armed = False target = out_params.get("name") span_name = f"MCP send {method}{f' {target}' if isinstance(target, str) else ''}" # TODO(maxisbey): move the otel span + inject into an outbound # middleware once that seam exists; the dispatcher should not own otel. try: with otel_span( span_name, kind=SpanKind.CLIENT, attributes={"mcp.method.name": method, "jsonrpc.request.id": str(request_id)}, ): # SEP-414: inject W3C trace context; `_meta` stays on the wire even with a no-op tracer. inject_trace_context(out_meta) msg = JSONRPCRequest(jsonrpc="2.0", id=request_id, method=method, params=out_params) # Surface a pre-existing cancellation while the request provably # never started; past this point a cancelled write counts as issued. await anyio.lowlevel.checkpoint_if_cancelled() request_write_started = True try: await self._write(msg, plan.metadata) except (anyio.BrokenResourceError, anyio.ClosedResourceError): # Transport tore down before run() noticed EOF; surface the documented contract. raise MCPError(code=CONNECTION_CLOSED, message="Connection closed") from None with anyio.fail_after(opts.get("timeout")): timeout_armed = True outcome = await receive.receive() except TimeoutError: if not timeout_armed: # `fail_after` arms only after the write, so this TimeoutError is the # transport's own bounded send() failing - a transport error, not # `opts["timeout"]` elapsing. Propagate it raw (v1 kept the write # outside the timeout-catching try and did the same). raise # Courtesy cancel (spec-recommended, new vs v1) so the peer stops work; # unshielded so an outer caller cancellation can still interrupt the write. if plan.cancel_on_abandon: await self._final_write( partial( self._cancel_outbound, request_id, f"timed out after {opts.get('timeout')}s", _related_request_id, ), shield=False, timeout=_ABANDON_WRITE_TIMEOUT, describe=f"courtesy cancel for timed-out request {request_id!r}", ) raise MCPError(code=REQUEST_TIMEOUT, message=f"Request {method!r} timed out") from None except anyio.get_cancelled_exc_class(): # Caller cancelled: bare awaits re-raise here, so the shielded helper # lets the courtesy cancel go out before we propagate. if plan.cancel_on_abandon and request_write_started: await self._final_write( partial(self._cancel_outbound, request_id, "caller cancelled", _related_request_id), shield=True, timeout=_ABANDON_WRITE_TIMEOUT, describe=f"courtesy cancel for caller-cancelled request {request_id!r}", ) raise finally: # Remove the waiter on every path so a late response is dropped, not leaked. self._pending.pop(request_id, None) send.close() receive.close() if isinstance(outcome, ErrorData): raise MCPError(code=outcome.code, message=outcome.message, data=outcome.data) return outcome async def notify( self, method: str, params: Mapping[str, Any] | None, *, _related_request_id: RequestId | None = None, ) -> None: """Send a fire-and-forget notification. Fire-and-forget all the way: a post-close send or a write onto a torn-down transport drops the notification with a debug log instead of raising (same policy as the response writes and `ctx.notify`). """ if self._closed: logger.debug("dropped %s: dispatcher closed", method) return # Leave `params` unset when None: with `exclude_unset=True` an explicit # None would serialize as `"params": null`, which JSON-RPC 2.0 forbids. if params is not None: msg = JSONRPCNotification(jsonrpc="2.0", method=method, params=dict(params)) else: msg = JSONRPCNotification(jsonrpc="2.0", method=method) try: await self._write(msg, _plan_outbound(_related_request_id, None).metadata) except (anyio.BrokenResourceError, anyio.ClosedResourceError): # Transport tore down before run() noticed EOF. logger.debug("dropped %s: write stream closed", method) async def run( self, on_request: OnRequest, on_notify: OnNotify, *, task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED, ) -> None: """Drive the receive loop until the read stream closes. `task_status.started()` fires once `send_raw_request` is usable. Single-shot: once the loop ends the dispatcher stays closed and cannot be restarted. """ try: # LIFO exits: the write stream closes only after the task-group join, so teardown writes still land. async with self._write_stream: async with anyio.create_task_group() as tg: self._tg = tg self._running = True task_status.started() try: async with self._read_stream: try: async for item in self._read_stream: # Duck-typed: only `ContextReceiveStream` carries the # sender's per-message contextvars snapshot. sender_ctx: contextvars.Context | None = getattr( self._read_stream, "last_context", None ) await self._dispatch(item, on_request, on_notify, sender_ctx) except anyio.ClosedResourceError: # Receive end closed under us (stateless SHTTP teardown); same as EOF. logger.debug("read stream closed by transport; treating as EOF") # EOF: wake blocked `send_raw_request` waiters with CONNECTION_CLOSED. self._running = False self._closed = True self._fan_out_closed() finally: # Cancel in-flight handlers; otherwise the task-group join # waits on handlers whose callers are already gone. tg.cancel_scope.cancel() finally: # Covers cancel/crash paths that skip the inline fan-out; idempotent. self._running = False self._closed = True self._tg = None self._fan_out_closed() await resync_tracer() async def _dispatch( self, item: SessionMessage | Exception, on_request: OnRequest, on_notify: OnNotify, sender_ctx: contextvars.Context | None, ) -> None: """Route one inbound item. Only `inline_methods` requests and the `on_stream_exception` observer are awaited; any other `await` would head-of-line block the read loop. """ if isinstance(item, Exception): if self._on_stream_exception is None: logger.debug("transport yielded exception: %r", item) return try: await self._on_stream_exception(item) except Exception: logger.exception("on_stream_exception observer raised") return metadata = item.metadata msg = item.message match msg: case JSONRPCRequest(): await self._dispatch_request(msg, metadata, on_request, sender_ctx) case JSONRPCNotification(): self._dispatch_notification(msg, metadata, on_notify, sender_ctx) case JSONRPCResponse(): self._resolve_pending(msg.id, msg.result) case JSONRPCError(): # pragma: no branch # Exhaustive over JSONRPCMessage, so the no-match arc is unreachable. self._resolve_pending(msg.id, msg.error) async def _dispatch_request( self, req: JSONRPCRequest, metadata: MessageMetadata, on_request: OnRequest, sender_ctx: contextvars.Context | None, ) -> None: progress_token: ProgressToken | None match req.params: # bool subclasses int: without the guard True would alias request id 1. case {"_meta": {"progressToken": str() | int() as progress_token}} if not isinstance(progress_token, bool): pass case _: progress_token = None try: transport_ctx = self._transport_builder(metadata) except Exception: # A raising builder must cost only this message, not the connection. logger.exception("transport_builder raised; rejecting request %r", req.id) self._spawn( self._write_error, req.id, ErrorData(code=INTERNAL_ERROR, message="transport context unavailable"), sender_ctx=sender_ctx, ) return dctx = _JSONRPCDispatchContext( transport=transport_ctx, _dispatcher=self, _request_id=req.id, message_metadata=metadata, _progress_token=progress_token, ) scope = anyio.CancelScope() # TODO(maxisbey): duplicate ids blind-overwrite (v1/TS parity); revisit # rejecting with INVALID_REQUEST. Key coerced so a stringified # `notifications/cancelled` id still correlates. self._in_flight[_coerce_id(req.id)] = _InFlight(scope=scope, dctx=dctx) if req.method in self._inline_methods: # Spawn so `sender_ctx` applies, but park the read loop until the # handler returns - that's the inline ordering guarantee. done = anyio.Event() async def _run_inline() -> None: try: await self._handle_request(req, dctx, scope, on_request) finally: done.set() self._spawn(_run_inline, sender_ctx=sender_ctx) await done.wait() else: self._spawn(self._handle_request, req, dctx, scope, on_request, sender_ctx=sender_ctx) def _dispatch_notification( self, msg: JSONRPCNotification, metadata: MessageMetadata, on_notify: OnNotify, sender_ctx: contextvars.Context | None, ) -> None: """Route one inbound notification. `notifications/cancelled` and `notifications/progress` are intercepted here (they correlate against the `_in_flight`/`_pending` tables this layer owns) and still teed to `on_notify` afterwards. """ if msg.method == "notifications/cancelled": match msg.params: # bool subclasses int: the guards keep True from aliasing request id 1. case {"requestId": str() | int() as rid} if ( not isinstance(rid, bool) and (in_flight := self._in_flight.get(_coerce_id(rid))) is not None ): in_flight.dctx.cancel_requested.set() if self._peer_cancel_mode == "interrupt": in_flight.scope.cancel() case _: pass elif msg.method == "notifications/progress": match msg.params: case {"progressToken": str() | int() as token, "progress": int() | float() as progress} if ( not isinstance(token, bool) and not isinstance(progress, bool) and (pending := self._pending.get(_coerce_id(token))) is not None and pending.on_progress is not None ): total = msg.params.get("total") message = msg.params.get("message") self._spawn( _shielded_progress(pending.on_progress), float(progress), float(total) if isinstance(total, int | float) else None, message if isinstance(message, str) else None, sender_ctx=sender_ctx, ) case _: pass try: transport_ctx = self._transport_builder(metadata) except Exception: # Same containment as `_dispatch_request`: drop the notification, keep the loop. logger.exception("transport_builder raised; dropping notification %r", msg.method) return dctx = _JSONRPCDispatchContext( transport=transport_ctx, _dispatcher=self, _request_id=None, message_metadata=metadata ) self._spawn(_contained_notify(on_notify), dctx, msg.method, msg.params, sender_ctx=sender_ctx) def _resolve_pending(self, request_id: RequestId | None, outcome: dict[str, Any] | ErrorData) -> None: pending = self._pending.get(_coerce_id(request_id)) if request_id is not None else None if pending is None: logger.debug("dropping response for unknown/late request id %r", request_id) return try: pending.send.send_nowait(outcome) except (anyio.WouldBlock, anyio.BrokenResourceError, anyio.ClosedResourceError): logger.debug("waiter for request id %r already gone", request_id) def _spawn( self, fn: Callable[..., Awaitable[Any]], *args: object, sender_ctx: contextvars.Context | None, ) -> None: """Schedule `fn(*args)` in the run() task group, propagating the sender's contextvars. ASGI middleware (auth, OTel) sets contextvars on the task that wrote the message; `Context.run` makes the spawned handler inherit that context. """ assert self._tg is not None if sender_ctx is not None: sender_ctx.run(self._tg.start_soon, fn, *args) else: self._tg.start_soon(fn, *args) def _fan_out_closed(self) -> None: """Wake every pending `send_raw_request` waiter with `CONNECTION_CLOSED`. Synchronous: callers may be inside a cancelled scope. Idempotent. """ closed = ErrorData(code=CONNECTION_CLOSED, message="Connection closed") for pending in self._pending.values(): try: pending.send.send_nowait(closed) except (anyio.WouldBlock, anyio.BrokenResourceError, anyio.ClosedResourceError): pass self._pending.clear() async def _handle_request( self, req: JSONRPCRequest, dctx: _JSONRPCDispatchContext[TransportT], scope: anyio.CancelScope, on_request: OnRequest, ) -> None: """Run `on_request` for one inbound request and write its response. The single exception-to-wire boundary: handler exceptions become `JSONRPCError` here. """ answer_write_started = False try: with scope: try: result = await on_request(dctx, req.method, req.params) finally: # Close the back-channel and drop from `_in_flight`; no checkpoint # since handler return, so a peer cancel can't interleave. # Identity guard: don't evict a duplicate id's newer entry. dctx.close() key = _coerce_id(req.id) if (entry := self._in_flight.get(key)) is not None and entry.dctx is dctx: del self._in_flight[key] # A write interrupted by cancellation may still have delivered # (a memory-stream send can hand its item to the receiver and # still raise), so a started answer write counts as sent below: # peers drop late responses, while a second answer for one id # would break JSON-RPC. answer_write_started = True await self._write_result(req.id, result) if scope.cancelled_caught: # anyio absorbs the scope's own cancel at __exit__, and # `cancelled_caught` (unlike `cancel_called`) guarantees the # result write above did not happen - no double response. # TODO(L38): spec says SHOULD NOT respond after cancel; # the existing server always has, so match that for now. answer_write_started = True await self._write_error(req.id, ErrorData(code=0, message="Request cancelled")) except anyio.get_cancelled_exc_class(): # Shutdown: answer the request so the peer isn't left waiting - unless # an answer write already started (it may have reached the transport; # prefer possibly-zero answers over possibly-two). The shielded helper # is needed because bare awaits re-raise here. if not answer_write_started: await self._final_write( partial(self._write_error, req.id, ErrorData(code=CONNECTION_CLOSED, message="Connection closed")), shield=True, timeout=_SHUTDOWN_WRITE_TIMEOUT, describe=f"shutdown error response for request {req.id!r}", ) raise except Exception as e: error = handler_exception_to_error_data(e) if error is not None: await self._write_error(req.id, error) else: logger.exception("handler for %r raised", req.method) # TODO(L58): code=0 pins existing-server compat; JSON-RPC says # INTERNAL_ERROR. Revisit per the suite's divergence entry. await self._write_error(req.id, ErrorData(code=0, message=str(e))) if self._raise_handler_exceptions: raise # No `_in_flight` pop here: the inner finally covers every path, and a late pop could evict a reused id. def _allocate_id(self) -> int: self._next_id += 1 return self._next_id async def _write(self, message: JSONRPCMessage, metadata: MessageMetadata = None) -> None: await self._write_stream.send(SessionMessage(message=message, metadata=metadata)) async def _write_result(self, request_id: RequestId, result: dict[str, Any]) -> None: try: await self._write(JSONRPCResponse(jsonrpc="2.0", id=request_id, result=result)) except (anyio.BrokenResourceError, anyio.ClosedResourceError): logger.debug("dropped result for %r: write stream closed", request_id) async def _write_error(self, request_id: RequestId, error: ErrorData) -> None: try: await self._write(JSONRPCError(jsonrpc="2.0", id=request_id, error=error)) except (anyio.BrokenResourceError, anyio.ClosedResourceError): logger.debug("dropped error for %r: write stream closed", request_id) async def _final_write( self, write: Callable[[], Awaitable[None]], *, shield: bool, timeout: float, describe: str, ) -> None: """Attempt one last write under the shared abandon/teardown policy. `shield=True` is for arms already inside a cancelled scope (a bare `await` would re-raise); the bound keeps a wedged transport write from becoming an uncancellable hang. """ with anyio.move_on_after(timeout, shield=shield) as scope: await write() if scope.cancelled_caught: logger.warning("%s gave up: transport write blocked", describe) async def _cancel_outbound(self, request_id: RequestId, reason: str, related_request_id: RequestId | None) -> None: # Thread `related_request_id` so streamable HTTP routes the cancel onto # the request's own SSE stream instead of a possibly-absent GET stream. # `notify` swallows connection-state errors itself, so no guard here. await self.notify( "notifications/cancelled", {"requestId": request_id, "reason": reason}, _related_request_id=related_request_id, )