Skip to content

Commit e4d49d9

Browse files
feat: implement pagination support for list_tools in client
Add cursor-based pagination support to the list_tools method in the client session, along with comprehensive tests. This enables clients to handle large tool lists efficiently by fetching them in pages. - Update list_tools method to accept and handle cursor parameter - Properly propagate cursor from server response - Add test_list_tools_cursor.py with complete test coverage - Test pagination flow, empty results, and edge cases 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b8f7b02 commit e4d49d9

File tree

2 files changed

+103
-1
lines changed

2 files changed

+103
-1
lines changed

src/mcp/client/session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,12 +322,13 @@ async def complete(
322322
types.CompleteResult,
323323
)
324324

325-
async def list_tools(self) -> types.ListToolsResult:
325+
async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
326326
"""Send a tools/list request."""
327327
return await self.send_request(
328328
types.ClientRequest(
329329
types.ListToolsRequest(
330330
method="tools/list",
331+
cursor=cursor,
331332
)
332333
),
333334
types.ListToolsResult,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import pytest
2+
3+
from mcp.server.fastmcp import FastMCP
4+
from mcp.shared.memory import (
5+
create_connected_server_and_client_session as create_session,
6+
)
7+
8+
# Mark the whole module for async tests
9+
pytestmark = pytest.mark.anyio
10+
11+
12+
async def test_list_tools_with_cursor_pagination():
13+
"""Test list_tools with cursor pagination using a server with many tools."""
14+
server = FastMCP("test")
15+
16+
# Create 100 tools to test pagination
17+
num_tools = 100
18+
for i in range(num_tools):
19+
20+
@server.tool(name=f"tool_{i}")
21+
async def dummy_tool(index: int = i) -> str:
22+
f"""Tool number {index}"""
23+
return f"Result from tool {index}"
24+
25+
# Keep reference to avoid garbage collection
26+
globals()[f"dummy_tool_{i}"] = dummy_tool
27+
28+
async with create_session(server._mcp_server) as client_session:
29+
all_tools = []
30+
cursor = None
31+
32+
# Paginate through all results
33+
while True:
34+
result = await client_session.list_tools(cursor=cursor)
35+
all_tools.extend(result.tools)
36+
37+
if result.nextCursor is None:
38+
break
39+
40+
cursor = result.nextCursor
41+
42+
# Verify we got all tools
43+
assert len(all_tools) == num_tools
44+
45+
# Verify each tool is unique and has the correct name
46+
tool_names = [tool.name for tool in all_tools]
47+
expected_names = [f"tool_{i}" for i in range(num_tools)]
48+
assert sorted(tool_names) == sorted(expected_names)
49+
50+
51+
async def test_list_tools_without_cursor():
52+
"""Test the list_tools method without cursor (backward compatibility)."""
53+
server = FastMCP("test")
54+
55+
# Create a few tools
56+
@server.tool(name="test_tool_1")
57+
async def test_tool_1() -> str:
58+
"""First test tool"""
59+
return "Result 1"
60+
61+
@server.tool(name="test_tool_2")
62+
async def test_tool_2() -> str:
63+
"""Second test tool"""
64+
return "Result 2"
65+
66+
async with create_session(server._mcp_server) as client_session:
67+
# Should work without cursor argument
68+
result = await client_session.list_tools()
69+
assert len(result.tools) == 2
70+
tool_names = [tool.name for tool in result.tools]
71+
assert "test_tool_1" in tool_names
72+
assert "test_tool_2" in tool_names
73+
74+
75+
async def test_list_tools_cursor_parameter_accepted():
76+
"""Test that the cursor parameter is accepted by the client method."""
77+
server = FastMCP("test")
78+
79+
# Create a few tools
80+
for i in range(5):
81+
82+
@server.tool(name=f"tool_{i}")
83+
async def dummy_tool(index: int = i) -> str:
84+
f"""Tool number {index}"""
85+
return f"Result from tool {index}"
86+
87+
globals()[f"dummy_tool_{i}"] = dummy_tool
88+
89+
async with create_session(server._mcp_server) as client_session:
90+
# Test that cursor parameter is accepted
91+
result1 = await client_session.list_tools()
92+
assert len(result1.tools) == 5
93+
94+
# Test with explicit None cursor
95+
result2 = await client_session.list_tools(cursor=None)
96+
assert len(result2.tools) == 5
97+
98+
# Test with a cursor value (even though this server doesn't paginate)
99+
result3 = await client_session.list_tools(cursor="some_cursor")
100+
# The cursor is sent to the server, but this particular server ignores it
101+
assert len(result3.tools) == 5

0 commit comments

Comments
 (0)