@@ -946,3 +946,215 @@ async def elicitation_callback(
946946 assert result == snapshot (
947947 ReadResourceResult (contents = [TextResourceContents (uri = "memory://gated" , text = "unlocked" )])
948948 )
949+
950+
951+ # ---------- list_all_* pagination-draining helpers ----------
952+ #
953+ # Each helper drives the existing single-page list_* method through a
954+ # cursor loop and returns the flattened list. The fixtures return one
955+ # page per call in order; the helper under test must dispatch until the
956+ # server stops sending a next_cursor.
957+
958+
959+ def _make_tools (names : list [str ]) -> list [types .Tool ]:
960+ return [types .Tool (name = n , input_schema = {"type" : "object" }) for n in names ]
961+
962+
963+ def _make_resources (uris : list [str ]) -> list [types .Resource ]:
964+ return [types .Resource (uri = u , name = u .rsplit ("/" , 1 )[- 1 ]) for u in uris ]
965+
966+
967+ def _make_resource_templates (uris : list [str ]) -> list [types .ResourceTemplate ]:
968+ return [types .ResourceTemplate (uri_template = u , name = u .rsplit ("/" , 1 )[- 1 ]) for u in uris ]
969+
970+
971+ def _make_prompts (names : list [str ]) -> list [types .Prompt ]:
972+ return [types .Prompt (name = n ) for n in names ]
973+
974+
975+ async def test_list_all_tools_drains_pagination () -> None :
976+ """`list_all_tools` keeps calling `tools/list` with the previous next_cursor until the
977+ server returns a page with `next_cursor=None`, and returns the flattened list."""
978+
979+ pages : list [types .ListToolsResult ] = [
980+ types .ListToolsResult (tools = _make_tools (["a" , "b" ]), next_cursor = "p2" ),
981+ types .ListToolsResult (tools = _make_tools (["c" ]), next_cursor = "p3" ),
982+ types .ListToolsResult (tools = _make_tools (["d" , "e" ])),
983+ ]
984+
985+ async def on_list_tools (
986+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
987+ ) -> types .ListToolsResult :
988+ cursor = (params .cursor if params else None ) or "p1"
989+ idx = {"p1" : 0 , "p2" : 1 , "p3" : 2 }[cursor ]
990+ return pages [idx ]
991+
992+ server = Server ("test" , on_list_tools = on_list_tools )
993+
994+ with anyio .fail_after (5 ):
995+ async with Client (server ) as client :
996+ tools = await client .session .list_all_tools ()
997+ assert [t .name for t in tools ] == ["a" , "b" , "c" , "d" , "e" ]
998+
999+
1000+ async def test_list_all_tools_single_page_works () -> None :
1001+ """`list_all_tools` returns the single page's items when no cursor is involved."""
1002+
1003+ async def on_list_tools (
1004+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1005+ ) -> types .ListToolsResult :
1006+ return types .ListToolsResult (tools = _make_tools (["only" ]))
1007+
1008+ server = Server ("test" , on_list_tools = on_list_tools )
1009+
1010+ with anyio .fail_after (5 ):
1011+ async with Client (server ) as client :
1012+ tools = await client .session .list_all_tools ()
1013+ assert [t .name for t in tools ] == ["only" ]
1014+
1015+
1016+ async def test_list_all_tools_empty_returns_empty_list () -> None :
1017+ async def on_list_tools (
1018+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1019+ ) -> types .ListToolsResult :
1020+ return types .ListToolsResult (tools = [])
1021+
1022+ server = Server ("test" , on_list_tools = on_list_tools )
1023+
1024+ with anyio .fail_after (5 ):
1025+ async with Client (server ) as client :
1026+ tools = await client .session .list_all_tools ()
1027+ assert tools == []
1028+
1029+
1030+ async def test_list_all_resources_drains_pagination () -> None :
1031+ pages : list [types .ListResourcesResult ] = [
1032+ types .ListResourcesResult (resources = _make_resources (["memory://r1" , "memory://r2" ]), next_cursor = "p2" ),
1033+ types .ListResourcesResult (resources = _make_resources (["memory://r3" ])),
1034+ ]
1035+
1036+ async def on_list_resources (
1037+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1038+ ) -> types .ListResourcesResult :
1039+ cursor = (params .cursor if params else None ) or "p1"
1040+ return pages [0 ] if cursor == "p1" else pages [1 ]
1041+
1042+ server = Server ("test" , on_list_resources = on_list_resources )
1043+
1044+ with anyio .fail_after (5 ):
1045+ async with Client (server ) as client :
1046+ resources = await client .session .list_all_resources ()
1047+ assert [r .uri for r in resources ] == ["memory://r1" , "memory://r2" , "memory://r3" ]
1048+
1049+
1050+ async def test_list_all_resource_templates_drains_pagination () -> None :
1051+ pages : list [types .ListResourceTemplatesResult ] = [
1052+ types .ListResourceTemplatesResult (
1053+ resource_templates = _make_resource_templates (["tmpl://{a}" ]),
1054+ next_cursor = "p2" ,
1055+ ),
1056+ types .ListResourceTemplatesResult (resource_templates = _make_resource_templates (["tmpl://{b}" ])),
1057+ ]
1058+
1059+ async def on_list_resource_templates (
1060+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1061+ ) -> types .ListResourceTemplatesResult :
1062+ cursor = (params .cursor if params else None ) or "p1"
1063+ return pages [0 ] if cursor == "p1" else pages [1 ]
1064+
1065+ server = Server ("test" , on_list_resource_templates = on_list_resource_templates )
1066+
1067+ with anyio .fail_after (5 ):
1068+ async with Client (server ) as client :
1069+ templates = await client .session .list_all_resource_templates ()
1070+ assert [t .uri_template for t in templates ] == ["tmpl://{a}" , "tmpl://{b}" ]
1071+
1072+
1073+ async def test_list_all_prompts_drains_pagination () -> None :
1074+ pages : list [types .ListPromptsResult ] = [
1075+ types .ListPromptsResult (prompts = _make_prompts (["p1" ]), next_cursor = "p2" ),
1076+ types .ListPromptsResult (prompts = _make_prompts (["p2" , "p3" ])),
1077+ ]
1078+
1079+ async def on_list_prompts (
1080+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1081+ ) -> types .ListPromptsResult :
1082+ cursor = (params .cursor if params else None ) or "p1"
1083+ return pages [0 ] if cursor == "p1" else pages [1 ]
1084+
1085+ server = Server ("test" , on_list_prompts = on_list_prompts )
1086+
1087+ with anyio .fail_after (5 ):
1088+ async with Client (server ) as client :
1089+ prompts = await client .session .list_all_prompts ()
1090+ assert [p .name for p in prompts ] == ["p1" , "p2" , "p3" ]
1091+
1092+
1093+ async def test_list_all_helpers_do_not_share_cursor_state () -> None :
1094+ """Each `list_all_*` helper starts at cursor=None; calling one does not affect another's
1095+ pagination state (the helpers read no shared cursor, only the server's per-page next_cursor)."""
1096+
1097+ async def on_list_tools (
1098+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1099+ ) -> types .ListToolsResult :
1100+ return types .ListToolsResult (tools = _make_tools (["t1" ]))
1101+
1102+ async def on_list_prompts (
1103+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1104+ ) -> types .ListPromptsResult :
1105+ return types .ListPromptsResult (prompts = _make_prompts (["only-prompt" ]))
1106+
1107+ server = Server ("test" , on_list_tools = on_list_tools , on_list_prompts = on_list_prompts )
1108+
1109+ with anyio .fail_after (5 ):
1110+ async with Client (server ) as client :
1111+ tools = await client .session .list_all_tools ()
1112+ prompts = await client .session .list_all_prompts ()
1113+ assert [t .name for t in tools ] == ["t1" ]
1114+ assert [p .name for p in prompts ] == ["only-prompt" ]
1115+
1116+
1117+ async def test_list_all_tools_stops_when_server_sends_terminal_empty_page () -> None :
1118+ """A terminal page with cursor=None (even if zero items) ends the drain loop."""
1119+
1120+ pages : list [types .ListToolsResult ] = [
1121+ types .ListToolsResult (tools = _make_tools (["first" ]), next_cursor = "p2" ),
1122+ types .ListToolsResult (tools = [], next_cursor = None ),
1123+ types .ListToolsResult (tools = _make_tools (["never-returned" ])),
1124+ ]
1125+
1126+ async def on_list_tools (
1127+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1128+ ) -> types .ListToolsResult :
1129+ cursor = (params .cursor if params else None ) or "p1"
1130+ return pages [0 ] if cursor == "p1" else pages [1 ]
1131+
1132+ server = Server ("test" , on_list_tools = on_list_tools )
1133+
1134+ with anyio .fail_after (5 ):
1135+ async with Client (server ) as client :
1136+ tools = await client .session .list_all_tools ()
1137+ assert [t .name for t in tools ] == ["first" ]
1138+
1139+
1140+ async def test_list_all_helpers_pass_cursor_through_paginated_request_params () -> None :
1141+ """The helpers must hand the server's `next_cursor` back as `params.cursor` on the next call
1142+ (not as a positional arg or otherwise mangled). Server-side capture proves it."""
1143+
1144+ captured : list [str | None ] = []
1145+
1146+ async def on_list_tools (
1147+ ctx : ServerRequestContext , params : types .PaginatedRequestParams | None
1148+ ) -> types .ListToolsResult :
1149+ captured .append (params .cursor if params else None )
1150+ if len (captured ) == 1 :
1151+ return types .ListToolsResult (tools = _make_tools (["page1-item" ]), next_cursor = "next-cursor" )
1152+ return types .ListToolsResult (tools = _make_tools (["page2-item" ]), next_cursor = None )
1153+
1154+ server = Server ("test" , on_list_tools = on_list_tools )
1155+
1156+ with anyio .fail_after (5 ):
1157+ async with Client (server ) as client :
1158+ tools = await client .session .list_all_tools ()
1159+ assert captured == [None , "next-cursor" ]
1160+ assert [t .name for t in tools ] == ["page1-item" , "page2-item" ]
0 commit comments