Remove deprecated cursor parameter and ResourceReference (#1871)
This commit is contained in:
parent
72a8631f0a
commit
812a46ab97
4 changed files with 48 additions and 228 deletions
|
|
@ -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).
|
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):**
|
**Before (v1):**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mcp.types import Content
|
from mcp.types import Content, ResourceReference
|
||||||
```
|
```
|
||||||
|
|
||||||
**After (v2):**
|
**After (v2):**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from mcp.types import ContentBlock
|
from mcp.types import ContentBlock, ResourceTemplateReference
|
||||||
```
|
```
|
||||||
|
|
||||||
### `args` parameter removed from `ClientSessionGroup.call_tool()`
|
### `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"})
|
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
|
## Deprecations
|
||||||
|
|
||||||
<!-- Add deprecations below -->
|
<!-- Add deprecations below -->
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Protocol, overload
|
from typing import Any, Protocol
|
||||||
|
|
||||||
import anyio.lowlevel
|
import anyio.lowlevel
|
||||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||||
from pydantic import AnyUrl, TypeAdapter
|
from pydantic import AnyUrl, TypeAdapter
|
||||||
from typing_extensions import deprecated
|
|
||||||
|
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.client.experimental import ExperimentalClientFeatures
|
from mcp.client.experimental import ExperimentalClientFeatures
|
||||||
|
|
@ -256,112 +255,48 @@ class ClientSession(
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overload
|
async def list_resources(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListResourcesResult:
|
||||||
@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:
|
|
||||||
"""Send a resources/list request.
|
"""Send a resources/list request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cursor: Simple cursor string for pagination (deprecated, use params instead)
|
|
||||||
params: Full pagination parameters including cursor and any future fields
|
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(
|
return await self.send_request(
|
||||||
types.ClientRequest(types.ListResourcesRequest(params=request_params)),
|
types.ClientRequest(types.ListResourcesRequest(params=params)),
|
||||||
types.ListResourcesResult,
|
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(
|
async def list_resource_templates(
|
||||||
self, *, params: types.PaginatedRequestParams | None
|
self, *, params: types.PaginatedRequestParams | None = 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,
|
|
||||||
) -> types.ListResourceTemplatesResult:
|
) -> types.ListResourceTemplatesResult:
|
||||||
"""Send a resources/templates/list request.
|
"""Send a resources/templates/list request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cursor: Simple cursor string for pagination (deprecated, use params instead)
|
|
||||||
params: Full pagination parameters including cursor and any future fields
|
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(
|
return await self.send_request(
|
||||||
types.ClientRequest(types.ListResourceTemplatesRequest(params=request_params)),
|
types.ClientRequest(types.ListResourceTemplatesRequest(params=params)),
|
||||||
types.ListResourceTemplatesResult,
|
types.ListResourceTemplatesResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult:
|
async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult:
|
||||||
"""Send a resources/read request."""
|
"""Send a resources/read request."""
|
||||||
return await self.send_request(
|
return await self.send_request(
|
||||||
types.ClientRequest(
|
types.ClientRequest(types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=uri))),
|
||||||
types.ReadResourceRequest(
|
|
||||||
params=types.ReadResourceRequestParams(uri=uri),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
types.ReadResourceResult,
|
types.ReadResourceResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
|
async def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
|
||||||
"""Send a resources/subscribe request."""
|
"""Send a resources/subscribe request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request( # pragma: no cover
|
||||||
types.ClientRequest(
|
types.ClientRequest(types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri))),
|
||||||
types.SubscribeRequest(
|
|
||||||
params=types.SubscribeRequestParams(uri=uri),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
|
async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
|
||||||
"""Send a resources/unsubscribe request."""
|
"""Send a resources/unsubscribe request."""
|
||||||
return await self.send_request( # pragma: no cover
|
return await self.send_request( # pragma: no cover
|
||||||
types.ClientRequest(
|
types.ClientRequest(types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri))),
|
||||||
types.UnsubscribeRequest(
|
|
||||||
params=types.UnsubscribeRequestParams(uri=uri),
|
|
||||||
)
|
|
||||||
),
|
|
||||||
types.EmptyResult,
|
types.EmptyResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -422,40 +357,14 @@ class ClientSession(
|
||||||
except SchemaError as e: # pragma: no cover
|
except SchemaError as e: # pragma: no cover
|
||||||
raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover
|
raise RuntimeError(f"Invalid schema for tool {name}: {e}") # pragma: no cover
|
||||||
|
|
||||||
@overload
|
async def list_prompts(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListPromptsResult:
|
||||||
@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:
|
|
||||||
"""Send a prompts/list request.
|
"""Send a prompts/list request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cursor: Simple cursor string for pagination (deprecated, use params instead)
|
|
||||||
params: Full pagination parameters including cursor and any future fields
|
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(
|
return await self.send_request(
|
||||||
types.ClientRequest(types.ListPromptsRequest(params=request_params)),
|
types.ClientRequest(types.ListPromptsRequest(params=params)),
|
||||||
types.ListPromptsResult,
|
types.ListPromptsResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -494,40 +403,15 @@ class ClientSession(
|
||||||
types.CompleteResult,
|
types.CompleteResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
@overload
|
async def list_tools(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListToolsResult:
|
||||||
@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:
|
|
||||||
"""Send a tools/list request.
|
"""Send a tools/list request.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cursor: Simple cursor string for pagination (deprecated, use params instead)
|
cursor: Simple cursor string for pagination (deprecated, use params instead)
|
||||||
params: Full pagination parameters including cursor and any future fields
|
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(
|
result = await self.send_request(
|
||||||
types.ClientRequest(types.ListToolsRequest(params=request_params)),
|
types.ClientRequest(types.ListToolsRequest(params=params)),
|
||||||
types.ListToolsResult,
|
types.ListToolsResult,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
|
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
|
||||||
from pydantic.networks import AnyUrl, UrlConstraints
|
from pydantic.networks import AnyUrl, UrlConstraints
|
||||||
from typing_extensions import deprecated
|
|
||||||
|
|
||||||
LATEST_PROTOCOL_VERSION = "2025-11-25"
|
LATEST_PROTOCOL_VERSION = "2025-11-25"
|
||||||
|
|
||||||
|
|
@ -1587,11 +1586,6 @@ class ResourceTemplateReference(BaseModel):
|
||||||
model_config = ConfigDict(extra="allow")
|
model_config = ConfigDict(extra="allow")
|
||||||
|
|
||||||
|
|
||||||
@deprecated("`ResourceReference` is deprecated, you should use `ResourceTemplateReference`.")
|
|
||||||
class ResourceReference(ResourceTemplateReference):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PromptReference(BaseModel):
|
class PromptReference(BaseModel):
|
||||||
"""Identifies a prompt."""
|
"""Identifies a prompt."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,65 +46,6 @@ async def full_featured_server():
|
||||||
return server
|
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(
|
@pytest.mark.parametrize(
|
||||||
"method_name,request_method",
|
"method_name,request_method",
|
||||||
[
|
[
|
||||||
|
|
@ -164,37 +105,6 @@ async def test_list_methods_params_parameter(
|
||||||
assert requests[0].params["cursor"] == "some_cursor_value"
|
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():
|
async def test_list_tools_with_strict_server_validation():
|
||||||
"""Test that list_tools works with strict servers require a params field,
|
"""Test that list_tools works with strict servers require a params field,
|
||||||
even if it is empty.
|
even if it is empty.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue