Skip to content

Commit daca273

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 daca273

File tree

2 files changed

+109
-70
lines changed

2 files changed

+109
-70
lines changed

src/mcp/client/session.py

Lines changed: 94 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import logging
2-
import warnings
32
from datetime import timedelta
4-
from typing import Any, Protocol
3+
from typing import Any, Protocol, overload
4+
5+
from typing_extensions import deprecated
56

67
import anyio.lowlevel
78
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
@@ -213,6 +214,16 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
213214
types.EmptyResult,
214215
)
215216

217+
@deprecated("Use params=PaginatedRequestParams(...) instead")
218+
@overload
219+
async def list_resources(self, cursor: str) -> types.ListResourcesResult: ...
220+
221+
@overload
222+
async def list_resources(self, *, params: types.PaginatedRequestParams) -> types.ListResourcesResult: ...
223+
224+
@overload
225+
async def list_resources(self) -> types.ListResourcesResult: ...
226+
216227
async def list_resources(
217228
self,
218229
cursor: str | None = None,
@@ -222,23 +233,36 @@ async def list_resources(
222233
"""Send a resources/list request.
223234
224235
Args:
225-
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
226-
params: Pagination parameters. Defaults to None (omits params field).
236+
cursor: Simple cursor string for pagination (deprecated, use params instead)
237+
params: Full pagination parameters including cursor and any future fields
227238
"""
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)
239+
if params is not None and cursor is not None:
240+
raise ValueError("Cannot specify both cursor and params")
241+
242+
if params is not None:
243+
request_params = params
244+
elif cursor is not None:
245+
request_params = types.PaginatedRequestParams(cursor=cursor)
246+
else:
247+
request_params = None
236248

237249
return await self.send_request(
238-
types.ClientRequest(types.ListResourcesRequest(params=params)),
250+
types.ClientRequest(types.ListResourcesRequest(params=request_params)),
239251
types.ListResourcesResult,
240252
)
241253

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

263288
return await self.send_request(
264-
types.ClientRequest(types.ListResourceTemplatesRequest(params=params)),
289+
types.ClientRequest(types.ListResourceTemplatesRequest(params=request_params)),
265290
types.ListResourceTemplatesResult,
266291
)
267292

@@ -330,7 +355,7 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
330355
"""Validate the structured content of a tool result against its output schema."""
331356
if name not in self._tool_output_schemas:
332357
# refresh output schema cache
333-
await self.list_tools()
358+
await self.list_tools() # type: ignore[reportDeprecated]
334359

335360
output_schema = None
336361
if name in self._tool_output_schemas:
@@ -348,6 +373,16 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
348373
except SchemaError as e:
349374
raise RuntimeError(f"Invalid schema for tool {name}: {e}")
350375

376+
@deprecated("Use params=PaginatedRequestParams(...) instead")
377+
@overload
378+
async def list_prompts(self, cursor: str) -> types.ListPromptsResult: ...
379+
380+
@overload
381+
async def list_prompts(self, *, params: types.PaginatedRequestParams) -> types.ListPromptsResult: ...
382+
383+
@overload
384+
async def list_prompts(self) -> types.ListPromptsResult: ...
385+
351386
async def list_prompts(
352387
self,
353388
cursor: str | None = None,
@@ -357,20 +392,21 @@ async def list_prompts(
357392
"""Send a prompts/list request.
358393
359394
Args:
360-
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
361-
params: Pagination parameters. Defaults to None (omits params field).
395+
cursor: Simple cursor string for pagination (deprecated, use params instead)
396+
params: Full pagination parameters including cursor and any future fields
362397
"""
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)
398+
if params is not None and cursor is not None:
399+
raise ValueError("Cannot specify both cursor and params")
400+
401+
if params is not None:
402+
request_params = params
403+
elif cursor is not None:
404+
request_params = types.PaginatedRequestParams(cursor=cursor)
405+
else:
406+
request_params = None
371407

372408
return await self.send_request(
373-
types.ClientRequest(types.ListPromptsRequest(params=params)),
409+
types.ClientRequest(types.ListPromptsRequest(params=request_params)),
374410
types.ListPromptsResult,
375411
)
376412

@@ -409,6 +445,16 @@ async def complete(
409445
types.CompleteResult,
410446
)
411447

448+
@deprecated("Use params=PaginatedRequestParams(...) instead")
449+
@overload
450+
async def list_tools(self, cursor: str) -> types.ListToolsResult: ...
451+
452+
@overload
453+
async def list_tools(self, *, params: types.PaginatedRequestParams) -> types.ListToolsResult: ...
454+
455+
@overload
456+
async def list_tools(self) -> types.ListToolsResult: ...
457+
412458
async def list_tools(
413459
self,
414460
cursor: str | None = None,
@@ -418,20 +464,21 @@ async def list_tools(
418464
"""Send a tools/list request.
419465
420466
Args:
421-
cursor: (Deprecated) Pagination cursor. Use params parameter instead.
422-
params: Pagination parameters. Defaults to None (omits params field).
467+
cursor: Simple cursor string for pagination (deprecated, use params instead)
468+
params: Full pagination parameters including cursor and any future fields
423469
"""
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)
470+
if params is not None and cursor is not None:
471+
raise ValueError("Cannot specify both cursor and params")
472+
473+
if params is not None:
474+
request_params = params
475+
elif cursor is not None:
476+
request_params = types.PaginatedRequestParams(cursor=cursor)
477+
else:
478+
request_params = None
432479

433480
result = await self.send_request(
434-
types.ClientRequest(types.ListToolsRequest(params=params)),
481+
types.ClientRequest(types.ListToolsRequest(params=request_params)),
435482
types.ListToolsResult,
436483
)
437484

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)