diff --git a/docs/migration.md b/docs/migration.md index e849c5250..c68e4856e 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -52,20 +52,25 @@ async with http_client: The `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx.AsyncClient` instead (see example above). -### `Content` type alias removed +### Removed type aliases and classes -The deprecated `Content` type alias has been removed. Use `ContentBlock` directly instead. +The following deprecated type aliases and classes have been removed from `mcp.types`: + +| Removed | Replacement | +|---------|-------------| +| `Content` | `ContentBlock` | +| `ResourceReference` | `ResourceTemplateReference` | **Before (v1):** ```python -from mcp.types import Content +from mcp.types import Content, ResourceReference ``` **After (v2):** ```python -from mcp.types import ContentBlock +from mcp.types import ContentBlock, ResourceTemplateReference ``` ### `args` parameter removed from `ClientSessionGroup.call_tool()` @@ -84,6 +89,33 @@ result = await session_group.call_tool("my_tool", args={"key": "value"}) result = await session_group.call_tool("my_tool", arguments={"key": "value"}) ``` +### `cursor` parameter removed from `ClientSession` list methods + +The deprecated `cursor` parameter has been removed from the following `ClientSession` methods: + +- `list_resources()` +- `list_resource_templates()` +- `list_prompts()` +- `list_tools()` + +Use `params=PaginatedRequestParams(cursor=...)` instead. + +**Before (v1):** + +```python +result = await session.list_resources(cursor="next_page_token") +result = await session.list_tools(cursor="next_page_token") +``` + +**After (v2):** + +```python +from mcp.types import PaginatedRequestParams + +result = await session.list_resources(params=PaginatedRequestParams(cursor="next_page_token")) +result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token")) +``` + ## Deprecations diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index b61bf0b03..de87b19aa 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -1,10 +1,9 @@ import logging -from typing import Any, Protocol, overload +from typing import Any, Protocol import anyio.lowlevel from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from pydantic import AnyUrl, TypeAdapter -from typing_extensions import deprecated import mcp.types as types from mcp.client.experimental import ExperimentalClientFeatures @@ -256,112 +255,48 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul types.EmptyResult, ) - @overload - @deprecated("Use list_resources(params=PaginatedRequestParams(...)) instead") - async def list_resources(self, cursor: str | None) -> types.ListResourcesResult: ... - - @overload - async def list_resources(self, *, params: types.PaginatedRequestParams | None) -> types.ListResourcesResult: ... - - @overload - async def list_resources(self) -> types.ListResourcesResult: ... - - async def list_resources( - self, - cursor: str | None = None, - *, - params: types.PaginatedRequestParams | None = None, - ) -> types.ListResourcesResult: + async def list_resources(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListResourcesResult: """Send a resources/list request. Args: - cursor: Simple cursor string for pagination (deprecated, use params instead) params: Full pagination parameters including cursor and any future fields """ - if params is not None and cursor is not None: - raise ValueError("Cannot specify both cursor and params") - - if params is not None: - request_params = params - elif cursor is not None: - request_params = types.PaginatedRequestParams(cursor=cursor) - else: - request_params = None - return await self.send_request( - types.ClientRequest(types.ListResourcesRequest(params=request_params)), + types.ClientRequest(types.ListResourcesRequest(params=params)), types.ListResourcesResult, ) - @overload - @deprecated("Use list_resource_templates(params=PaginatedRequestParams(...)) instead") - async def list_resource_templates(self, cursor: str | None) -> types.ListResourceTemplatesResult: ... - - @overload async def list_resource_templates( - self, *, params: types.PaginatedRequestParams | None - ) -> types.ListResourceTemplatesResult: ... - - @overload - async def list_resource_templates(self) -> types.ListResourceTemplatesResult: ... - - async def list_resource_templates( - self, - cursor: str | None = None, - *, - params: types.PaginatedRequestParams | None = None, + self, *, params: types.PaginatedRequestParams | None = None ) -> types.ListResourceTemplatesResult: """Send a resources/templates/list request. Args: - cursor: Simple cursor string for pagination (deprecated, use params instead) params: Full pagination parameters including cursor and any future fields """ - if params is not None and cursor is not None: - raise ValueError("Cannot specify both cursor and params") - - if params is not None: - request_params = params - elif cursor is not None: - request_params = types.PaginatedRequestParams(cursor=cursor) - else: - request_params = None - return await self.send_request( - types.ClientRequest(types.ListResourceTemplatesRequest(params=request_params)), + types.ClientRequest(types.ListResourceTemplatesRequest(params=params)), types.ListResourceTemplatesResult, ) async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult: """Send a resources/read request.""" return await self.send_request( - types.ClientRequest( - types.ReadResourceRequest( - params=types.ReadResourceRequestParams(uri=uri), - ) - ), + types.ClientRequest(types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=uri))), types.ReadResourceResult, ) async def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult: """Send a resources/subscribe request.""" return await self.send_request( # pragma: no cover - types.ClientRequest( - types.SubscribeRequest( - params=types.SubscribeRequestParams(uri=uri), - ) - ), + types.ClientRequest(types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri))), types.EmptyResult, ) async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult: """Send a resources/unsubscribe request.""" return await self.send_request( # pragma: no cover - types.ClientRequest( - types.UnsubscribeRequest( - params=types.UnsubscribeRequestParams(uri=uri), - ) - ), + types.ClientRequest(types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri))), types.EmptyResult, ) @@ -422,40 +357,14 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) - except SchemaError as e: # pragma: no cover raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover - @overload - @deprecated("Use list_prompts(params=PaginatedRequestParams(...)) instead") - async def list_prompts(self, cursor: str | None) -> types.ListPromptsResult: ... - - @overload - async def list_prompts(self, *, params: types.PaginatedRequestParams | None) -> types.ListPromptsResult: ... - - @overload - async def list_prompts(self) -> types.ListPromptsResult: ... - - async def list_prompts( - self, - cursor: str | None = None, - *, - params: types.PaginatedRequestParams | None = None, - ) -> types.ListPromptsResult: + async def list_prompts(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListPromptsResult: """Send a prompts/list request. Args: - cursor: Simple cursor string for pagination (deprecated, use params instead) params: Full pagination parameters including cursor and any future fields """ - if params is not None and cursor is not None: - raise ValueError("Cannot specify both cursor and params") - - if params is not None: - request_params = params - elif cursor is not None: - request_params = types.PaginatedRequestParams(cursor=cursor) - else: - request_params = None - return await self.send_request( - types.ClientRequest(types.ListPromptsRequest(params=request_params)), + types.ClientRequest(types.ListPromptsRequest(params=params)), types.ListPromptsResult, ) @@ -494,40 +403,15 @@ async def complete( types.CompleteResult, ) - @overload - @deprecated("Use list_tools(params=PaginatedRequestParams(...)) instead") - async def list_tools(self, cursor: str | None) -> types.ListToolsResult: ... - - @overload - async def list_tools(self, *, params: types.PaginatedRequestParams | None) -> types.ListToolsResult: ... - - @overload - async def list_tools(self) -> types.ListToolsResult: ... - - async def list_tools( - self, - cursor: str | None = None, - *, - params: types.PaginatedRequestParams | None = None, - ) -> types.ListToolsResult: + async def list_tools(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListToolsResult: """Send a tools/list request. Args: cursor: Simple cursor string for pagination (deprecated, use params instead) params: Full pagination parameters including cursor and any future fields """ - if params is not None and cursor is not None: - raise ValueError("Cannot specify both cursor and params") - - if params is not None: - request_params = params - elif cursor is not None: - request_params = types.PaginatedRequestParams(cursor=cursor) - else: - request_params = None - result = await self.send_request( - types.ClientRequest(types.ListToolsRequest(params=request_params)), + types.ClientRequest(types.ListToolsRequest(params=params)), types.ListToolsResult, ) diff --git a/src/mcp/types.py b/src/mcp/types.py index 84f6acdaa..2671eb3f7 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -6,7 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel from pydantic.networks import AnyUrl, UrlConstraints -from typing_extensions import deprecated LATEST_PROTOCOL_VERSION = "2025-11-25" @@ -1587,11 +1586,6 @@ class ResourceTemplateReference(BaseModel): model_config = ConfigDict(extra="allow") -@deprecated("`ResourceReference` is deprecated, you should use `ResourceTemplateReference`.") -class ResourceReference(ResourceTemplateReference): - pass - - class PromptReference(BaseModel): """Identifies a prompt.""" diff --git a/tests/client/test_list_methods_cursor.py b/tests/client/test_list_methods_cursor.py index 94a72c34e..9b6e886f9 100644 --- a/tests/client/test_list_methods_cursor.py +++ b/tests/client/test_list_methods_cursor.py @@ -46,65 +46,6 @@ async def test_template(name: str) -> str: # pragma: no cover return server -@pytest.mark.parametrize( - "method_name,request_method", - [ - ("list_tools", "tools/list"), - ("list_resources", "resources/list"), - ("list_prompts", "prompts/list"), - ("list_resource_templates", "resources/templates/list"), - ], -) -@pytest.mark.filterwarnings("ignore::DeprecationWarning") -async def test_list_methods_cursor_parameter( - stream_spy: Callable[[], StreamSpyCollection], - full_featured_server: FastMCP, - method_name: str, - request_method: str, -): - """Test that the cursor parameter is accepted and correctly passed to the server. - - Covers: list_tools, list_resources, list_prompts, list_resource_templates - - See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format - """ - async with create_session(full_featured_server._mcp_server) as client_session: - spies = stream_spy() - - # Test without cursor parameter (omitted) - method = getattr(client_session, method_name) - _ = await method() - requests = spies.get_client_requests(method=request_method) - assert len(requests) == 1 - assert requests[0].params is None - - spies.clear() - - # Test with cursor=None - _ = await method(cursor=None) - requests = spies.get_client_requests(method=request_method) - assert len(requests) == 1 - assert requests[0].params is None - - spies.clear() - - # Test with cursor as string - _ = await method(cursor="some_cursor_value") - requests = spies.get_client_requests(method=request_method) - assert len(requests) == 1 - assert requests[0].params is not None - assert requests[0].params["cursor"] == "some_cursor_value" - - spies.clear() - - # Test with empty string cursor - _ = await method(cursor="") - requests = spies.get_client_requests(method=request_method) - assert len(requests) == 1 - assert requests[0].params is not None - assert requests[0].params["cursor"] == "" - - @pytest.mark.parametrize( "method_name,request_method", [ @@ -164,37 +105,6 @@ async def test_list_methods_params_parameter( assert requests[0].params["cursor"] == "some_cursor_value" -@pytest.mark.parametrize( - "method_name", - [ - "list_tools", - "list_resources", - "list_prompts", - "list_resource_templates", - ], -) -async def test_list_methods_raises_error_when_both_cursor_and_params_provided( - full_featured_server: FastMCP, - method_name: str, -): - """Test that providing both cursor and params raises ValueError. - - Covers: list_tools, list_resources, list_prompts, list_resource_templates - - When both cursor and params are provided, a ValueError should be raised - to prevent ambiguity. - """ - async with create_session(full_featured_server._mcp_server) as client_session: - method = getattr(client_session, method_name) - - # Call with both cursor and params - should raise ValueError - with pytest.raises(ValueError, match="Cannot specify both cursor and params"): - await method( - cursor="old_cursor", - params=types.PaginatedRequestParams(cursor="new_cursor"), - ) - - async def test_list_tools_with_strict_server_validation(): """Test that list_tools works with strict servers require a params field, even if it is empty.