Skip to content

Commit 5dc3f59

Browse files
feat: add pagination to mcp_client list_tools_sync (#436)
1 parent 61f9c59 commit 5dc3f59

File tree

4 files changed

+62
-5
lines changed

4 files changed

+62
-5
lines changed

src/strands/tools/mcp/mcp_client.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
from concurrent import futures
1717
from datetime import timedelta
1818
from types import TracebackType
19-
from typing import Any, Callable, Coroutine, Dict, List, Optional, TypeVar, Union
19+
from typing import Any, Callable, Coroutine, Dict, Optional, TypeVar, Union
2020

2121
from mcp import ClientSession, ListToolsResult
2222
from mcp.types import CallToolResult as MCPCallToolResult
2323
from mcp.types import ImageContent as MCPImageContent
2424
from mcp.types import TextContent as MCPTextContent
2525

26+
from ...types import PaginatedList
2627
from ...types.exceptions import MCPClientInitializationError
2728
from ...types.media import ImageFormat
2829
from ...types.tools import ToolResult, ToolResultContent, ToolResultStatus
@@ -140,7 +141,7 @@ async def _set_close_event() -> None:
140141
self._background_thread = None
141142
self._session_id = uuid.uuid4()
142143

143-
def list_tools_sync(self) -> List[MCPAgentTool]:
144+
def list_tools_sync(self, pagination_token: Optional[str] = None) -> PaginatedList[MCPAgentTool]:
144145
"""Synchronously retrieves the list of available tools from the MCP server.
145146
146147
This method calls the asynchronous list_tools method on the MCP session
@@ -154,14 +155,14 @@ def list_tools_sync(self) -> List[MCPAgentTool]:
154155
raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE)
155156

156157
async def _list_tools_async() -> ListToolsResult:
157-
return await self._background_thread_session.list_tools()
158+
return await self._background_thread_session.list_tools(cursor=pagination_token)
158159

159160
list_tools_response: ListToolsResult = self._invoke_on_background_thread(_list_tools_async()).result()
160161
self._log_debug_with_thread("received %d tools from MCP server", len(list_tools_response.tools))
161162

162163
mcp_tools = [MCPAgentTool(tool, self) for tool in list_tools_response.tools]
163164
self._log_debug_with_thread("successfully adapted %d MCP tools", len(mcp_tools))
164-
return mcp_tools
165+
return PaginatedList[MCPAgentTool](mcp_tools, token=list_tools_response.nextCursor)
165166

166167
def call_tool_sync(
167168
self,

src/strands/types/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
"""SDK type definitions."""
2+
3+
from .collections import PaginatedList
4+
5+
__all__ = ["PaginatedList"]

src/strands/types/collections.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Generic collection types for the Strands SDK."""
2+
3+
from typing import Generic, List, Optional, TypeVar
4+
5+
T = TypeVar("T")
6+
7+
8+
class PaginatedList(list, Generic[T]):
9+
"""A generic list-like object that includes a pagination token.
10+
11+
This maintains backwards compatibility by inheriting from list,
12+
so existing code that expects List[T] will continue to work.
13+
"""
14+
15+
def __init__(self, data: List[T], token: Optional[str] = None):
16+
"""Initialize a PaginatedList with data and an optional pagination token.
17+
18+
Args:
19+
data: The list of items to store.
20+
token: Optional pagination token for retrieving additional items.
21+
"""
22+
super().__init__(data)
23+
self.pagination_token = token

tests/strands/tools/mcp/test_mcp_client.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,11 @@ def test_list_tools_sync(mock_transport, mock_session):
7171
with MCPClient(mock_transport["transport_callable"]) as client:
7272
tools = client.list_tools_sync()
7373

74-
mock_session.list_tools.assert_called_once()
74+
mock_session.list_tools.assert_called_once_with(cursor=None)
7575

7676
assert len(tools) == 1
7777
assert tools[0].tool_name == "test_tool"
78+
assert tools.pagination_token is None
7879

7980

8081
def test_list_tools_sync_session_not_active():
@@ -85,6 +86,34 @@ def test_list_tools_sync_session_not_active():
8586
client.list_tools_sync()
8687

8788

89+
def test_list_tools_sync_with_pagination_token(mock_transport, mock_session):
90+
"""Test that list_tools_sync correctly passes pagination token and returns next cursor."""
91+
mock_tool = MCPTool(name="test_tool", description="A test tool", inputSchema={"type": "object", "properties": {}})
92+
mock_session.list_tools.return_value = ListToolsResult(tools=[mock_tool], nextCursor="next_page_token")
93+
94+
with MCPClient(mock_transport["transport_callable"]) as client:
95+
tools = client.list_tools_sync(pagination_token="current_page_token")
96+
97+
mock_session.list_tools.assert_called_once_with(cursor="current_page_token")
98+
assert len(tools) == 1
99+
assert tools[0].tool_name == "test_tool"
100+
assert tools.pagination_token == "next_page_token"
101+
102+
103+
def test_list_tools_sync_without_pagination_token(mock_transport, mock_session):
104+
"""Test that list_tools_sync works without pagination token and handles missing next cursor."""
105+
mock_tool = MCPTool(name="test_tool", description="A test tool", inputSchema={"type": "object", "properties": {}})
106+
mock_session.list_tools.return_value = ListToolsResult(tools=[mock_tool]) # No nextCursor
107+
108+
with MCPClient(mock_transport["transport_callable"]) as client:
109+
tools = client.list_tools_sync()
110+
111+
mock_session.list_tools.assert_called_once_with(cursor=None)
112+
assert len(tools) == 1
113+
assert tools[0].tool_name == "test_tool"
114+
assert tools.pagination_token is None
115+
116+
88117
@pytest.mark.parametrize("is_error,expected_status", [(False, "success"), (True, "error")])
89118
def test_call_tool_sync_status(mock_transport, mock_session, is_error, expected_status):
90119
"""Test that call_tool_sync correctly handles success and error results."""

0 commit comments

Comments
 (0)