Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/mcp/client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,23 +201,29 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
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."""
return await self.send_request(
types.ClientRequest(
types.ListResourcesRequest(
method="resources/list",
cursor=cursor,
)
),
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."""
return await self.send_request(
types.ClientRequest(
types.ListResourceTemplatesRequest(
method="resources/templates/list",
cursor=cursor,
)
),
types.ListResourceTemplatesResult,
Expand Down Expand Up @@ -278,12 +284,13 @@ async def call_tool(
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."""
return await self.send_request(
types.ClientRequest(
types.ListPromptsRequest(
method="prompts/list",
cursor=cursor,
)
),
types.ListPromptsResult,
Expand Down Expand Up @@ -322,12 +329,13 @@ async def complete(
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."""
return await self.send_request(
types.ClientRequest(
types.ListToolsRequest(
method="tools/list",
cursor=cursor,
)
),
types.ListToolsResult,
Expand Down
142 changes: 142 additions & 0 deletions tests/client/test_list_methods_cursor.py
Original file line number Diff line number Diff line change
@@ -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
Loading