From 1da25ec1828925fcd61882f46db40ea7ecdda2fe Mon Sep 17 00:00:00 2001 From: Max Isbey <224885523+maxisbey@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:52:58 +0000 Subject: [PATCH] refactor: rename Dispatcher.call to send_request, replace RequestSender with Outbound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The design doc's `send_request = call` alias only makes the concrete class satisfy RequestSender, not the abstract Dispatcher Protocol — so any consumer typed against `Dispatcher[TT]` (Connection, ServerRunner) couldn't pass it to something expecting a RequestSender without a cast or hand-written bridge. RequestSender was also half a contract: every implementor (Dispatcher, DispatchContext, Connection, Context) has `notify` too, and PeerMixin needs both for its typed sugar (elicit/sample are requests, log is a notification). Outbound(Protocol) declares both methods; Dispatcher and DispatchContext extend it. PeerMixin will wrap an Outbound. One verb everywhere, no aliases, no extra Protocols. - Dispatcher.call -> send_request - OnCall -> OnRequest, on_call -> on_request - RequestSender -> Outbound (now also declares notify) - Dispatcher(Outbound, Protocol[TT]), DispatchContext(Outbound, Protocol[TT]) --- src/mcp/shared/direct_dispatcher.py | 38 ++++---- src/mcp/shared/dispatcher.py | 146 ++++++++++++---------------- tests/shared/test_dispatcher.py | 115 +++++++++++----------- 3 files changed, 138 insertions(+), 161 deletions(-) diff --git a/src/mcp/shared/direct_dispatcher.py b/src/mcp/shared/direct_dispatcher.py index 46506194..79b68d05 100644 --- a/src/mcp/shared/direct_dispatcher.py +++ b/src/mcp/shared/direct_dispatcher.py @@ -1,7 +1,7 @@ """In-memory `Dispatcher` that wires two peers together with no transport. -`DirectDispatcher` is the simplest possible `Dispatcher` implementation: a call -on one side directly invokes the other side's `on_call`. There is no +`DirectDispatcher` is the simplest possible `Dispatcher` implementation: a +request on one side directly invokes the other side's `on_request`. There is no serialization, no JSON-RPC framing, and no streams. It exists to: * prove the `Dispatcher` Protocol is implementable without JSON-RPC @@ -21,7 +21,7 @@ from typing import Any import anyio -from mcp.shared.dispatcher import CallOptions, OnCall, OnNotify, ProgressFnT +from mcp.shared.dispatcher import CallOptions, OnNotify, OnRequest, ProgressFnT from mcp.shared.exceptions import MCPError, NoBackChannelError from mcp.shared.transport_context import TransportContext from mcp.types import INTERNAL_ERROR, REQUEST_TIMEOUT @@ -31,20 +31,20 @@ __all__ = ["DirectDispatcher", "create_direct_dispatcher_pair"] DIRECT_TRANSPORT_KIND = "direct" -_Call = Callable[[str, Mapping[str, Any] | None, CallOptions | None], Awaitable[dict[str, Any]]] +_Request = Callable[[str, Mapping[str, Any] | None, CallOptions | None], Awaitable[dict[str, Any]]] _Notify = Callable[[str, Mapping[str, Any] | None], Awaitable[None]] @dataclass class _DirectDispatchContext: - """`DispatchContext` for an inbound call on a `DirectDispatcher`. + """`DispatchContext` for an inbound request on a `DirectDispatcher`. The back-channel callables target the *originating* side, so a handler's - `send_request` reaches the peer that made the inbound call. + `send_request` reaches the peer that made the inbound request. """ transport: TransportContext - _back_call: _Call + _back_request: _Request _back_notify: _Notify _on_progress: ProgressFnT | None = None cancel_requested: anyio.Event = field(default_factory=anyio.Event) @@ -60,7 +60,7 @@ class _DirectDispatchContext: ) -> dict[str, Any]: if not self.transport.can_send_request: raise NoBackChannelError(method) - return await self._back_call(method, params, opts) + return await self._back_request(method, params, opts) async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: if self._on_progress is not None: @@ -71,14 +71,14 @@ class DirectDispatcher: """A `Dispatcher` that calls a peer's handlers directly, in-process. Two instances are wired together with `create_direct_dispatcher_pair`; each - holds a reference to the other. `call` on one awaits the peer's `on_call`. - `run` parks until `close` is called. + holds a reference to the other. `send_request` on one awaits the peer's + `on_request`. `run` parks until `close` is called. """ def __init__(self, transport_ctx: TransportContext): self._transport_ctx = transport_ctx self._peer: DirectDispatcher | None = None - self._on_call: OnCall | None = None + self._on_request: OnRequest | None = None self._on_notify: OnNotify | None = None self._ready = anyio.Event() self._closed = anyio.Event() @@ -86,7 +86,7 @@ class DirectDispatcher: def connect_to(self, peer: DirectDispatcher) -> None: self._peer = peer - async def call( + async def send_request( self, method: str, params: Mapping[str, Any] | None, @@ -94,15 +94,15 @@ class DirectDispatcher: ) -> dict[str, Any]: if self._peer is None: raise RuntimeError("DirectDispatcher has no peer; use create_direct_dispatcher_pair()") - return await self._peer._dispatch_call(method, params, opts) + return await self._peer._dispatch_request(method, params, opts) async def notify(self, method: str, params: Mapping[str, Any] | None) -> None: if self._peer is None: raise RuntimeError("DirectDispatcher has no peer; use create_direct_dispatcher_pair()") await self._peer._dispatch_notify(method, params) - async def run(self, on_call: OnCall, on_notify: OnNotify) -> None: - self._on_call = on_call + async def run(self, on_request: OnRequest, on_notify: OnNotify) -> None: + self._on_request = on_request self._on_notify = on_notify self._ready.set() await self._closed.wait() @@ -115,25 +115,25 @@ class DirectDispatcher: peer = self._peer return _DirectDispatchContext( transport=self._transport_ctx, - _back_call=lambda m, p, o: peer._dispatch_call(m, p, o), + _back_request=lambda m, p, o: peer._dispatch_request(m, p, o), _back_notify=lambda m, p: peer._dispatch_notify(m, p), _on_progress=on_progress, ) - async def _dispatch_call( + async def _dispatch_request( self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None, ) -> dict[str, Any]: await self._ready.wait() - assert self._on_call is not None + assert self._on_request is not None opts = opts or {} dctx = self._make_context(on_progress=opts.get("on_progress")) try: with anyio.fail_after(opts.get("timeout")): try: - return await self._on_call(dctx, method, params) + return await self._on_request(dctx, method, params) except MCPError: raise except Exception as e: diff --git a/src/mcp/shared/dispatcher.py b/src/mcp/shared/dispatcher.py index b63c00c0..872fb01e 100644 --- a/src/mcp/shared/dispatcher.py +++ b/src/mcp/shared/dispatcher.py @@ -2,9 +2,9 @@ A Dispatcher turns a duplex message channel into two things: -* an outbound API: ``call(method, params)`` and ``notify(method, params)`` -* an inbound pump: ``run(on_call, on_notify)`` that drives the receive loop and - invokes the supplied handlers for each incoming request/notification +* an outbound API: ``send_request(method, params)`` and ``notify(method, params)`` +* an inbound pump: ``run(on_request, on_notify)`` that drives the receive loop + and invokes the supplied handlers for each incoming request/notification It is deliberately *not* MCP-aware. Method names are strings, params and results are ``dict[str, Any]``. The MCP type layer (request/result models, @@ -28,23 +28,23 @@ __all__ = [ "DispatchContext", "DispatchMiddleware", "Dispatcher", - "OnCall", "OnNotify", + "OnRequest", + "Outbound", "ProgressFnT", - "RequestSender", ] TransportT_co = TypeVar("TransportT_co", bound=TransportContext, covariant=True) class ProgressFnT(Protocol): - """Callback invoked when a progress notification arrives for a pending call.""" + """Callback invoked when a progress notification arrives for a pending request.""" async def __call__(self, progress: float, total: float | None, message: str | None) -> None: ... class CallOptions(TypedDict, total=False): - """Per-call options for `RequestSender.send_request` / `Dispatcher.call`. + """Per-call options for `Outbound.send_request`. All keys are optional. Dispatchers ignore keys they do not understand. """ @@ -53,21 +53,22 @@ class CallOptions(TypedDict, total=False): """Seconds to wait for a result before raising and sending ``notifications/cancelled``.""" on_progress: ProgressFnT - """Receive ``notifications/progress`` updates for this call.""" + """Receive ``notifications/progress`` updates for this request.""" resumption_token: str - """Opaque token to resume a previously interrupted call (transport-dependent).""" + """Opaque token to resume a previously interrupted request (transport-dependent).""" on_resumption_token: Callable[[str], Awaitable[None]] """Receive a resumption token when the transport issues one.""" @runtime_checkable -class RequestSender(Protocol): - """Anything that can send a request and await its result. +class Outbound(Protocol): + """Anything that can send requests and notifications to the peer. - `DispatchContext` satisfies this; `PeerMixin` (and `Connection`/`Peer`) wrap - a `RequestSender` to provide typed request methods. + Both `Dispatcher` (top-level outbound) and `DispatchContext` (back-channel + during an inbound request) extend this. `PeerMixin` wraps an `Outbound` to + provide typed MCP request/notification methods. """ async def send_request( @@ -75,74 +76,6 @@ class RequestSender(Protocol): method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None, - ) -> dict[str, Any]: ... - - -class DispatchContext(Protocol[TransportT_co]): - """Per-request context handed to ``on_call`` / ``on_notify``. - - Carries the transport metadata for the inbound message and provides the - back-channel for sending requests/notifications to the peer while handling - it. - """ - - @property - def transport(self) -> TransportT_co: - """Transport-specific metadata for this inbound message.""" - ... - - @property - def cancel_requested(self) -> anyio.Event: - """Set when the peer sends ``notifications/cancelled`` for this request.""" - ... - - async def notify(self, method: str, params: Mapping[str, Any] | None) -> None: - """Send a notification to the peer.""" - ... - - async def send_request( - self, - method: str, - params: Mapping[str, Any] | None, - opts: CallOptions | None = None, - ) -> dict[str, Any]: - """Send a request to the peer on the back-channel and await its result. - - Raises: - NoBackChannelError: if ``transport.can_send_request`` is ``False``. - """ - ... - - async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: - """Report progress for the inbound request, if the peer supplied a progress token. - - A no-op when no token was supplied. - """ - ... - - -OnCall = Callable[[DispatchContext[TransportContext], str, Mapping[str, Any] | None], Awaitable[dict[str, Any]]] -"""Handler for inbound requests: ``(ctx, method, params) -> result``. Raise ``MCPError`` to send an error response.""" - -OnNotify = Callable[[DispatchContext[TransportContext], str, Mapping[str, Any] | None], Awaitable[None]] -"""Handler for inbound notifications: ``(ctx, method, params)``.""" - -DispatchMiddleware = Callable[[OnCall], OnCall] -"""Wraps an ``OnCall`` to produce another ``OnCall``. Applied outermost-first.""" - - -class Dispatcher(Protocol[TransportT_co]): - """A duplex request/notification channel with call-return semantics. - - Implementations own correlation of outbound calls to inbound results, the - receive loop, per-request concurrency, and cancellation/progress wiring. - """ - - async def call( - self, - method: str, - params: Mapping[str, Any] | None, - opts: CallOptions | None = None, ) -> dict[str, Any]: """Send a request and await its result. @@ -157,11 +90,56 @@ class Dispatcher(Protocol[TransportT_co]): """Send a fire-and-forget notification.""" ... - async def run(self, on_call: OnCall, on_notify: OnNotify) -> None: + +class DispatchContext(Outbound, Protocol[TransportT_co]): + """Per-request context handed to ``on_request`` / ``on_notify``. + + Carries the transport metadata for the inbound message and provides the + back-channel for sending requests/notifications to the peer while handling + it. `send_request` raises `NoBackChannelError` if + ``transport.can_send_request`` is ``False``. + """ + + @property + def transport(self) -> TransportT_co: + """Transport-specific metadata for this inbound message.""" + ... + + @property + def cancel_requested(self) -> anyio.Event: + """Set when the peer sends ``notifications/cancelled`` for this request.""" + ... + + async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None: + """Report progress for the inbound request, if the peer supplied a progress token. + + A no-op when no token was supplied. + """ + ... + + +OnRequest = Callable[[DispatchContext[TransportContext], str, Mapping[str, Any] | None], Awaitable[dict[str, Any]]] +"""Handler for inbound requests: ``(ctx, method, params) -> result``. Raise ``MCPError`` to send an error response.""" + +OnNotify = Callable[[DispatchContext[TransportContext], str, Mapping[str, Any] | None], Awaitable[None]] +"""Handler for inbound notifications: ``(ctx, method, params)``.""" + +DispatchMiddleware = Callable[[OnRequest], OnRequest] +"""Wraps an ``OnRequest`` to produce another ``OnRequest``. Applied outermost-first.""" + + +class Dispatcher(Outbound, Protocol[TransportT_co]): + """A duplex request/notification channel with call-return semantics. + + Implementations own correlation of outbound requests to inbound results, the + receive loop, per-request concurrency, and cancellation/progress wiring. + """ + + async def run(self, on_request: OnRequest, on_notify: OnNotify) -> None: """Drive the receive loop until the underlying channel closes. - Each inbound request is dispatched to ``on_call`` in its own task; the - returned dict (or raised ``MCPError``) is sent back as the response. + Each inbound request is dispatched to ``on_request`` in its own task; + the returned dict (or raised ``MCPError``) is sent back as the response. Inbound notifications go to ``on_notify``. """ ... diff --git a/tests/shared/test_dispatcher.py b/tests/shared/test_dispatcher.py index ddfe1f79..44ab622a 100644 --- a/tests/shared/test_dispatcher.py +++ b/tests/shared/test_dispatcher.py @@ -13,7 +13,7 @@ import anyio import pytest from mcp.shared.direct_dispatcher import DirectDispatcher, create_direct_dispatcher_pair -from mcp.shared.dispatcher import DispatchContext, Dispatcher, OnCall, OnNotify +from mcp.shared.dispatcher import DispatchContext, Dispatcher, OnNotify, OnRequest, Outbound from mcp.shared.exceptions import MCPError, NoBackChannelError from mcp.shared.transport_context import TransportContext from mcp.types import INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, REQUEST_TIMEOUT @@ -21,17 +21,17 @@ from mcp.types import INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, REQUEST_T class Recorder: def __init__(self) -> None: - self.calls: list[tuple[str, Mapping[str, Any] | None]] = [] + self.requests: list[tuple[str, Mapping[str, Any] | None]] = [] self.notifications: list[tuple[str, Mapping[str, Any] | None]] = [] self.contexts: list[DispatchContext[TransportContext]] = [] self.notified = anyio.Event() -def echo_handlers(recorder: Recorder) -> tuple[OnCall, OnNotify]: - async def on_call( +def echo_handlers(recorder: Recorder) -> tuple[OnRequest, OnNotify]: + async def on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: - recorder.calls.append((method, params)) + recorder.requests.append((method, params)) recorder.contexts.append(ctx) return {"echoed": method, "params": dict(params or {})} @@ -39,26 +39,26 @@ def echo_handlers(recorder: Recorder) -> tuple[OnCall, OnNotify]: recorder.notifications.append((method, params)) recorder.notified.set() - return on_call, on_notify + return on_request, on_notify @asynccontextmanager async def running_pair( *, - server_on_call: OnCall | None = None, + server_on_request: OnRequest | None = None, server_on_notify: OnNotify | None = None, - client_on_call: OnCall | None = None, + client_on_request: OnRequest | None = None, client_on_notify: OnNotify | None = None, can_send_request: bool = True, ) -> AsyncIterator[tuple[DirectDispatcher, DirectDispatcher, Recorder, Recorder]]: """Yield ``(client, server, client_recorder, server_recorder)`` with both ``run()`` loops live.""" client, server = create_direct_dispatcher_pair(can_send_request=can_send_request) client_rec, server_rec = Recorder(), Recorder() - c_call, c_notify = echo_handlers(client_rec) - s_call, s_notify = echo_handlers(server_rec) + c_req, c_notify = echo_handlers(client_rec) + s_req, s_notify = echo_handlers(server_rec) async with anyio.create_task_group() as tg: - tg.start_soon(client.run, client_on_call or c_call, client_on_notify or c_notify) - tg.start_soon(server.run, server_on_call or s_call, server_on_notify or s_notify) + tg.start_soon(client.run, client_on_request or c_req, client_on_notify or c_notify) + tg.start_soon(server.run, server_on_request or s_req, server_on_notify or s_notify) try: yield client, server, client_rec, server_rec finally: @@ -67,53 +67,53 @@ async def running_pair( @pytest.mark.anyio -async def test_call_returns_result_from_peer_on_call(): +async def test_send_request_returns_result_from_peer_on_request(): async with running_pair() as (client, _server, _crec, srec): with anyio.fail_after(5): - result = await client.call("tools/list", {"cursor": "abc"}) + result = await client.send_request("tools/list", {"cursor": "abc"}) assert result == {"echoed": "tools/list", "params": {"cursor": "abc"}} - assert srec.calls == [("tools/list", {"cursor": "abc"})] + assert srec.requests == [("tools/list", {"cursor": "abc"})] @pytest.mark.anyio -async def test_call_reraises_mcperror_from_handler_unchanged(): - async def on_call( +async def test_send_request_reraises_mcperror_from_handler_unchanged(): + async def on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: raise MCPError(code=INVALID_PARAMS, message="bad cursor") - async with running_pair(server_on_call=on_call) as (client, *_): + async with running_pair(server_on_request=on_request) as (client, *_): with anyio.fail_after(5), pytest.raises(MCPError) as exc: - await client.call("tools/list", {}) + await client.send_request("tools/list", {}) assert exc.value.error.code == INVALID_PARAMS assert exc.value.error.message == "bad cursor" @pytest.mark.anyio -async def test_call_wraps_non_mcperror_exception_as_internal_error(): - async def on_call( +async def test_send_request_wraps_non_mcperror_exception_as_internal_error(): + async def on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: raise ValueError("oops") - async with running_pair(server_on_call=on_call) as (client, *_): + async with running_pair(server_on_request=on_request) as (client, *_): with anyio.fail_after(5), pytest.raises(MCPError) as exc: - await client.call("tools/list", {}) + await client.send_request("tools/list", {}) assert exc.value.error.code == INTERNAL_ERROR assert isinstance(exc.value.__cause__, ValueError) @pytest.mark.anyio -async def test_call_with_timeout_raises_mcperror_request_timeout(): - async def on_call( +async def test_send_request_with_timeout_raises_mcperror_request_timeout(): + async def on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: await anyio.sleep_forever() raise NotImplementedError - async with running_pair(server_on_call=on_call) as (client, *_): + async with running_pair(server_on_request=on_request) as (client, *_): with anyio.fail_after(5), pytest.raises(MCPError) as exc: - await client.call("slow", None, {"timeout": 0}) + await client.send_request("slow", None, {"timeout": 0}) assert exc.value.error.code == REQUEST_TIMEOUT @@ -128,53 +128,53 @@ async def test_notify_invokes_peer_on_notify(): @pytest.mark.anyio async def test_ctx_send_request_round_trips_to_calling_side(): - """A handler's ctx.send_request reaches the side that made the inbound call.""" + """A handler's ctx.send_request reaches the side that made the inbound request.""" - async def server_on_call( + async def server_on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: sample = await ctx.send_request("sampling/createMessage", {"prompt": "hi"}) return {"sampled": sample} - async with running_pair(server_on_call=server_on_call) as (client, _server, crec, _srec): + async with running_pair(server_on_request=server_on_request) as (client, _server, crec, _srec): with anyio.fail_after(5): - result = await client.call("tools/call", None) - assert crec.calls == [("sampling/createMessage", {"prompt": "hi"})] + result = await client.send_request("tools/call", None) + assert crec.requests == [("sampling/createMessage", {"prompt": "hi"})] assert result == {"sampled": {"echoed": "sampling/createMessage", "params": {"prompt": "hi"}}} @pytest.mark.anyio async def test_ctx_send_request_raises_nobackchannelerror_when_transport_disallows(): - async def server_on_call( + async def server_on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: return await ctx.send_request("sampling/createMessage", None) - async with running_pair(server_on_call=server_on_call, can_send_request=False) as (client, *_): + async with running_pair(server_on_request=server_on_request, can_send_request=False) as (client, *_): with anyio.fail_after(5), pytest.raises(NoBackChannelError) as exc: - await client.call("tools/call", None) + await client.send_request("tools/call", None) assert exc.value.method == "sampling/createMessage" assert exc.value.error.code == INVALID_REQUEST @pytest.mark.anyio async def test_ctx_notify_invokes_calling_side_on_notify(): - async def server_on_call( + async def server_on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: await ctx.notify("notifications/message", {"level": "info"}) return {} - async with running_pair(server_on_call=server_on_call) as (client, _server, crec, _srec): + async with running_pair(server_on_request=server_on_request) as (client, _server, crec, _srec): with anyio.fail_after(5): - await client.call("tools/call", None) + await client.send_request("tools/call", None) await crec.notified.wait() assert crec.notifications == [("notifications/message", {"level": "info"})] @pytest.mark.anyio async def test_ctx_progress_invokes_caller_on_progress_callback(): - async def server_on_call( + async def server_on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: await ctx.progress(0.5, total=1.0, message="halfway") @@ -185,27 +185,27 @@ async def test_ctx_progress_invokes_caller_on_progress_callback(): async def on_progress(progress: float, total: float | None, message: str | None) -> None: received.append((progress, total, message)) - async with running_pair(server_on_call=server_on_call) as (client, *_): + async with running_pair(server_on_request=server_on_request) as (client, *_): with anyio.fail_after(5): - await client.call("tools/call", None, {"on_progress": on_progress}) + await client.send_request("tools/call", None, {"on_progress": on_progress}) assert received == [(0.5, 1.0, "halfway")] @pytest.mark.anyio -async def test_call_issued_before_peer_run_blocks_until_peer_ready(): +async def test_send_request_issued_before_peer_run_blocks_until_peer_ready(): client, server = create_direct_dispatcher_pair() - s_call, s_notify = echo_handlers(Recorder()) - c_call, c_notify = echo_handlers(Recorder()) + s_req, s_notify = echo_handlers(Recorder()) + c_req, c_notify = echo_handlers(Recorder()) async def late_start(): await anyio.sleep(0) - await server.run(s_call, s_notify) + await server.run(s_req, s_notify) async with anyio.create_task_group() as tg: - tg.start_soon(client.run, c_call, c_notify) + tg.start_soon(client.run, c_req, c_notify) tg.start_soon(late_start) with anyio.fail_after(5): - result = await client.call("ping", None) + result = await client.send_request("ping", None) assert result == {"echoed": "ping", "params": {}} client.close() server.close() @@ -213,23 +213,23 @@ async def test_call_issued_before_peer_run_blocks_until_peer_ready(): @pytest.mark.anyio async def test_ctx_progress_is_noop_when_caller_supplied_no_callback(): - async def server_on_call( + async def server_on_request( ctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None ) -> dict[str, Any]: await ctx.progress(0.5) return {"ok": True} - async with running_pair(server_on_call=server_on_call) as (client, *_): + async with running_pair(server_on_request=server_on_request) as (client, *_): with anyio.fail_after(5): - result = await client.call("tools/call", None) + result = await client.send_request("tools/call", None) assert result == {"ok": True} @pytest.mark.anyio -async def test_call_and_notify_raise_runtimeerror_when_no_peer_connected(): +async def test_send_request_and_notify_raise_runtimeerror_when_no_peer_connected(): d = DirectDispatcher(TransportContext(kind="direct", can_send_request=True)) with pytest.raises(RuntimeError, match="no peer"): - await d.call("ping", None) + await d.send_request("ping", None) with pytest.raises(RuntimeError, match="no peer"): await d.notify("ping", None) @@ -237,16 +237,15 @@ async def test_call_and_notify_raise_runtimeerror_when_no_peer_connected(): @pytest.mark.anyio async def test_close_makes_run_return(): client, server = create_direct_dispatcher_pair() - on_call, on_notify = echo_handlers(Recorder()) + on_request, on_notify = echo_handlers(Recorder()) with anyio.fail_after(5): async with anyio.create_task_group() as tg: - tg.start_soon(server.run, on_call, on_notify) - tg.start_soon(client.run, on_call, on_notify) + tg.start_soon(server.run, on_request, on_notify) + tg.start_soon(client.run, on_request, on_notify) client.close() server.close() if TYPE_CHECKING: - _dispatcher_check: Dispatcher[TransportContext] = DirectDispatcher( - TransportContext(kind="direct", can_send_request=True) - ) + _d: Dispatcher[TransportContext] = DirectDispatcher(TransportContext(kind="direct", can_send_request=True)) + _o: Outbound = _d