Skip to content

Commit 3217282

Browse files
refactor: add @overload decorators and @deprecated for list methods
Implements feedback from maxisbey on PR #1453. This change improves type safety and deprecation handling for list methods (list_tools, list_resources, list_prompts, list_resource_templates): - Add @overload decorators with three signatures to provide clear IDE typing: - cursor: str (positional argument) - params: PaginatedRequestParams (keyword-only argument) - no arguments - Replace warnings.warn() with @deprecated decorator from typing_extensions for cleaner deprecation signaling - Add validation to raise ValueError when both cursor and params are provided, preventing ambiguous parameter combinations - Update tests to expect ValueError when both parameters are specified This ensures the Python SDK has type checking equivalent to the TypeScript SDK and provides better developer experience through IDE assistance and clear deprecation messaging.
1 parent a98c4f7 commit 3217282

File tree

4 files changed

+112
-74
lines changed

4 files changed

+112
-74
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1840,7 +1840,7 @@ import asyncio
18401840

18411841
from mcp.client.session import ClientSession
18421842
from mcp.client.stdio import StdioServerParameters, stdio_client
1843-
from mcp.types import Resource
1843+
from mcp.types import PaginatedRequestParams, Resource
18441844

18451845

18461846
async def list_all_resources() -> None:
@@ -1857,7 +1857,7 @@ async def list_all_resources() -> None:
18571857

18581858
while True:
18591859
# Fetch a page of resources
1860-
result = await session.list_resources(cursor=cursor)
1860+
result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor))
18611861
all_resources.extend(result.resources)
18621862

18631863
print(f"Fetched {len(result.resources)} resources")

examples/snippets/clients/pagination_client.py

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

77
from mcp.client.session import ClientSession
88
from mcp.client.stdio import StdioServerParameters, stdio_client
9-
from mcp.types import Resource
9+
from mcp.types import PaginatedRequestParams, Resource
1010

1111

1212
async def list_all_resources() -> None:
@@ -23,7 +23,7 @@ async def list_all_resources() -> None:
2323

2424
while True:
2525
# Fetch a page of resources
26-
result = await session.list_resources(cursor=cursor)
26+
result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor))
2727
all_resources.extend(result.resources)
2828

2929
print(f"Fetched {len(result.resources)} resources")

src/mcp/client/session.py

Lines changed: 93 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import logging
2-
import warnings
32
from datetime import timedelta
4-
from typing import Any, Protocol
3+
from typing import Any, Protocol, overload
54

65
import anyio.lowlevel
76
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
87
from jsonschema import SchemaError, ValidationError, validate
98
from pydantic import AnyUrl, TypeAdapter
9+
from typing_extensions import deprecated
1010

1111
import mcp.types as types
1212
from mcp.shared.context import RequestContext
@@ -213,6 +213,16 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
213213
types.EmptyResult,
214214
)
215215

216+
@deprecated("Use params=PaginatedRequestParams(...) instead")
217+
@overload
218+
async def list_resources(self, cursor: str | None) -> types.ListResourcesResult: ...
219+
220+
@overload
221+
async def list_resources(self, *, params: types.PaginatedRequestParams | None) -> types.ListResourcesResult: ...
222+
223+
@overload
224+
async def list_resources(self) -> types.ListResourcesResult: ...
225+
216226
async def list_resources(
217227
self,
218228
cursor: str | None = None,
@@ -222,23 +232,36 @@ async def list_resources(
222232
"""Send a resources/list request.
223233
224234
Args:
225-
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
226-
params: Pagination parameters. Defaults to None (omits params field).
235+
cursor: Simple cursor string for pagination (deprecated, use params instead)
236+
params: Full pagination parameters including cursor and any future fields
227237
"""
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)
238+
if params is not None and cursor is not None:
239+
raise ValueError("Cannot specify both cursor and params")
240+
241+
if params is not None:
242+
request_params = params
243+
elif cursor is not None:
244+
request_params = types.PaginatedRequestParams(cursor=cursor)
245+
else:
246+
request_params = None
236247

237248
return await self.send_request(
238-
types.ClientRequest(types.ListResourcesRequest(params=params)),
249+
types.ClientRequest(types.ListResourcesRequest(params=request_params)),
239250
types.ListResourcesResult,
240251
)
241252

253+
@deprecated("Use params=PaginatedRequestParams(...) instead")
254+
@overload
255+
async def list_resource_templates(self, cursor: str | None) -> types.ListResourceTemplatesResult: ...
256+
257+
@overload
258+
async def list_resource_templates(
259+
self, *, params: types.PaginatedRequestParams | None
260+
) -> types.ListResourceTemplatesResult: ...
261+
262+
@overload
263+
async def list_resource_templates(self) -> types.ListResourceTemplatesResult: ...
264+
242265
async def list_resource_templates(
243266
self,
244267
cursor: str | None = None,
@@ -248,20 +271,21 @@ async def list_resource_templates(
248271
"""Send a resources/templates/list request.
249272
250273
Args:
251-
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
252-
params: Pagination parameters. Defaults to None (omits params field).
274+
cursor: Simple cursor string for pagination (deprecated, use params instead)
275+
params: Full pagination parameters including cursor and any future fields
253276
"""
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)
277+
if params is not None and cursor is not None:
278+
raise ValueError("Cannot specify both cursor and params")
279+
280+
if params is not None:
281+
request_params = params
282+
elif cursor is not None:
283+
request_params = types.PaginatedRequestParams(cursor=cursor)
284+
else:
285+
request_params = None
262286

263287
return await self.send_request(
264-
types.ClientRequest(types.ListResourceTemplatesRequest(params=params)),
288+
types.ClientRequest(types.ListResourceTemplatesRequest(params=request_params)),
265289
types.ListResourceTemplatesResult,
266290
)
267291

@@ -330,7 +354,7 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
330354
"""Validate the structured content of a tool result against its output schema."""
331355
if name not in self._tool_output_schemas:
332356
# refresh output schema cache
333-
await self.list_tools()
357+
await self.list_tools() # type: ignore[reportDeprecated]
334358

335359
output_schema = None
336360
if name in self._tool_output_schemas:
@@ -348,6 +372,16 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
348372
except SchemaError as e:
349373
raise RuntimeError(f"Invalid schema for tool {name}: {e}")
350374

375+
@deprecated("Use params=PaginatedRequestParams(...) instead")
376+
@overload
377+
async def list_prompts(self, cursor: str | None) -> types.ListPromptsResult: ...
378+
379+
@overload
380+
async def list_prompts(self, *, params: types.PaginatedRequestParams | None) -> types.ListPromptsResult: ...
381+
382+
@overload
383+
async def list_prompts(self) -> types.ListPromptsResult: ...
384+
351385
async def list_prompts(
352386
self,
353387
cursor: str | None = None,
@@ -357,20 +391,21 @@ async def list_prompts(
357391
"""Send a prompts/list request.
358392
359393
Args:
360-
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
361-
params: Pagination parameters. Defaults to None (omits params field).
394+
cursor: Simple cursor string for pagination (deprecated, use params instead)
395+
params: Full pagination parameters including cursor and any future fields
362396
"""
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)
397+
if params is not None and cursor is not None:
398+
raise ValueError("Cannot specify both cursor and params")
399+
400+
if params is not None:
401+
request_params = params
402+
elif cursor is not None:
403+
request_params = types.PaginatedRequestParams(cursor=cursor)
404+
else:
405+
request_params = None
371406

372407
return await self.send_request(
373-
types.ClientRequest(types.ListPromptsRequest(params=params)),
408+
types.ClientRequest(types.ListPromptsRequest(params=request_params)),
374409
types.ListPromptsResult,
375410
)
376411

@@ -409,6 +444,16 @@ async def complete(
409444
types.CompleteResult,
410445
)
411446

447+
@deprecated("Use params=PaginatedRequestParams(...) instead")
448+
@overload
449+
async def list_tools(self, cursor: str | None) -> types.ListToolsResult: ...
450+
451+
@overload
452+
async def list_tools(self, *, params: types.PaginatedRequestParams | None) -> types.ListToolsResult: ...
453+
454+
@overload
455+
async def list_tools(self) -> types.ListToolsResult: ...
456+
412457
async def list_tools(
413458
self,
414459
cursor: str | None = None,
@@ -418,20 +463,21 @@ async def list_tools(
418463
"""Send a tools/list request.
419464
420465
Args:
421-
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
422-
params: Pagination parameters. Defaults to None (omits params field).
466+
cursor: Simple cursor string for pagination (deprecated, use params instead)
467+
params: Full pagination parameters including cursor and any future fields
423468
"""
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)
469+
if params is not None and cursor is not None:
470+
raise ValueError("Cannot specify both cursor and params")
471+
472+
if params is not None:
473+
request_params = params
474+
elif cursor is not None:
475+
request_params = types.PaginatedRequestParams(cursor=cursor)
476+
else:
477+
request_params = None
432478

433479
result = await self.send_request(
434-
types.ClientRequest(types.ListToolsRequest(params=params)),
480+
types.ClientRequest(types.ListToolsRequest(params=request_params)),
435481
types.ListToolsResult,
436482
)
437483

tests/client/test_list_methods_cursor.py

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -165,42 +165,34 @@ async def test_list_methods_params_parameter(
165165

166166

167167
@pytest.mark.parametrize(
168-
"method_name,request_method",
168+
"method_name",
169169
[
170-
("list_tools", "tools/list"),
171-
("list_resources", "resources/list"),
172-
("list_prompts", "prompts/list"),
173-
("list_resource_templates", "resources/templates/list"),
170+
"list_tools",
171+
"list_resources",
172+
"list_prompts",
173+
"list_resource_templates",
174174
],
175175
)
176-
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
177-
async def test_list_methods_params_takes_precedence_over_cursor(
178-
stream_spy: Callable[[], StreamSpyCollection],
176+
async def test_list_methods_raises_error_when_both_cursor_and_params_provided(
179177
full_featured_server: FastMCP,
180178
method_name: str,
181-
request_method: str,
182179
):
183-
"""Test that params parameter takes precedence over cursor parameter.
180+
"""Test that providing both cursor and params raises ValueError.
184181
185182
Covers: list_tools, list_resources, list_prompts, list_resource_templates
186183
187-
When both cursor and params are provided, params should be used and
188-
cursor should be ignored, ensuring safe migration path.
184+
When both cursor and params are provided, a ValueError should be raised
185+
to prevent ambiguity.
189186
"""
190187
async with create_session(full_featured_server._mcp_server) as client_session:
191-
spies = stream_spy()
192188
method = getattr(client_session, method_name)
193189

194-
# Call with both cursor and params - params should take precedence
195-
_ = await method(
196-
cursor="old_cursor",
197-
params=types.PaginatedRequestParams(cursor="new_cursor"),
198-
)
199-
requests = spies.get_client_requests(method=request_method)
200-
assert len(requests) == 1
201-
# Verify params takes precedence (new_cursor should be used, not old_cursor)
202-
assert requests[0].params is not None
203-
assert requests[0].params["cursor"] == "new_cursor"
190+
# Call with both cursor and params - should raise ValueError
191+
with pytest.raises(ValueError, match="Cannot specify both cursor and params"):
192+
await method(
193+
cursor="old_cursor",
194+
params=types.PaginatedRequestParams(cursor="new_cursor"),
195+
)
204196

205197

206198
async def test_list_tools_with_strict_server_validation():

0 commit comments

Comments
 (0)