|
8 | 8 | import anyio.abc |
9 | 9 | import anyio.streams.memory |
10 | 10 | import pytest |
| 11 | +from pydantic import FileUrl |
11 | 12 |
|
12 | 13 | from mcp import types |
13 | 14 | from mcp.client import ClientRequestContext |
@@ -972,18 +973,54 @@ async def server_on_notify( |
972 | 973 |
|
973 | 974 | session = ClientSession(dispatcher=client_side) |
974 | 975 | results: list[types.EmptyResult] = [] |
975 | | - async with anyio.create_task_group() as tg: |
976 | | - await tg.start(server_side.run, server_on_request, server_on_notify) |
977 | | - async with session: |
978 | | - results.append(await session.send_ping(meta=None)) |
979 | | - # Server-to-client: direct dispatch delivers ping with no params member (no _meta injection). |
980 | | - assert await server_side.send_raw_request("ping", None) == {} |
981 | | - await session.send_notification(types.RootsListChangedNotification()) |
982 | | - server_side.close() |
| 976 | + with anyio.fail_after(5): |
| 977 | + async with anyio.create_task_group() as tg: |
| 978 | + await tg.start(server_side.run, server_on_request, server_on_notify) |
| 979 | + async with session: |
| 980 | + results.append(await session.send_ping(meta=None)) |
| 981 | + # Server-to-client: direct dispatch delivers ping with no params member (no _meta injection). |
| 982 | + assert await server_side.send_raw_request("ping", None) == {} |
| 983 | + await session.send_notification(types.RootsListChangedNotification()) |
| 984 | + server_side.close() |
983 | 985 | assert results == [types.EmptyResult()] |
984 | 986 | assert notified == ["notifications/roots/list_changed"] |
985 | 987 |
|
986 | 988 |
|
| 989 | +@pytest.mark.anyio |
| 990 | +async def test_direct_dispatch_roots_list_reaches_callback_with_synthesized_request_id(): |
| 991 | + """A server-initiated roots/list over dispatcher= reaches the registered callback and round-trips |
| 992 | + the result; the callback context carries an int request_id (SDK-defined: DirectDispatcher |
| 993 | + synthesizes ids).""" |
| 994 | + client_side, server_side = create_direct_dispatcher_pair() |
| 995 | + contexts: list[ClientRequestContext] = [] |
| 996 | + |
| 997 | + async def list_roots(context: ClientRequestContext) -> types.ListRootsResult: |
| 998 | + contexts.append(context) |
| 999 | + return types.ListRootsResult(roots=[types.Root(uri=FileUrl("file:///workspace"))]) |
| 1000 | + |
| 1001 | + async def server_on_request( |
| 1002 | + ctx: DispatchContext[TransportContext], method: str, params: dict[str, object] | None |
| 1003 | + ) -> dict[str, object]: |
| 1004 | + raise NotImplementedError |
| 1005 | + |
| 1006 | + async def server_on_notify( |
| 1007 | + ctx: DispatchContext[TransportContext], method: str, params: dict[str, object] | None |
| 1008 | + ) -> None: |
| 1009 | + raise NotImplementedError |
| 1010 | + |
| 1011 | + session = ClientSession(dispatcher=client_side, list_roots_callback=list_roots) |
| 1012 | + result: dict[str, Any] | None = None |
| 1013 | + with anyio.fail_after(5): |
| 1014 | + async with anyio.create_task_group() as tg: |
| 1015 | + await tg.start(server_side.run, server_on_request, server_on_notify) |
| 1016 | + async with session: |
| 1017 | + result = await server_side.send_raw_request("roots/list", None) |
| 1018 | + server_side.close() |
| 1019 | + assert result == {"roots": [{"uri": "file:///workspace"}]} |
| 1020 | + assert len(contexts) == 1 |
| 1021 | + assert isinstance(contexts[0].request_id, int) |
| 1022 | + |
| 1023 | + |
987 | 1024 | @pytest.mark.anyio |
988 | 1025 | async def test_initialize_opts_out_of_cancel_on_abandon_while_other_requests_leave_it_unset(): |
989 | 1026 | """`send_request` passes `cancel_on_abandon=False` for `initialize` — the spec forbids |
@@ -1021,9 +1058,10 @@ async def notify(self, method: str, params: Mapping[str, Any] | None) -> None: |
1021 | 1058 | pass |
1022 | 1059 |
|
1023 | 1060 | dispatcher = RecordingDispatcher() |
1024 | | - async with ClientSession(dispatcher=dispatcher) as session: |
1025 | | - await session.initialize() |
1026 | | - await session.send_ping() |
| 1061 | + with anyio.fail_after(5): |
| 1062 | + async with ClientSession(dispatcher=dispatcher) as session: |
| 1063 | + await session.initialize() |
| 1064 | + await session.send_ping() |
1027 | 1065 | opts_by_method = dict(dispatcher.calls) |
1028 | 1066 | assert opts_by_method["initialize"].get("cancel_on_abandon") is False |
1029 | 1067 | assert "cancel_on_abandon" not in opts_by_method["ping"] |
|
0 commit comments