"""Behavioral tests for the client-side `subscriptions/listen` driver (SDK-defined contract). Public API only, against in-process servers; wire-shape assertions live in the interaction suite. """ from itertools import count from typing import Any import anyio import mcp_types as types import pytest from mcp_types import SubscriptionFilter import mcp.client.subscriptions as subscriptions_module from mcp import Client, MCPError from mcp.client.session import ClientSession from mcp.client.subscriptions import ( ListenNotSupportedError, ListenRoute, PromptsListChanged, ResourcesListChanged, ResourceUpdated, ServerEvent, Subscription, SubscriptionLost, ToolsListChanged, listen, ) from mcp.server import Server, ServerRequestContext from mcp.server.subscriptions import ( SUBSCRIPTION_ID_META_KEY, InMemorySubscriptionBus, ListenHandler, ) from mcp.shared.direct_dispatcher import create_direct_dispatcher_pair from mcp.shared.dispatcher import CallOptions pytestmark = pytest.mark.anyio def _bus_server(bus: InMemorySubscriptionBus, *, max_subscriptions: int | None = None) -> Server[Any]: """A lowlevel server whose only feature is serving listen streams from `bus`.""" handler = ( ListenHandler(bus) if max_subscriptions is None else ListenHandler(bus, max_subscriptions=max_subscriptions) ) return Server("subs", on_subscriptions_listen=handler) async def _ack(ctx: ServerRequestContext[Any, Any], honored: SubscriptionFilter) -> dict[str, Any]: """Send a hand-rolled ack for a scripted listen handler; returns the stamped meta.""" assert ctx.request_id is not None meta: dict[str, Any] = {SUBSCRIPTION_ID_META_KEY: ctx.request_id} await ctx.session.send_notification( types.SubscriptionsAcknowledgedNotification( params=types.SubscriptionsAcknowledgedNotificationParams(notifications=honored, _meta=meta) ), related_request_id=ctx.request_id, ) return meta async def test_listen_surfaces_the_honored_filter_and_subscription_id(): """Entering waits for the server ack and surfaces the honored filter and subscription id.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with client.listen( # pragma: no branch tools_list_changed=True, resource_subscriptions=["note://todo"] ) as sub: assert isinstance(sub, Subscription) assert sub.honored.tools_list_changed is True assert sub.honored.resource_subscriptions == ["note://todo"] assert isinstance(sub.subscription_id, str) assert sub.subscription_id.startswith("listen-") async def test_listen_delivers_all_four_typed_event_kinds(): """Bus publishes come back as the same typed event values, in order.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with client.listen( # pragma: no branch tools_list_changed=True, prompts_list_changed=True, resources_list_changed=True, resource_subscriptions=["note://todo"], ) as sub: for event in ( ToolsListChanged(), PromptsListChanged(), ResourcesListChanged(), ResourceUpdated(uri="note://todo"), ): await bus.publish(event) assert await anext(sub) == event async def test_unconsumed_duplicate_events_coalesce(): """Events are level triggers: duplicates pending consumption collapse to one.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with client.listen( # pragma: no branch tools_list_changed=True, resource_subscriptions=["note://todo"] ) as sub: for _ in range(3): await bus.publish(ToolsListChanged()) await bus.publish(ResourceUpdated(uri="note://todo")) await anyio.wait_all_tasks_blocked() assert await anext(sub) == ToolsListChanged() assert await anext(sub) == ResourceUpdated(uri="note://todo") async def test_graceful_server_close_ends_the_loop_cleanly(): """The server's deliberate close ends iteration cleanly, after draining prior events.""" bus = InMemorySubscriptionBus() handler = ListenHandler(bus) server = Server("subs", on_subscriptions_listen=handler) events: list[object] = [] async with Client(server) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch await bus.publish(ToolsListChanged()) handler.close() events.extend([event async for event in sub]) assert events == [ToolsListChanged()] async def test_abrupt_stream_end_raises_subscription_lost(): """A stream dying without the graceful result raises `SubscriptionLost` with the cause chained.""" proceed = anyio.Event() async def dropping_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: await _ack(ctx, params.notifications) await proceed.wait() raise MCPError(types.INTERNAL_ERROR, "stream torn down") server = Server("subs", on_subscriptions_listen=dropping_listen) async with Client(server) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch proceed.set() with pytest.raises(SubscriptionLost) as exc_info: # pragma: no branch await anext(sub) assert isinstance(exc_info.value.__cause__, MCPError) assert exc_info.value.__cause__.error.message == "stream torn down" async def test_listen_on_a_legacy_connection_raises_the_typed_steer(): """On a 2025 connection `listen` fails fast with the typed error steering to the legacy verbs.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus), mode="legacy") as client: with anyio.fail_after(5): # Entering is where the guard fires; __aenter__ directly avoids an unreachable with-body. with pytest.raises(ListenNotSupportedError) as exc_info: # pragma: no branch await client.listen(tools_list_changed=True).__aenter__() assert exc_info.value.negotiated_version == "2025-11-25" assert "subscribe_resource" in str(exc_info.value) async def test_server_rejection_raises_from_enter_not_from_iteration(): """A server without the listen handler fails the open from entering the context.""" server = Server("no-listen") async with Client(server) as client: with anyio.fail_after(5): with pytest.raises(MCPError) as exc_info: # pragma: no branch await client.listen(tools_list_changed=True).__aenter__() assert exc_info.value.error.code == types.METHOD_NOT_FOUND async def test_immediate_result_without_ack_opens_already_closed(): """A bare result with no ack yields a subscription already gracefully over: no filter, no events.""" async def degenerate_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: assert ctx.request_id is not None return types.SubscriptionsListenResult(_meta={SUBSCRIPTION_ID_META_KEY: ctx.request_id}) server = Server("subs", on_subscriptions_listen=degenerate_listen) async with Client(server) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch assert sub.honored == SubscriptionFilter() with pytest.raises(StopAsyncIteration): # pragma: no branch await anext(sub) async def test_server_sent_cancelled_for_the_listen_id_raises_subscription_lost(): """Server-sent notifications/cancelled for the listen id surfaces as a lost subscription.""" proceed = anyio.Event() async def cancelling_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: assert ctx.request_id is not None await _ack(ctx, params.notifications) await proceed.wait() await ctx.session.send_notification( types.CancelledNotification(params=types.CancelledNotificationParams(request_id=ctx.request_id)), related_request_id=ctx.request_id, ) await anyio.sleep_forever() raise AssertionError("unreachable") # pragma: no cover server = Server("subs", on_subscriptions_listen=cancelling_listen) async with Client(server) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch proceed.set() with pytest.raises(SubscriptionLost): # pragma: no branch await anext(sub) async def test_exiting_the_context_frees_the_server_slot(): """Leaving the block ends the subscription server-side: a one-slot handler admits a second listen.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus, max_subscriptions=1)) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as first: assert first.honored.tools_list_changed is True async with client.listen(tools_list_changed=True) as second: # pragma: no branch assert second.honored.tools_list_changed is True assert second.subscription_id != first.subscription_id async def test_concurrent_subscriptions_demux_independently(): """Two open subscriptions each receive only their own filter's events.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with ( # pragma: no branch client.listen(tools_list_changed=True) as tools_sub, client.listen(resource_subscriptions=["note://todo"]) as notes_sub, ): await bus.publish(ToolsListChanged()) await bus.publish(ResourceUpdated(uri="note://todo")) assert await anext(tools_sub) == ToolsListChanged() assert await anext(notes_sub) == ResourceUpdated(uri="note://todo") # Neither stream received the other's event. await bus.publish(ToolsListChanged()) assert await anext(tools_sub) == ToolsListChanged() async def test_change_notifications_still_reach_message_handler(): """The demux tees: a delivered event's notification still reaches message_handler; the ack never does.""" bus = InMemorySubscriptionBus() seen: list[str] = [] async def on_message(message: object) -> None: assert not isinstance(message, types.SubscriptionsAcknowledgedNotification) if isinstance(message, types.ToolListChangedNotification): # pragma: no branch seen.append("tools-changed") async with Client(_bus_server(bus), message_handler=on_message) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch await bus.publish(ToolsListChanged()) assert await anext(sub) == ToolsListChanged() await anyio.wait_all_tasks_blocked() assert seen == ["tools-changed"] async def test_enter_times_out_when_the_ack_never_arrives(): """The ack wait rides the session's read timeout, so a wedged server cannot hang the open.""" async def silent_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: await anyio.sleep_forever() raise AssertionError("unreachable") # pragma: no cover server = Server("subs", on_subscriptions_listen=silent_listen) async with Client(server, read_timeout_seconds=0.05) as client: with anyio.fail_after(5): with pytest.raises(TimeoutError): # pragma: no branch await client.listen(tools_list_changed=True).__aenter__() async def test_an_open_stream_outlives_the_session_read_timeout(): """The listen request is exempt from the read timeout: the stream delivers after the deadline.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus), read_timeout_seconds=0.05) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch # Real clock on purpose: this pins a timeout feature. await anyio.sleep(0.2) await bus.publish(ToolsListChanged()) assert await anext(sub) == ToolsListChanged() async def test_a_duplicate_ack_does_not_overwrite_the_honored_filter(): """The first ack wins; a later conflicting ack is a no-op.""" proceed = anyio.Event() async def double_acking_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: assert ctx.request_id is not None await _ack(ctx, params.notifications) await _ack(ctx, SubscriptionFilter()) await proceed.wait() return types.SubscriptionsListenResult(_meta={SUBSCRIPTION_ID_META_KEY: ctx.request_id}) server = Server("subs", on_subscriptions_listen=double_acking_listen) async with Client(server) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch assert sub.honored.tools_list_changed is True proceed.set() async def test_a_non_event_frame_with_the_subscription_id_is_teed_not_delivered(): """A stamped non-event notification never surfaces as an event; it flows to message_handler.""" proceed = anyio.Event() async def logging_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: assert ctx.request_id is not None meta = await _ack(ctx, params.notifications) await ctx.session.send_notification( types.LoggingMessageNotification( params=types.LoggingMessageNotificationParams(level="info", data="not an event", _meta=meta) ), related_request_id=ctx.request_id, ) await proceed.wait() return types.SubscriptionsListenResult(_meta=meta) logged: list[str] = [] async def on_message(message: object) -> None: if isinstance(message, types.LoggingMessageNotification): # pragma: no branch logged.append(str(message.params.data)) server = Server("subs", on_subscriptions_listen=logging_listen) async with Client(server, message_handler=on_message) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch await anyio.wait_all_tasks_blocked() proceed.set() with pytest.raises(StopAsyncIteration): # pragma: no branch await anext(sub) assert logged == ["not an event"] async def test_session_teardown_unblocks_a_sibling_consumer_with_subscription_lost(): """Session teardown settles every open route as lost, unblocking parked consumers.""" bus = InMemorySubscriptionBus() outcome: list[str] = [] entered = anyio.Event() async def consume(client: Client) -> None: with pytest.raises(SubscriptionLost): async with client.listen(tools_list_changed=True) as sub: entered.set() await anext(sub) outcome.append("lost") with anyio.fail_after(5): async with anyio.create_task_group() as tg: async with Client(_bus_server(bus)) as client: # pragma: no branch tg.start_soon(consume, client) await entered.wait() assert outcome == ["lost"] async def test_server_cancel_before_the_ack_raises_subscription_lost_from_enter(): """A stream torn down before it was ever acknowledged is a failed open: enter raises.""" async def cancel_first_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: assert ctx.request_id is not None await ctx.session.send_notification( types.CancelledNotification(params=types.CancelledNotificationParams(request_id=ctx.request_id)), related_request_id=ctx.request_id, ) await anyio.sleep_forever() raise AssertionError("unreachable") # pragma: no cover server = Server("subs", on_subscriptions_listen=cancel_first_listen) async with Client(server) as client: with anyio.fail_after(5): with pytest.raises(SubscriptionLost, match="before it was acknowledged"): # pragma: no branch await client.listen(tools_list_changed=True).__aenter__() async def test_listen_on_an_exited_session_raises_and_leaks_no_route(): """Opening on an exited session fails loudly and leaves no demux registration behind.""" bus = InMemorySubscriptionBus() client = Client(_bus_server(bus)) async with client: session = client.session with pytest.raises(RuntimeError): await listen(session, tools_list_changed=True).__aenter__() assert session._listen_routes == {} # pyright: ignore[reportPrivateUsage] async def test_listen_on_a_never_entered_session_raises_runtime_error(): """An adopted-but-never-entered session has no task group to drive the stream.""" dispatcher, _peer = create_direct_dispatcher_pair() session = ClientSession(dispatcher=dispatcher) session.adopt( types.DiscoverResult( supported_versions=["2026-07-28"], capabilities=types.ServerCapabilities(), server_info=types.Implementation(name="stub", version="0"), ) ) with pytest.raises(RuntimeError, match="entered session"): await listen(session, tools_list_changed=True).__aenter__() assert session._listen_routes == {} # pyright: ignore[reportPrivateUsage] async def test_a_retained_handle_after_exit_does_not_serve_stale_events(): """Leaving the block abandons the backlog: a stashed handle must not replay buffered events.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: await bus.publish(ToolsListChanged()) await anyio.wait_all_tasks_blocked() with pytest.raises(StopAsyncIteration): # pragma: no branch await anext(sub) async def test_a_stray_ack_outside_the_driver_namespace_still_reaches_message_handler(): """Acks for ids the driver never minted flow to message_handler (the raw-listen escape hatch).""" proceed = anyio.Event() async def stray_acking_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: assert ctx.request_id is not None await _ack(ctx, params.notifications) await ctx.session.send_notification( types.SubscriptionsAcknowledgedNotification( params=types.SubscriptionsAcknowledgedNotificationParams( notifications=SubscriptionFilter(), _meta={SUBSCRIPTION_ID_META_KEY: 424242} ) ), related_request_id=ctx.request_id, ) await proceed.wait() return types.SubscriptionsListenResult(_meta={SUBSCRIPTION_ID_META_KEY: ctx.request_id}) handled: list[str] = [] async def on_message(message: object) -> None: handled.append(type(message).__name__) server = Server("subs", on_subscriptions_listen=stray_acking_listen) async with Client(server, message_handler=on_message) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch await anyio.wait_all_tasks_blocked() proceed.set() with pytest.raises(StopAsyncIteration): # pragma: no branch await anext(sub) assert "SubscriptionsAcknowledgedNotification" in handled async def test_a_bare_string_for_resource_subscriptions_is_rejected(): """A bare string would explode into per-character URIs; it is rejected before touching the wire.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as client: with pytest.raises(TypeError, match="sequence of URIs"): await client.listen(resource_subscriptions="note://todo").__aenter__() # pyright: ignore[reportArgumentType] def test_the_route_admits_only_honored_events_and_only_while_live(): """Route admission: nothing before the ack, only honored events while live, nothing after the end.""" route = ListenRoute() route.deliver(ToolsListChanged()) assert route._pending == {} # pyright: ignore[reportPrivateUsage] route.set_acked(SubscriptionFilter(tools_list_changed=True, resource_subscriptions=["note://todo"])) route.deliver(PromptsListChanged()) # kind not honored route.deliver(ResourceUpdated(uri="note://todo/draft")) # sub-resource of a subscribed URI: spec says admit route.deliver(ResourceUpdated(uri="note://todo")) route.deliver(ToolsListChanged()) route.deliver(ToolsListChanged()) # duplicate pending consumption collapses assert list(route._pending) == [ # pyright: ignore[reportPrivateUsage] ResourceUpdated(uri="note://todo/draft"), ResourceUpdated(uri="note://todo"), ToolsListChanged(), ] route.settle("graceful") route.deliver(ResourceUpdated(uri="note://todo")) # post-close noise is refused assert len(route._pending) == 3 # pyright: ignore[reportPrivateUsage] def test_a_peer_flooding_distinct_uris_costs_the_subscription_not_client_memory(): """A peer flooding distinct URIs trips the `_MAX_PENDING_EVENTS` backstop: the route settles lost instead of growing client memory without bound.""" route = ListenRoute() route.set_acked(SubscriptionFilter(resource_subscriptions=["note://todo"])) for n in range(subscriptions_module._MAX_PENDING_EVENTS): # pyright: ignore[reportPrivateUsage] route.deliver(ResourceUpdated(uri=f"note://todo/{n}")) assert route.end is None route.deliver(ResourceUpdated(uri="note://todo/one-too-many")) assert route.end == "lost" assert route.error is not None assert "backlog" in route.error.error.message # The overflowing event was not queued. assert len(route._pending) == subscriptions_module._MAX_PENDING_EVENTS # pyright: ignore[reportPrivateUsage] async def test_a_cancelled_on_event_barrier_does_not_lose_the_event(): """Cancelling `anext` mid-barrier leaves the event queued; the next `anext` re-runs the idempotent barrier and returns it.""" bus = InMemorySubscriptionBus() entered = anyio.Event() release = anyio.Event() async def parked_barrier(event: ServerEvent) -> None: entered.set() await release.wait() async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with listen( client.session, tools_list_changed=True, on_event=parked_barrier ) as sub: # pragma: no branch await bus.publish(ToolsListChanged()) async with anyio.create_task_group() as tg: cancel_scope = anyio.CancelScope() async def first_attempt() -> None: with cancel_scope: await anext(sub) raise AssertionError("must be cancelled mid-barrier") # pragma: no cover tg.start_soon(first_attempt) await entered.wait() cancel_scope.cancel() release.set() assert await anext(sub) == ToolsListChanged() async def test_events_outside_the_honored_filter_are_never_delivered(): """A server violating its acknowledged filter cannot reach the consumer or grow the backlog.""" proceed = anyio.Event() async def overreaching_listen( ctx: ServerRequestContext[Any, Any], params: types.SubscriptionsListenRequestParams ) -> types.SubscriptionsListenResult: meta = await _ack(ctx, params.notifications) # honors exactly what was requested: tools only await ctx.session.send_notification( types.ResourceUpdatedNotification( params=types.ResourceUpdatedNotificationParams(uri="note://uninvited", _meta=meta) ), related_request_id=ctx.request_id, ) await ctx.session.send_notification( types.ToolListChangedNotification(params=types.NotificationParams(_meta=meta)), related_request_id=ctx.request_id, ) await proceed.wait() return types.SubscriptionsListenResult(_meta=meta) server = Server("subs", on_subscriptions_listen=overreaching_listen) async with Client(server) as client: with anyio.fail_after(5): async with client.listen(tools_list_changed=True) as sub: # pragma: no branch assert await anext(sub) == ToolsListChanged() proceed.set() with pytest.raises(StopAsyncIteration): # pragma: no branch await anext(sub) async def test_the_on_event_barrier_completes_before_each_event_is_returned(): """`on_event` is awaited before the iterator returns each event (the Client wires cache eviction here).""" bus = InMemorySubscriptionBus() order: list[str] = [] async def barrier(event: ServerEvent) -> None: order.append(f"barrier:{type(event).__name__}") async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with listen(client.session, tools_list_changed=True, on_event=barrier) as sub: # pragma: no branch await bus.publish(ToolsListChanged()) event = await anext(sub) order.append(f"returned:{type(event).__name__}") assert order == ["barrier:ToolsListChanged", "returned:ToolsListChanged"] async def test_client_listen_installs_the_cache_eviction_barrier_exactly_when_a_cache_exists(): """`Client.listen` wires the response-cache evictor as the barrier only when a cache exists.""" bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as cached_client: with anyio.fail_after(5): async with cached_client.listen(tools_list_changed=True) as sub: # pragma: no branch assert sub._on_event == cached_client._evict_for_listen_event # pyright: ignore[reportPrivateUsage] async with Client(_bus_server(bus), cache=False) as uncached_client: with anyio.fail_after(5): async with uncached_client.listen(tools_list_changed=True) as sub: # pragma: no branch assert sub._on_event is None # pyright: ignore[reportPrivateUsage] async def test_the_cache_eviction_barrier_maps_events_and_contains_store_faults( monkeypatch: pytest.MonkeyPatch, ) -> None: """The barrier evicts through the same notification mapping as the message_handler wrapper; a raising store costs a log line, not the delivery.""" client = Client(_bus_server(InMemorySubscriptionBus())) cache = client._response_cache # pyright: ignore[reportPrivateUsage] assert cache is not None evicted: list[types.ServerNotification] = [] async def record(notification: types.ServerNotification) -> None: evicted.append(notification) monkeypatch.setattr(cache, "evict_for_notification", record) await client._evict_for_listen_event(ResourceUpdated(uri="note://x")) # pyright: ignore[reportPrivateUsage] assert isinstance(evicted[0], types.ResourceUpdatedNotification) assert evicted[0].params.uri == "note://x" async def broken(notification: types.ServerNotification) -> None: raise RuntimeError("store down") monkeypatch.setattr(cache, "evict_for_notification", broken) # Contained: a cache fault must not block delivery. await client._evict_for_listen_event(ToolsListChanged()) # pyright: ignore[reportPrivateUsage] async def test_a_raw_request_id_collision_fails_the_subscription_not_the_session( monkeypatch: pytest.MonkeyPatch, ) -> None: """A raw caller occupying the driver's next minted id fails that one listen from enter; the session survives and the next listen opens normally.""" monkeypatch.setattr(subscriptions_module, "_listen_ids", count(7000)) bus = InMemorySubscriptionBus() async with Client(_bus_server(bus)) as client: with anyio.fail_after(5): async with anyio.create_task_group() as tg: # pragma: no branch raw_scope = anyio.CancelScope() async def raw_listen() -> None: request = types.SubscriptionsListenRequest( params=types.SubscriptionsListenRequestParams( notifications=SubscriptionFilter(tools_list_changed=True) ) ) data = request.model_dump(by_alias=True, mode="json", exclude_none=True) opts: CallOptions = {"request_id": "listen-7000"} client.session._stamp(data, opts) # pyright: ignore[reportPrivateUsage] with raw_scope: await client.session._dispatcher.send_raw_request( # pyright: ignore[reportPrivateUsage] data["method"], data.get("params"), opts ) tg.start_soon(raw_listen) await anyio.wait_all_tasks_blocked() with pytest.raises(MCPError) as exc_info: await client.listen(tools_list_changed=True).__aenter__() assert "already in flight" in exc_info.value.error.message # The failed open released the colliding id's demux registration. assert client.session._listen_routes == {} # pyright: ignore[reportPrivateUsage] raw_scope.cancel() async with client.listen(tools_list_changed=True) as sub: # pragma: no branch assert sub.subscription_id == "listen-7001"