feat: add cursor pagination support to all client list methods (#718)

This commit is contained in:
Jerome
2025-05-15 04:04:04 -04:00
committed by GitHub
parent b8f7b027c3
commit a00b20a427
2 changed files with 154 additions and 4 deletions

View File

@@ -201,23 +201,29 @@ class ClientSession(
types.EmptyResult, types.EmptyResult,
) )
async def list_resources(self) -> types.ListResourcesResult: async def list_resources(
self, cursor: str | None = None
) -> types.ListResourcesResult:
"""Send a resources/list request.""" """Send a resources/list request."""
return await self.send_request( return await self.send_request(
types.ClientRequest( types.ClientRequest(
types.ListResourcesRequest( types.ListResourcesRequest(
method="resources/list", method="resources/list",
cursor=cursor,
) )
), ),
types.ListResourcesResult, types.ListResourcesResult,
) )
async def list_resource_templates(self) -> types.ListResourceTemplatesResult: async def list_resource_templates(
self, cursor: str | None = None
) -> types.ListResourceTemplatesResult:
"""Send a resources/templates/list request.""" """Send a resources/templates/list request."""
return await self.send_request( return await self.send_request(
types.ClientRequest( types.ClientRequest(
types.ListResourceTemplatesRequest( types.ListResourceTemplatesRequest(
method="resources/templates/list", method="resources/templates/list",
cursor=cursor,
) )
), ),
types.ListResourceTemplatesResult, types.ListResourceTemplatesResult,
@@ -278,12 +284,13 @@ class ClientSession(
request_read_timeout_seconds=read_timeout_seconds, request_read_timeout_seconds=read_timeout_seconds,
) )
async def list_prompts(self) -> types.ListPromptsResult: async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResult:
"""Send a prompts/list request.""" """Send a prompts/list request."""
return await self.send_request( return await self.send_request(
types.ClientRequest( types.ClientRequest(
types.ListPromptsRequest( types.ListPromptsRequest(
method="prompts/list", method="prompts/list",
cursor=cursor,
) )
), ),
types.ListPromptsResult, types.ListPromptsResult,
@@ -322,12 +329,13 @@ class ClientSession(
types.CompleteResult, types.CompleteResult,
) )
async def list_tools(self) -> types.ListToolsResult: async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
"""Send a tools/list request.""" """Send a tools/list request."""
return await self.send_request( return await self.send_request(
types.ClientRequest( types.ClientRequest(
types.ListToolsRequest( types.ListToolsRequest(
method="tools/list", method="tools/list",
cursor=cursor,
) )
), ),
types.ListToolsResult, types.ListToolsResult,

View File

@@ -0,0 +1,142 @@
import pytest
from mcp.server.fastmcp import FastMCP
from mcp.shared.memory import (
create_connected_server_and_client_session as create_session,
)
# Mark the whole module for async tests
pytestmark = pytest.mark.anyio
async def test_list_tools_cursor_parameter():
"""Test that the cursor parameter is accepted for list_tools.
Note: FastMCP doesn't currently implement pagination, so this test
only verifies that the cursor parameter is accepted by the client.
"""
server = FastMCP("test")
# Create a couple of test tools
@server.tool(name="test_tool_1")
async def test_tool_1() -> str:
"""First test tool"""
return "Result 1"
@server.tool(name="test_tool_2")
async def test_tool_2() -> str:
"""Second test tool"""
return "Result 2"
async with create_session(server._mcp_server) as client_session:
# Test without cursor parameter (omitted)
result1 = await client_session.list_tools()
assert len(result1.tools) == 2
# Test with cursor=None
result2 = await client_session.list_tools(cursor=None)
assert len(result2.tools) == 2
# Test with cursor as string
result3 = await client_session.list_tools(cursor="some_cursor_value")
assert len(result3.tools) == 2
# Test with empty string cursor
result4 = await client_session.list_tools(cursor="")
assert len(result4.tools) == 2
async def test_list_resources_cursor_parameter():
"""Test that the cursor parameter is accepted for list_resources.
Note: FastMCP doesn't currently implement pagination, so this test
only verifies that the cursor parameter is accepted by the client.
"""
server = FastMCP("test")
# Create a test resource
@server.resource("resource://test/data")
async def test_resource() -> str:
"""Test resource"""
return "Test data"
async with create_session(server._mcp_server) as client_session:
# Test without cursor parameter (omitted)
result1 = await client_session.list_resources()
assert len(result1.resources) >= 1
# Test with cursor=None
result2 = await client_session.list_resources(cursor=None)
assert len(result2.resources) >= 1
# Test with cursor as string
result3 = await client_session.list_resources(cursor="some_cursor")
assert len(result3.resources) >= 1
# Test with empty string cursor
result4 = await client_session.list_resources(cursor="")
assert len(result4.resources) >= 1
async def test_list_prompts_cursor_parameter():
"""Test that the cursor parameter is accepted for list_prompts.
Note: FastMCP doesn't currently implement pagination, so this test
only verifies that the cursor parameter is accepted by the client.
"""
server = FastMCP("test")
# Create a test prompt
@server.prompt()
async def test_prompt(name: str) -> str:
"""Test prompt"""
return f"Hello, {name}!"
async with create_session(server._mcp_server) as client_session:
# Test without cursor parameter (omitted)
result1 = await client_session.list_prompts()
assert len(result1.prompts) >= 1
# Test with cursor=None
result2 = await client_session.list_prompts(cursor=None)
assert len(result2.prompts) >= 1
# Test with cursor as string
result3 = await client_session.list_prompts(cursor="some_cursor")
assert len(result3.prompts) >= 1
# Test with empty string cursor
result4 = await client_session.list_prompts(cursor="")
assert len(result4.prompts) >= 1
async def test_list_resource_templates_cursor_parameter():
"""Test that the cursor parameter is accepted for list_resource_templates.
Note: FastMCP doesn't currently implement pagination, so this test
only verifies that the cursor parameter is accepted by the client.
"""
server = FastMCP("test")
# Create a test resource template
@server.resource("resource://test/{name}")
async def test_template(name: str) -> str:
"""Test resource template"""
return f"Data for {name}"
async with create_session(server._mcp_server) as client_session:
# Test without cursor parameter (omitted)
result1 = await client_session.list_resource_templates()
assert len(result1.resourceTemplates) >= 1
# Test with cursor=None
result2 = await client_session.list_resource_templates(cursor=None)
assert len(result2.resourceTemplates) >= 1
# Test with cursor as string
result3 = await client_session.list_resource_templates(cursor="some_cursor")
assert len(result3.resourceTemplates) >= 1
# Test with empty string cursor
result4 = await client_session.list_resource_templates(cursor="")
assert len(result4.resourceTemplates) >= 1