Skip to content

Commit d921ba5

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 <[email protected]>
1 parent c3717e7 commit d921ba5

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
@@ -241,6 +241,19 @@ async def handler(_: Any):
241241

242242
return decorator
243243

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

270283
return decorator
271284

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

397423
return decorator
398424

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