Skip to content

Commit 7a394ee

Browse files
committed
feat: change to passing requests instead of cursors for pagination
1 parent 02a2c30 commit 7a394ee

File tree

7 files changed

+134
-117
lines changed

7 files changed

+134
-117
lines changed

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1763,10 +1763,13 @@ ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items
17631763

17641764

17651765
@server.list_resources()
1766-
async def list_resources_paginated(cursor: types.Cursor | None) -> types.ListResourcesResult:
1766+
async def list_resources_paginated(request: types.ListResourcesRequest) -> types.ListResourcesResult:
17671767
"""List resources with pagination support."""
17681768
page_size = 10
17691769

1770+
# Extract cursor from request params
1771+
cursor = request.params.cursor if request.params is not None else None
1772+
17701773
# Parse cursor to get offset
17711774
start = 0 if cursor is None else int(cursor)
17721775
end = start + page_size

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ def main(port: int, transport: str) -> int:
5959

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

65+
cursor = request.params.cursor if request.params is not None else None
6566
if cursor is None:
6667
# First page
6768
start_idx = 0
@@ -86,10 +87,11 @@ async def list_tools_paginated(cursor: types.Cursor | None) -> types.ListToolsRe
8687
# Paginated list_resources - returns 10 resources per page
8788
@app.list_resources()
8889
async def list_resources_paginated(
89-
cursor: types.Cursor | None,
90+
request: types.ListResourcesRequest,
9091
) -> types.ListResourcesResult:
9192
page_size = 10
9293

94+
cursor = request.params.cursor if request.params is not None else None
9395
if cursor is None:
9496
# First page
9597
start_idx = 0
@@ -114,10 +116,11 @@ async def list_resources_paginated(
114116
# Paginated list_prompts - returns 7 prompts per page
115117
@app.list_prompts()
116118
async def list_prompts_paginated(
117-
cursor: types.Cursor | None,
119+
request: types.ListPromptsRequest,
118120
) -> types.ListPromptsResult:
119121
page_size = 7
120122

123+
cursor = request.params.cursor if request.params is not None else None
121124
if cursor is None:
122125
# First page
123126
start_idx = 0

examples/snippets/servers/pagination_example.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515

1616

1717
@server.list_resources()
18-
async def list_resources_paginated(cursor: types.Cursor | None) -> types.ListResourcesResult:
18+
async def list_resources_paginated(request: types.ListResourcesRequest) -> types.ListResourcesResult:
1919
"""List resources with pagination support."""
2020
page_size = 10
2121

22+
# Extract cursor from request params
23+
cursor = request.params.cursor if request.params is not None else None
24+
2225
# Parse cursor to get offset
2326
start = 0 if cursor is None else int(cursor)
2427
end = start + page_size

src/mcp/server/lowlevel/func_inspection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Any
44

55

6-
def accepts_cursor(func: Callable[..., Any]) -> bool:
6+
def accepts_request(func: Callable[..., Any]) -> bool:
77
"""
88
True if the function accepts a cursor parameter call, otherwise false.
99

src/mcp/server/lowlevel/server.py

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +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
85+
from mcp.server.lowlevel.func_inspection import accepts_request
8686
from mcp.server.lowlevel.helper_types import ReadResourceContents
8787
from mcp.server.models import InitializationOptions
8888
from mcp.server.session import ServerSession
@@ -233,19 +233,19 @@ def request_context(
233233
def list_prompts(self):
234234
def decorator(
235235
func: Callable[[], Awaitable[list[types.Prompt]]]
236-
| Callable[[types.Cursor | None], Awaitable[types.ListPromptsResult]],
236+
| Callable[[types.ListPromptsRequest], Awaitable[types.ListPromptsResult]],
237237
):
238238
logger.debug("Registering handler for PromptListRequest")
239-
pass_cursor = accepts_cursor(func)
239+
pass_request = accepts_request(func)
240240

241-
if pass_cursor:
242-
cursor_func = cast(Callable[[types.Cursor | None], Awaitable[types.ListPromptsResult]], func)
241+
if pass_request:
242+
request_func = cast(Callable[[types.ListPromptsRequest], Awaitable[types.ListPromptsResult]], func)
243243

244-
async def cursor_handler(req: types.ListPromptsRequest):
245-
result = await cursor_func(req.params.cursor if req.params is not None else None)
244+
async def request_handler(req: types.ListPromptsRequest):
245+
result = await request_func(req)
246246
return types.ServerResult(result)
247247

248-
handler = cursor_handler
248+
handler = request_handler
249249
else:
250250
list_func = cast(Callable[[], Awaitable[list[types.Prompt]]], func)
251251

@@ -278,19 +278,19 @@ async def handler(req: types.GetPromptRequest):
278278
def list_resources(self):
279279
def decorator(
280280
func: Callable[[], Awaitable[list[types.Resource]]]
281-
| Callable[[types.Cursor | None], Awaitable[types.ListResourcesResult]],
281+
| Callable[[types.ListResourcesRequest], Awaitable[types.ListResourcesResult]],
282282
):
283283
logger.debug("Registering handler for ListResourcesRequest")
284-
pass_cursor = accepts_cursor(func)
284+
pass_request = accepts_request(func)
285285

286-
if pass_cursor:
287-
cursor_func = cast(Callable[[types.Cursor | None], Awaitable[types.ListResourcesResult]], func)
286+
if pass_request:
287+
request_func = cast(Callable[[types.ListResourcesRequest], Awaitable[types.ListResourcesResult]], func)
288288

289-
async def cursor_handler(req: types.ListResourcesRequest):
290-
result = await cursor_func(req.params.cursor if req.params is not None else None)
289+
async def request_handler(req: types.ListResourcesRequest):
290+
result = await request_func(req)
291291
return types.ServerResult(result)
292292

293-
handler = cursor_handler
293+
handler = request_handler
294294
else:
295295
list_func = cast(Callable[[], Awaitable[list[types.Resource]]], func)
296296

@@ -418,22 +418,22 @@ async def handler(req: types.UnsubscribeRequest):
418418
def list_tools(self):
419419
def decorator(
420420
func: Callable[[], Awaitable[list[types.Tool]]]
421-
| Callable[[types.Cursor | None], Awaitable[types.ListToolsResult]],
421+
| Callable[[types.ListToolsRequest], Awaitable[types.ListToolsResult]],
422422
):
423423
logger.debug("Registering handler for ListToolsRequest")
424-
pass_cursor = accepts_cursor(func)
424+
pass_request = accepts_request(func)
425425

426-
if pass_cursor:
427-
cursor_func = cast(Callable[[types.Cursor | None], Awaitable[types.ListToolsResult]], func)
426+
if pass_request:
427+
request_func = cast(Callable[[types.ListToolsRequest], Awaitable[types.ListToolsResult]], func)
428428

429-
async def cursor_handler(req: types.ListToolsRequest):
430-
result = await cursor_func(req.params.cursor if req.params is not None else None)
429+
async def request_handler(req: types.ListToolsRequest):
430+
result = await request_func(req)
431431
# Refresh the tool cache with returned tools
432432
for tool in result.tools:
433433
self._tool_cache[tool.name] = tool
434434
return types.ServerResult(result)
435435

436-
handler = cursor_handler
436+
handler = request_handler
437437
else:
438438
list_func = cast(Callable[[], Awaitable[list[types.Tool]]], func)
439439

tests/server/lowlevel/test_func_inspection.py

Lines changed: 61 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,83 @@
44
import pytest
55

66
from mcp import types
7-
from mcp.server.lowlevel.func_inspection import accepts_cursor
7+
from mcp.server.lowlevel.func_inspection import accepts_request
88

99

1010
# Test fixtures - functions and methods with various signatures
1111
class MyClass:
12-
async def no_cursor_method(self):
13-
"""Instance method without cursor parameter"""
12+
async def no_request_method(self):
13+
"""Instance method without request parameter"""
1414
pass
1515

1616
# noinspection PyMethodParameters
17-
async def no_cursor_method_bad_self_name(bad): # pyright: ignore[reportSelfClsParameterName]
18-
"""Instance method with cursor parameter, but with bad self name"""
17+
async def no_request_method_bad_self_name(bad): # pyright: ignore[reportSelfClsParameterName]
18+
"""Instance method without request parameter, but with bad self name"""
1919
pass
2020

21-
async def cursor_method(self, cursor: types.Cursor | None):
22-
"""Instance method with cursor parameter"""
21+
async def request_method(self, request: types.ListPromptsRequest):
22+
"""Instance method with request parameter"""
2323
pass
2424

2525
# noinspection PyMethodParameters
26-
async def cursor_method_bad_self_name(bad, cursor: types.Cursor | None): # pyright: ignore[reportSelfClsParameterName]
27-
"""Instance method with cursor parameter, but with bad self name"""
26+
async def request_method_bad_self_name(bad, request: types.ListPromptsRequest): # pyright: ignore[reportSelfClsParameterName]
27+
"""Instance method with request parameter, but with bad self name"""
2828
pass
2929

3030
@classmethod
31-
async def no_cursor_class_method(cls):
32-
"""Class method without cursor parameter"""
31+
async def no_request_class_method(cls):
32+
"""Class method without request parameter"""
3333
pass
3434

3535
# noinspection PyMethodParameters
3636
@classmethod
37-
async def no_cursor_class_method_bad_cls_name(bad): # pyright: ignore[reportSelfClsParameterName]
38-
"""Class method without cursor parameter, but with bad cls name"""
37+
async def no_request_class_method_bad_cls_name(bad): # pyright: ignore[reportSelfClsParameterName]
38+
"""Class method without request parameter, but with bad cls name"""
3939
pass
4040

4141
@classmethod
42-
async def cursor_class_method(cls, cursor: types.Cursor | None):
43-
"""Class method with cursor parameter"""
42+
async def request_class_method(cls, request: types.ListPromptsRequest):
43+
"""Class method with request parameter"""
4444
pass
4545

4646
# noinspection PyMethodParameters
4747
@classmethod
48-
async def cursor_class_method_bad_cls_name(bad, cursor: types.Cursor | None): # pyright: ignore[reportSelfClsParameterName]
49-
"""Class method with cursor parameter, but with bad cls name"""
48+
async def request_class_method_bad_cls_name(bad, request: types.ListPromptsRequest): # pyright: ignore[reportSelfClsParameterName]
49+
"""Class method with request parameter, but with bad cls name"""
5050
pass
5151

5252
@staticmethod
53-
async def no_cursor_static_method():
54-
"""Static method without cursor parameter"""
53+
async def no_request_static_method():
54+
"""Static method without request parameter"""
5555
pass
5656

5757
@staticmethod
58-
async def cursor_static_method(cursor: types.Cursor | None):
59-
"""Static method with cursor parameter"""
58+
async def request_static_method(request: types.ListPromptsRequest):
59+
"""Static method with request parameter"""
6060
pass
6161

6262
@staticmethod
63-
async def cursor_static_method_bad_arg_name(self: types.Cursor | None): # pyright: ignore[reportSelfClsParameterName]
64-
"""Static method with cursor parameter, but the cursor argument is named self"""
63+
async def request_static_method_bad_arg_name(self: types.ListPromptsRequest): # pyright: ignore[reportSelfClsParameterName]
64+
"""Static method with request parameter, but the request argument is named self"""
6565
pass
6666

6767

68-
async def no_cursor_func():
69-
"""Function without cursor parameter"""
68+
async def no_request_func():
69+
"""Function without request parameter"""
7070
pass
7171

7272

73-
async def cursor_func(cursor: types.Cursor | None):
74-
"""Function with cursor parameter"""
73+
async def request_func(request: types.ListPromptsRequest):
74+
"""Function with request parameter"""
7575
pass
7676

7777

78-
async def cursor_func_different_name(c: types.Cursor | None):
79-
"""Function with cursor parameter but different arg name"""
78+
async def request_func_different_name(req: types.ListPromptsRequest):
79+
"""Function with request parameter but different arg name"""
8080
pass
8181

8282

83-
async def cursor_func_with_self(self: types.Cursor | None):
83+
async def request_func_with_self(self: types.ListPromptsRequest):
8484
"""Function with parameter named 'self' (edge case)"""
8585
pass
8686

@@ -90,8 +90,8 @@ async def var_positional_func(*args: Any):
9090
pass
9191

9292

93-
async def positional_with_var_positional_func(cursor: types.Cursor | None, *args: Any):
94-
"""Function with cursor and *args"""
93+
async def positional_with_var_positional_func(request: types.ListPromptsRequest, *args: Any):
94+
"""Function with request and *args"""
9595
pass
9696

9797

@@ -100,18 +100,18 @@ async def var_keyword_func(**kwargs: Any):
100100
pass
101101

102102

103-
async def cursor_with_var_keyword_func(cursor: types.Cursor | None, **kwargs: Any):
104-
"""Function with cursor and **kwargs"""
103+
async def request_with_var_keyword_func(request: types.ListPromptsRequest, **kwargs: Any):
104+
"""Function with request and **kwargs"""
105105
pass
106106

107107

108-
async def cursor_with_default(cursor: types.Cursor | None = None):
109-
"""Function with cursor parameter having default value"""
108+
async def request_with_default(request: types.ListPromptsRequest | None = None):
109+
"""Function with request parameter having default value"""
110110
pass
111111

112112

113-
async def keyword_only_with_defaults(*, cursor: types.Cursor | None = None):
114-
"""Function with keyword-only cursor with default"""
113+
async def keyword_only_with_defaults(*, request: types.ListPromptsRequest | None = None):
114+
"""Function with keyword-only request with default"""
115115
pass
116116

117117

@@ -120,7 +120,7 @@ async def keyword_only_multiple_all_defaults(*, a: str = "test", b: int = 42):
120120
pass
121121

122122

123-
async def mixed_positional_and_keyword(cursor: types.Cursor | None, *, extra: str = "test"):
123+
async def mixed_positional_and_keyword(request: types.ListPromptsRequest, *, extra: str = "test"):
124124
"""Function with positional and keyword-only params"""
125125
pass
126126

@@ -129,45 +129,45 @@ async def mixed_positional_and_keyword(cursor: types.Cursor | None, *, extra: st
129129
"callable_obj,expected,description",
130130
[
131131
# Regular functions
132-
(no_cursor_func, False, "function without parameters"),
133-
(cursor_func, True, "function with cursor parameter"),
134-
(cursor_func_different_name, True, "function with cursor (different param name)"),
135-
(cursor_func_with_self, True, "function with param named 'self'"),
132+
(no_request_func, False, "function without parameters"),
133+
(request_func, True, "function with request parameter"),
134+
(request_func_different_name, True, "function with request (different param name)"),
135+
(request_func_with_self, True, "function with param named 'self'"),
136136
# Instance methods
137-
(MyClass().no_cursor_method, False, "instance method without cursor"),
138-
(MyClass().no_cursor_method_bad_self_name, False, "instance method without cursor (bad self name)"),
139-
(MyClass().cursor_method, True, "instance method with cursor"),
140-
(MyClass().cursor_method_bad_self_name, True, "instance method with cursor (bad self name)"),
137+
(MyClass().no_request_method, False, "instance method without request"),
138+
(MyClass().no_request_method_bad_self_name, False, "instance method without request (bad self name)"),
139+
(MyClass().request_method, True, "instance method with request"),
140+
(MyClass().request_method_bad_self_name, True, "instance method with request (bad self name)"),
141141
# Class methods
142-
(MyClass.no_cursor_class_method, False, "class method without cursor"),
143-
(MyClass.no_cursor_class_method_bad_cls_name, False, "class method without cursor (bad cls name)"),
144-
(MyClass.cursor_class_method, True, "class method with cursor"),
145-
(MyClass.cursor_class_method_bad_cls_name, True, "class method with cursor (bad cls name)"),
142+
(MyClass.no_request_class_method, False, "class method without request"),
143+
(MyClass.no_request_class_method_bad_cls_name, False, "class method without request (bad cls name)"),
144+
(MyClass.request_class_method, True, "class method with request"),
145+
(MyClass.request_class_method_bad_cls_name, True, "class method with request (bad cls name)"),
146146
# Static methods
147-
(MyClass.no_cursor_static_method, False, "static method without cursor"),
148-
(MyClass.cursor_static_method, True, "static method with cursor"),
149-
(MyClass.cursor_static_method_bad_arg_name, True, "static method with cursor (bad arg name)"),
147+
(MyClass.no_request_static_method, False, "static method without request"),
148+
(MyClass.request_static_method, True, "static method with request"),
149+
(MyClass.request_static_method_bad_arg_name, True, "static method with request (bad arg name)"),
150150
# Variadic parameters
151151
(var_positional_func, True, "function with *args"),
152-
(positional_with_var_positional_func, True, "function with cursor and *args"),
152+
(positional_with_var_positional_func, True, "function with request and *args"),
153153
(var_keyword_func, False, "function with **kwargs"),
154-
(cursor_with_var_keyword_func, True, "function with cursor and **kwargs"),
154+
(request_with_var_keyword_func, True, "function with request and **kwargs"),
155155
# Edge cases
156-
(cursor_with_default, True, "function with cursor having default value"),
156+
(request_with_default, True, "function with request having default value"),
157157
# Keyword-only parameters
158158
(keyword_only_with_defaults, False, "keyword-only with default (can call with no args)"),
159159
(keyword_only_multiple_all_defaults, False, "multiple keyword-only all with defaults"),
160160
(mixed_positional_and_keyword, True, "mixed positional and keyword-only params"),
161161
],
162162
ids=lambda x: x if isinstance(x, str) else "",
163163
)
164-
def test_accepts_cursor(callable_obj: Callable[..., Any], expected: bool, description: str):
165-
"""Test that accepts_cursor correctly identifies functions that accept a cursor parameter.
164+
def test_accepts_request(callable_obj: Callable[..., Any], expected: bool, description: str):
165+
"""Test that accepts_request correctly identifies functions that accept a request parameter.
166166
167167
The function should return True if the callable can potentially accept a positional
168-
cursor argument. Returns False if:
168+
request argument. Returns False if:
169169
- No parameters at all
170170
- Only keyword-only parameters that ALL have defaults (can call with no args)
171171
- Only **kwargs parameter (can't accept positional arguments)
172172
"""
173-
assert accepts_cursor(callable_obj) == expected, f"Failed for {description}"
173+
assert accepts_request(callable_obj) == expected, f"Failed for {description}"

0 commit comments

Comments
 (0)