Skip to content

Commit 7db0e25

Browse files
feat: deprecate cursor parameter in favor of params for list methods
Deprecate the cursor parameter in list_tools, list_prompts, list_resources, and list_resource_templates methods, replacing it with a new params parameter that accepts PaginatedRequestParams. This change maintains complete backwards compatibility while providing opt-in support for strict MCP servers that require the params field to always be present in JSON-RPC requests (even if empty). When params is not provided or is None, the SDK continues to omit the params field entirely, matching the previous behavior. The cursor parameter now issues a DeprecationWarning directing developers to use the new params parameter. This matches the TypeScript SDK's API design. Reported-by: justin-yi-wang
1 parent 03b89e9 commit 7db0e25

File tree

2 files changed

+104
-37
lines changed

2 files changed

+104
-37
lines changed

src/mcp/client/session.py

Lines changed: 89 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import warnings
23
from datetime import timedelta
34
from typing import Any, Protocol
45

@@ -212,25 +213,55 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
212213
types.EmptyResult,
213214
)
214215

215-
async def list_resources(self, cursor: str | None = None) -> types.ListResourcesResult:
216-
"""Send a resources/list request."""
216+
async def list_resources(
217+
self,
218+
cursor: str | None = None,
219+
*,
220+
params: types.PaginatedRequestParams | None = None,
221+
) -> types.ListResourcesResult:
222+
"""Send a resources/list request.
223+
224+
Args:
225+
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
226+
params: Pagination parameters. Defaults to None (omits params field).
227+
"""
228+
if cursor is not None:
229+
warnings.warn(
230+
"cursor parameter is deprecated, use params=PaginatedRequestParams(cursor=...) instead",
231+
DeprecationWarning,
232+
stacklevel=2,
233+
)
234+
if params is None:
235+
params = types.PaginatedRequestParams(cursor=cursor)
236+
217237
return await self.send_request(
218-
types.ClientRequest(
219-
types.ListResourcesRequest(
220-
params=types.PaginatedRequestParams(cursor=cursor),
221-
)
222-
),
238+
types.ClientRequest(types.ListResourcesRequest(params=params)),
223239
types.ListResourcesResult,
224240
)
225241

226-
async def list_resource_templates(self, cursor: str | None = None) -> types.ListResourceTemplatesResult:
227-
"""Send a resources/templates/list request."""
242+
async def list_resource_templates(
243+
self,
244+
cursor: str | None = None,
245+
*,
246+
params: types.PaginatedRequestParams | None = None,
247+
) -> types.ListResourceTemplatesResult:
248+
"""Send a resources/templates/list request.
249+
250+
Args:
251+
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
252+
params: Pagination parameters. Defaults to None (omits params field).
253+
"""
254+
if cursor is not None:
255+
warnings.warn(
256+
"cursor parameter is deprecated, use params=PaginatedRequestParams(cursor=...) instead",
257+
DeprecationWarning,
258+
stacklevel=2,
259+
)
260+
if params is None:
261+
params = types.PaginatedRequestParams(cursor=cursor)
262+
228263
return await self.send_request(
229-
types.ClientRequest(
230-
types.ListResourceTemplatesRequest(
231-
params=types.PaginatedRequestParams(cursor=cursor),
232-
)
233-
),
264+
types.ClientRequest(types.ListResourceTemplatesRequest(params=params)),
234265
types.ListResourceTemplatesResult,
235266
)
236267

@@ -317,14 +348,29 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
317348
except SchemaError as e:
318349
raise RuntimeError(f"Invalid schema for tool {name}: {e}")
319350

320-
async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResult:
321-
"""Send a prompts/list request."""
351+
async def list_prompts(
352+
self,
353+
cursor: str | None = None,
354+
*,
355+
params: types.PaginatedRequestParams | None = None,
356+
) -> types.ListPromptsResult:
357+
"""Send a prompts/list request.
358+
359+
Args:
360+
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
361+
params: Pagination parameters. Defaults to None (omits params field).
362+
"""
363+
if cursor is not None:
364+
warnings.warn(
365+
"cursor parameter is deprecated, use params=PaginatedRequestParams(cursor=...) instead",
366+
DeprecationWarning,
367+
stacklevel=2,
368+
)
369+
if params is None:
370+
params = types.PaginatedRequestParams(cursor=cursor)
371+
322372
return await self.send_request(
323-
types.ClientRequest(
324-
types.ListPromptsRequest(
325-
params=types.PaginatedRequestParams(cursor=cursor),
326-
)
327-
),
373+
types.ClientRequest(types.ListPromptsRequest(params=params)),
328374
types.ListPromptsResult,
329375
)
330376

@@ -363,14 +409,29 @@ async def complete(
363409
types.CompleteResult,
364410
)
365411

366-
async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
367-
"""Send a tools/list request."""
412+
async def list_tools(
413+
self,
414+
cursor: str | None = None,
415+
*,
416+
params: types.PaginatedRequestParams | None = None,
417+
) -> types.ListToolsResult:
418+
"""Send a tools/list request.
419+
420+
Args:
421+
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
422+
params: Pagination parameters. Defaults to None (omits params field).
423+
"""
424+
if cursor is not None:
425+
warnings.warn(
426+
"cursor parameter is deprecated, use params=PaginatedRequestParams(cursor=...) instead",
427+
DeprecationWarning,
428+
stacklevel=2,
429+
)
430+
if params is None:
431+
params = types.PaginatedRequestParams(cursor=cursor)
432+
368433
result = await self.send_request(
369-
types.ClientRequest(
370-
types.ListToolsRequest(
371-
params=types.PaginatedRequestParams(cursor=cursor),
372-
)
373-
),
434+
types.ClientRequest(types.ListToolsRequest(params=params)),
374435
types.ListToolsResult,
375436
)
376437

tests/client/test_list_methods_cursor.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44

5+
import mcp.types as types
56
from mcp.server import Server
67
from mcp.server.fastmcp import FastMCP
78
from mcp.shared.memory import create_connected_server_and_client_session as create_session
@@ -12,6 +13,7 @@
1213
pytestmark = pytest.mark.anyio
1314

1415

16+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
1517
async def test_list_tools_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
1618
"""Test that the cursor parameter is accepted for list_tools
1719
and that it is correctly passed to the server.
@@ -38,15 +40,15 @@ async def test_tool_2() -> str:
3840
_ = await client_session.list_tools()
3941
list_tools_requests = spies.get_client_requests(method="tools/list")
4042
assert len(list_tools_requests) == 1
41-
assert list_tools_requests[0].params == {}
43+
assert list_tools_requests[0].params is None
4244

4345
spies.clear()
4446

4547
# Test with cursor=None
4648
_ = await client_session.list_tools(cursor=None)
4749
list_tools_requests = spies.get_client_requests(method="tools/list")
4850
assert len(list_tools_requests) == 1
49-
assert list_tools_requests[0].params == {}
51+
assert list_tools_requests[0].params is None
5052

5153
spies.clear()
5254

@@ -67,6 +69,7 @@ async def test_tool_2() -> str:
6769
assert list_tools_requests[0].params["cursor"] == ""
6870

6971

72+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
7073
async def test_list_resources_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
7174
"""Test that the cursor parameter is accepted for list_resources
7275
and that it is correctly passed to the server.
@@ -88,15 +91,15 @@ async def test_resource() -> str:
8891
_ = await client_session.list_resources()
8992
list_resources_requests = spies.get_client_requests(method="resources/list")
9093
assert len(list_resources_requests) == 1
91-
assert list_resources_requests[0].params == {}
94+
assert list_resources_requests[0].params is None
9295

9396
spies.clear()
9497

9598
# Test with cursor=None
9699
_ = await client_session.list_resources(cursor=None)
97100
list_resources_requests = spies.get_client_requests(method="resources/list")
98101
assert len(list_resources_requests) == 1
99-
assert list_resources_requests[0].params == {}
102+
assert list_resources_requests[0].params is None
100103

101104
spies.clear()
102105

@@ -117,6 +120,7 @@ async def test_resource() -> str:
117120
assert list_resources_requests[0].params["cursor"] == ""
118121

119122

123+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
120124
async def test_list_prompts_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
121125
"""Test that the cursor parameter is accepted for list_prompts
122126
and that it is correctly passed to the server.
@@ -137,15 +141,15 @@ async def test_prompt(name: str) -> str:
137141
_ = await client_session.list_prompts()
138142
list_prompts_requests = spies.get_client_requests(method="prompts/list")
139143
assert len(list_prompts_requests) == 1
140-
assert list_prompts_requests[0].params == {}
144+
assert list_prompts_requests[0].params is None
141145

142146
spies.clear()
143147

144148
# Test with cursor=None
145149
_ = await client_session.list_prompts(cursor=None)
146150
list_prompts_requests = spies.get_client_requests(method="prompts/list")
147151
assert len(list_prompts_requests) == 1
148-
assert list_prompts_requests[0].params == {}
152+
assert list_prompts_requests[0].params is None
149153

150154
spies.clear()
151155

@@ -166,6 +170,7 @@ async def test_prompt(name: str) -> str:
166170
assert list_prompts_requests[0].params["cursor"] == ""
167171

168172

173+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
169174
async def test_list_resource_templates_cursor_parameter(stream_spy: Callable[[], StreamSpyCollection]):
170175
"""Test that the cursor parameter is accepted for list_resource_templates
171176
and that it is correctly passed to the server.
@@ -187,15 +192,15 @@ async def test_template(name: str) -> str:
187192
_ = await client_session.list_resource_templates()
188193
list_templates_requests = spies.get_client_requests(method="resources/templates/list")
189194
assert len(list_templates_requests) == 1
190-
assert list_templates_requests[0].params == {}
195+
assert list_templates_requests[0].params is None
191196

192197
spies.clear()
193198

194199
# Test with cursor=None
195200
_ = await client_session.list_resource_templates(cursor=None)
196201
list_templates_requests = spies.get_client_requests(method="resources/templates/list")
197202
assert len(list_templates_requests) == 1
198-
assert list_templates_requests[0].params == {}
203+
assert list_templates_requests[0].params is None
199204

200205
spies.clear()
201206

@@ -244,5 +249,6 @@ async def handle_list_tools(request: ListToolsRequest) -> ListToolsResult:
244249
return ListToolsResult(tools=[])
245250

246251
async with create_session(server) as client_session:
247-
result = await client_session.list_tools()
252+
# Use params to explicitly send params: {} for strict server compatibility
253+
result = await client_session.list_tools(params=types.PaginatedRequestParams())
248254
assert result is not None

0 commit comments

Comments
 (0)