Skip to content

Commit 2b2e80c

Browse files
committed
switch pagination to single decorator with callback inspection
1 parent 1a4c179 commit 2b2e80c

File tree

8 files changed

+429
-68
lines changed

8 files changed

+429
-68
lines changed

README.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,7 +1752,7 @@ server = Server("paginated-server")
17521752
ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items
17531753

17541754

1755-
@server.list_resources_paginated()
1755+
@server.list_resources()
17561756
async def list_resources_paginated(cursor: types.Cursor | None) -> types.ListResourcesResult:
17571757
"""List resources with pagination support."""
17581758
page_size = 10
@@ -1776,12 +1776,6 @@ async def list_resources_paginated(cursor: types.Cursor | None) -> types.ListRes
17761776
_Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/pagination_example.py)_
17771777
<!-- /snippet-source -->
17781778

1779-
Similar decorators are available for all list operations:
1780-
1781-
- `@server.list_tools_paginated()` - for paginating tools
1782-
- `@server.list_resources_paginated()` - for paginating resources
1783-
- `@server.list_prompts_paginated()` - for paginating prompts
1784-
17851779
#### Client-side Consumption
17861780

17871781
<!-- snippet-source examples/snippets/clients/pagination_client.py -->
@@ -1839,8 +1833,6 @@ _Full example: [examples/snippets/clients/pagination_client.py](https://github.c
18391833
- **Backward compatible** - clients that don't support pagination will still work (they'll just get the first page)
18401834
- **Flexible page sizes** - Each endpoint can define its own page size based on data characteristics
18411835

1842-
> **NOTE**: The paginated decorators (`list_tools_paginated()`, `list_resources_paginated()`, `list_prompts_paginated()`) are mutually exclusive with their non-paginated counterparts and cannot be used together on the same server instance.
1843-
18441836
See the [simple-pagination example](examples/servers/simple-pagination) for a complete implementation.
18451837

18461838
### Writing MCP Clients

examples/servers/simple-pagination/mcp_simple_pagination/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def main(port: int, transport: str) -> int:
5858
app = Server("mcp-simple-pagination")
5959

6060
# Paginated list_tools - returns 5 tools per page
61-
@app.list_tools_paginated()
61+
@app.list_tools()
6262
async def list_tools_paginated(cursor: types.Cursor | None) -> types.ListToolsResult:
6363
page_size = 5
6464

@@ -84,7 +84,7 @@ async def list_tools_paginated(cursor: types.Cursor | None) -> types.ListToolsRe
8484
return types.ListToolsResult(tools=page_tools, nextCursor=next_cursor)
8585

8686
# Paginated list_resources - returns 10 resources per page
87-
@app.list_resources_paginated()
87+
@app.list_resources()
8888
async def list_resources_paginated(
8989
cursor: types.Cursor | None,
9090
) -> types.ListResourcesResult:
@@ -112,7 +112,7 @@ async def list_resources_paginated(
112112
return types.ListResourcesResult(resources=page_resources, nextCursor=next_cursor)
113113

114114
# Paginated list_prompts - returns 7 prompts per page
115-
@app.list_prompts_paginated()
115+
@app.list_prompts()
116116
async def list_prompts_paginated(
117117
cursor: types.Cursor | None,
118118
) -> types.ListPromptsResult:

examples/snippets/servers/pagination_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items
1515

1616

17-
@server.list_resources_paginated()
17+
@server.list_resources()
1818
async def list_resources_paginated(cursor: types.Cursor | None) -> types.ListResourcesResult:
1919
"""List resources with pagination support."""
2020
page_size = 10
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import inspect
2+
from collections.abc import Callable
3+
from typing import Any
4+
5+
6+
def accepts_cursor(func: Callable[..., Any]) -> bool:
7+
"""
8+
True if the function accepts a cursor parameter call, otherwise false.
9+
10+
`accepts_cursor` does not validate that the function will work. For
11+
example, if `func` contains keyword-only arguments with no defaults,
12+
then it will not work when used in the `lowlevel/server.py` code, but
13+
this function will not raise an exception.
14+
"""
15+
try:
16+
sig = inspect.signature(func)
17+
except (ValueError, TypeError):
18+
return False
19+
20+
params = dict(sig.parameters.items())
21+
22+
method = inspect.ismethod(func)
23+
24+
if method:
25+
params.pop("self", None)
26+
params.pop("cls", None)
27+
28+
if len(params) == 0:
29+
# No parameters at all - can't accept cursor
30+
return False
31+
32+
# Check if ALL remaining parameters are keyword-only
33+
all_keyword_only = all(param.kind == inspect.Parameter.KEYWORD_ONLY for param in params.values())
34+
35+
if all_keyword_only:
36+
# If all params are keyword-only, check if they ALL have defaults
37+
# If they do, the function can be called with no arguments -> no cursor
38+
all_have_defaults = all(param.default is not inspect.Parameter.empty for param in params.values())
39+
return not all_have_defaults # False if all have defaults (no cursor), True otherwise
40+
41+
# Check if the ONLY parameter is **kwargs (VAR_KEYWORD)
42+
# A function with only **kwargs can't accept a positional cursor argument
43+
if len(params) == 1:
44+
only_param = next(iter(params.values()))
45+
if only_param.kind == inspect.Parameter.VAR_KEYWORD:
46+
return False # Can't pass positional cursor to **kwargs
47+
48+
# Has at least one positional or variadic parameter - can accept cursor
49+
# Important note: this is designed to _not_ handle the situation where
50+
# there are multiple keyword only arguments with no defaults. In those
51+
# situations it's an invalid handler function, and will error. But it's
52+
# not the responsibility of this function to check the validity of a
53+
# callback.
54+
return True

src/mcp/server/lowlevel/server.py

Lines changed: 64 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ async def main():
8282
from typing_extensions import TypeVar
8383

8484
import mcp.types as types
85+
from mcp.server.lowlevel.func_inspection import accepts_cursor
8586
from mcp.server.lowlevel.helper_types import ReadResourceContents
8687
from mcp.server.models import InitializationOptions
8788
from mcp.server.session import ServerSession
@@ -230,25 +231,29 @@ def request_context(
230231
return request_ctx.get()
231232

232233
def list_prompts(self):
233-
def decorator(func: Callable[[], Awaitable[list[types.Prompt]]]):
234+
def decorator(
235+
func: Callable[[], Awaitable[list[types.Prompt]]]
236+
| Callable[[types.Cursor | None], Awaitable[types.ListPromptsResult]],
237+
):
234238
logger.debug("Registering handler for PromptListRequest")
239+
pass_cursor = accepts_cursor(func)
235240

236-
async def handler(_: Any):
237-
prompts = await func()
238-
return types.ServerResult(types.ListPromptsResult(prompts=prompts))
241+
if pass_cursor:
242+
cursor_func = cast(Callable[[types.Cursor | None], Awaitable[types.ListPromptsResult]], func)
239243

240-
self.request_handlers[types.ListPromptsRequest] = handler
241-
return func
244+
async def cursor_handler(req: types.ListPromptsRequest):
245+
result = await cursor_func(req.params.cursor if req.params is not None else None)
246+
return types.ServerResult(result)
242247

243-
return decorator
248+
handler = cursor_handler
249+
else:
250+
list_func = cast(Callable[[], Awaitable[list[types.Prompt]]], func)
244251

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")
252+
async def list_handler(_: types.ListPromptsRequest):
253+
result = await list_func()
254+
return types.ServerResult(types.ListPromptsResult(prompts=result))
248255

249-
async def handler(req: types.ListPromptsRequest):
250-
result = await func(req.params.cursor if req.params else None)
251-
return types.ServerResult(result)
256+
handler = list_handler
252257

253258
self.request_handlers[types.ListPromptsRequest] = handler
254259
return func
@@ -271,25 +276,29 @@ async def handler(req: types.GetPromptRequest):
271276
return decorator
272277

273278
def list_resources(self):
274-
def decorator(func: Callable[[], Awaitable[list[types.Resource]]]):
279+
def decorator(
280+
func: Callable[[], Awaitable[list[types.Resource]]]
281+
| Callable[[types.Cursor | None], Awaitable[types.ListResourcesResult]],
282+
):
275283
logger.debug("Registering handler for ListResourcesRequest")
284+
pass_cursor = accepts_cursor(func)
276285

277-
async def handler(_: Any):
278-
resources = await func()
279-
return types.ServerResult(types.ListResourcesResult(resources=resources))
286+
if pass_cursor:
287+
cursor_func = cast(Callable[[types.Cursor | None], Awaitable[types.ListResourcesResult]], func)
280288

281-
self.request_handlers[types.ListResourcesRequest] = handler
282-
return func
289+
async def cursor_handler(req: types.ListResourcesRequest):
290+
result = await cursor_func(req.params.cursor if req.params is not None else None)
291+
return types.ServerResult(result)
283292

284-
return decorator
293+
handler = cursor_handler
294+
else:
295+
list_func = cast(Callable[[], Awaitable[list[types.Resource]]], func)
285296

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")
297+
async def list_handler(_: types.ListResourcesRequest):
298+
result = await list_func()
299+
return types.ServerResult(types.ListResourcesResult(resources=result))
289300

290-
async def handler(req: types.ListResourcesRequest):
291-
result = await func(req.params.cursor if req.params else None)
292-
return types.ServerResult(result)
301+
handler = list_handler
293302

294303
self.request_handlers[types.ListResourcesRequest] = handler
295304
return func
@@ -407,33 +416,36 @@ async def handler(req: types.UnsubscribeRequest):
407416
return decorator
408417

409418
def list_tools(self):
410-
def decorator(func: Callable[[], Awaitable[list[types.Tool]]]):
419+
def decorator(
420+
func: Callable[[], Awaitable[list[types.Tool]]]
421+
| Callable[[types.Cursor | None], Awaitable[types.ListToolsResult]],
422+
):
411423
logger.debug("Registering handler for ListToolsRequest")
412-
413-
async def handler(_: Any):
414-
tools = await func()
415-
# Refresh the tool cache
416-
self._tool_cache.clear()
417-
for tool in tools:
418-
self._tool_cache[tool.name] = tool
419-
return types.ServerResult(types.ListToolsResult(tools=tools))
420-
421-
self.request_handlers[types.ListToolsRequest] = handler
422-
return func
423-
424-
return decorator
425-
426-
def list_tools_paginated(self):
427-
def decorator(func: Callable[[types.Cursor | None], Awaitable[types.ListToolsResult]]):
428-
logger.debug("Registering paginated handler for ListToolsRequest")
429-
430-
async def handler(request: types.ListToolsRequest):
431-
cursor = request.params.cursor if request.params else None
432-
result = await func(cursor)
433-
# Refresh the tool cache with returned tools
434-
for tool in result.tools:
435-
self._tool_cache[tool.name] = tool
436-
return types.ServerResult(result)
424+
pass_cursor = accepts_cursor(func)
425+
426+
if pass_cursor:
427+
cursor_func = cast(Callable[[types.Cursor | None], Awaitable[types.ListToolsResult]], func)
428+
429+
async def cursor_handler(req: types.ListToolsRequest):
430+
result = await cursor_func(req.params.cursor if req.params is not None else None)
431+
# Refresh the tool cache with returned tools
432+
for tool in result.tools:
433+
self._tool_cache[tool.name] = tool
434+
return types.ServerResult(result)
435+
436+
handler = cursor_handler
437+
else:
438+
list_func = cast(Callable[[], Awaitable[list[types.Tool]]], func)
439+
440+
async def list_handler(req: types.ListToolsRequest):
441+
result = await list_func()
442+
# Clear and refresh the entire tool cache
443+
self._tool_cache.clear()
444+
for tool in result:
445+
self._tool_cache[tool.name] = tool
446+
return types.ServerResult(types.ListToolsResult(tools=result))
447+
448+
handler = list_handler
437449

438450
self.request_handlers[types.ListToolsRequest] = handler
439451
return func

0 commit comments

Comments
 (0)