Skip to content

Commit 4f23152

Browse files
maxisbeyclaude
andcommitted
feat: add paginated list decorators for prompts, resources, and tools
Add list_prompts_paginated, list_resources_paginated, and list_tools_paginated decorators to support cursor-based pagination for listing endpoints. These decorators: - Accept a cursor parameter (can be None for first page) - Return the respective ListResult type directly - Maintain backward compatibility with existing non-paginated decorators - Update tool cache for list_tools_paginated Also includes simplified unit tests that verify cursor passthrough. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d1ac8d6 commit 4f23152

File tree

3 files changed

+155
-0
lines changed

3 files changed

+155
-0
lines changed

src/mcp/server/lowlevel/server.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,19 @@ async def handler(_: Any):
242242

243243
return decorator
244244

245+
def list_prompts_paginated(self):
246+
def decorator(func: Callable[[types.Cursor | None], Awaitable[types.ListPromptsResult]]):
247+
logger.debug("Registering handler for PromptListRequest with pagination")
248+
249+
async def handler(req: types.ListPromptsRequest):
250+
result = await func(req.params.cursor if req.params else None)
251+
return types.ServerResult(result)
252+
253+
self.request_handlers[types.ListPromptsRequest] = handler
254+
return func
255+
256+
return decorator
257+
245258
def get_prompt(self):
246259
def decorator(
247260
func: Callable[[str, dict[str, str] | None], Awaitable[types.GetPromptResult]],
@@ -270,6 +283,19 @@ async def handler(_: Any):
270283

271284
return decorator
272285

286+
def list_resources_paginated(self):
287+
def decorator(func: Callable[[types.Cursor | None], Awaitable[types.ListResourcesResult]]):
288+
logger.debug("Registering handler for ListResourcesRequest with pagination")
289+
290+
async def handler(req: types.ListResourcesRequest):
291+
result = await func(req.params.cursor if req.params else None)
292+
return types.ServerResult(result)
293+
294+
self.request_handlers[types.ListResourcesRequest] = handler
295+
return func
296+
297+
return decorator
298+
273299
def list_resource_templates(self):
274300
def decorator(func: Callable[[], Awaitable[list[types.ResourceTemplate]]]):
275301
logger.debug("Registering handler for ListResourceTemplatesRequest")
@@ -397,6 +423,25 @@ async def handler(_: Any):
397423

398424
return decorator
399425

426+
def list_tools_paginated(self):
427+
def decorator(
428+
func: Callable[[types.Cursor | None], Awaitable[types.ListToolsResult]]
429+
):
430+
logger.debug("Registering paginated handler for ListToolsRequest")
431+
432+
async def handler(request: types.ListToolsRequest):
433+
cursor = request.params.cursor if request.params else None
434+
result = await func(cursor)
435+
# Refresh the tool cache with returned tools
436+
for tool in result.tools:
437+
self._tool_cache[tool.name] = tool
438+
return types.ServerResult(result)
439+
440+
self.request_handlers[types.ListToolsRequest] = handler
441+
return func
442+
443+
return decorator
444+
400445
def _make_error_result(self, error_message: str) -> types.ServerResult:
401446
"""Create a ServerResult with an error CallToolResult."""
402447
return types.ServerResult(

tests/server/lowlevel/__init__.py

Whitespace-only changes.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import pytest
2+
3+
from mcp.server import Server
4+
from mcp.types import (
5+
Cursor,
6+
ListPromptsRequest,
7+
ListPromptsResult,
8+
ListResourcesRequest,
9+
ListResourcesResult,
10+
ListToolsRequest,
11+
ListToolsResult,
12+
PaginatedRequestParams,
13+
ServerResult,
14+
)
15+
16+
17+
@pytest.mark.anyio
18+
async def test_list_prompts_pagination() -> None:
19+
server = Server("test")
20+
test_cursor = "test-cursor-123"
21+
22+
# Track what cursor was received
23+
received_cursor: Cursor | None = None
24+
25+
@server.list_prompts_paginated()
26+
async def handle_list_prompts(cursor: Cursor | None) -> ListPromptsResult:
27+
nonlocal received_cursor
28+
received_cursor = cursor
29+
return ListPromptsResult(prompts=[], nextCursor="next")
30+
31+
handler = server.request_handlers[ListPromptsRequest]
32+
33+
# Test: No cursor provided -> handler receives None
34+
request = ListPromptsRequest(method="prompts/list", params=None)
35+
result = await handler(request)
36+
assert received_cursor is None
37+
assert isinstance(result, ServerResult)
38+
39+
# Test: Cursor provided -> handler receives exact cursor value
40+
request_with_cursor = ListPromptsRequest(
41+
method="prompts/list",
42+
params=PaginatedRequestParams(cursor=test_cursor)
43+
)
44+
result2 = await handler(request_with_cursor)
45+
assert received_cursor == test_cursor
46+
assert isinstance(result2, ServerResult)
47+
48+
49+
@pytest.mark.anyio
50+
async def test_list_resources_pagination() -> None:
51+
server = Server("test")
52+
test_cursor = "resource-cursor-456"
53+
54+
# Track what cursor was received
55+
received_cursor: Cursor | None = None
56+
57+
@server.list_resources_paginated()
58+
async def handle_list_resources(cursor: Cursor | None) -> ListResourcesResult:
59+
nonlocal received_cursor
60+
received_cursor = cursor
61+
return ListResourcesResult(resources=[], nextCursor="next")
62+
63+
handler = server.request_handlers[ListResourcesRequest]
64+
65+
# Test: No cursor provided -> handler receives None
66+
request = ListResourcesRequest(method="resources/list", params=None)
67+
result = await handler(request)
68+
assert received_cursor is None
69+
assert isinstance(result, ServerResult)
70+
71+
# Test: Cursor provided -> handler receives exact cursor value
72+
request_with_cursor = ListResourcesRequest(
73+
method="resources/list",
74+
params=PaginatedRequestParams(cursor=test_cursor)
75+
)
76+
result2 = await handler(request_with_cursor)
77+
assert received_cursor == test_cursor
78+
assert isinstance(result2, ServerResult)
79+
80+
81+
@pytest.mark.anyio
82+
async def test_list_tools_pagination() -> None:
83+
server = Server("test")
84+
test_cursor = "tools-cursor-789"
85+
86+
# Track what cursor was received
87+
received_cursor: Cursor | None = None
88+
89+
@server.list_tools_paginated()
90+
async def handle_list_tools(cursor: Cursor | None) -> ListToolsResult:
91+
nonlocal received_cursor
92+
received_cursor = cursor
93+
return ListToolsResult(tools=[], nextCursor="next")
94+
95+
handler = server.request_handlers[ListToolsRequest]
96+
97+
# Test: No cursor provided -> handler receives None
98+
request = ListToolsRequest(method="tools/list", params=None)
99+
result = await handler(request)
100+
assert received_cursor is None
101+
assert isinstance(result, ServerResult)
102+
103+
# Test: Cursor provided -> handler receives exact cursor value
104+
request_with_cursor = ListToolsRequest(
105+
method="tools/list",
106+
params=PaginatedRequestParams(cursor=test_cursor)
107+
)
108+
result2 = await handler(request_with_cursor)
109+
assert received_cursor == test_cursor
110+
assert isinstance(result2, ServerResult)

0 commit comments

Comments
 (0)