[v1.x] Deprecate the WebSocket transport and the experimental tasks entry points (#2828)
This commit is contained in:
parent
2309e5ef97
commit
b8f491724c
18 changed files with 219 additions and 32 deletions
|
|
@ -1,12 +1,13 @@
|
||||||
# Experimental Features
|
# Experimental Features
|
||||||
|
|
||||||
!!! warning "Experimental APIs"
|
!!! warning "Deprecated"
|
||||||
|
|
||||||
The features in this section are experimental and may change without notice.
|
The experimental tasks API is deprecated and will be removed in mcp 2.0.
|
||||||
They track the evolving MCP specification and are not yet stable.
|
Tasks (SEP-1686) were removed from the MCP specification and are expected
|
||||||
|
to return as a separate MCP extension in a future release.
|
||||||
|
|
||||||
This section documents experimental features in the MCP Python SDK. These features
|
This section documents experimental features in the MCP Python SDK. These features
|
||||||
implement draft specifications that are still being refined.
|
are deprecated and remain available on the 1.x line only for existing users.
|
||||||
|
|
||||||
## Available Experimental Features
|
## Available Experimental Features
|
||||||
|
|
||||||
|
|
@ -36,8 +37,10 @@ async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
|
||||||
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
|
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Accessing the `.experimental` properties emits a `DeprecationWarning`.
|
||||||
|
|
||||||
## Providing Feedback
|
## Providing Feedback
|
||||||
|
|
||||||
Since these features are experimental, feedback is especially valuable. If you encounter
|
If you rely on these features and have feedback on their deprecation or the planned
|
||||||
issues or have suggestions, please open an issue on the
|
MCP extension, please open an issue on the
|
||||||
[python-sdk repository](https://github.com/modelcontextprotocol/python-sdk/issues).
|
[python-sdk repository](https://github.com/modelcontextprotocol/python-sdk/issues).
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
# Client Task Usage
|
# Client Task Usage
|
||||||
|
|
||||||
!!! warning "Experimental"
|
!!! warning "Deprecated"
|
||||||
|
|
||||||
Tasks are an experimental feature. The API may change without notice.
|
The experimental tasks API is deprecated and will be removed in mcp 2.0.
|
||||||
|
Tasks (SEP-1686) were removed from the MCP specification and are expected
|
||||||
|
to return as a separate MCP extension in a future release.
|
||||||
|
|
||||||
This guide covers calling task-augmented tools from clients, handling the `input_required` status, and advanced patterns like receiving task requests from servers.
|
This guide covers calling task-augmented tools from clients, handling the `input_required` status, and advanced patterns like receiving task requests from servers.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
# Server Task Implementation
|
# Server Task Implementation
|
||||||
|
|
||||||
!!! warning "Experimental"
|
!!! warning "Deprecated"
|
||||||
|
|
||||||
Tasks are an experimental feature. The API may change without notice.
|
The experimental tasks API is deprecated and will be removed in mcp 2.0.
|
||||||
|
Tasks (SEP-1686) were removed from the MCP specification and are expected
|
||||||
|
to return as a separate MCP extension in a future release.
|
||||||
|
|
||||||
This guide covers implementing task support in MCP servers, from basic setup to advanced patterns like elicitation and sampling within tasks.
|
This guide covers implementing task support in MCP servers, from basic setup to advanced patterns like elicitation and sampling within tasks.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
# Tasks
|
# Tasks
|
||||||
|
|
||||||
!!! warning "Experimental"
|
!!! warning "Deprecated"
|
||||||
|
|
||||||
Tasks are an experimental feature tracking the draft MCP specification.
|
The experimental tasks API is deprecated and will be removed in mcp 2.0.
|
||||||
The API may change without notice.
|
Tasks (SEP-1686) were removed from the MCP specification and are expected
|
||||||
|
to return as a separate MCP extension in a future release.
|
||||||
|
|
||||||
Tasks enable asynchronous request handling in MCP. Instead of blocking until an operation completes, the receiver creates a task, returns immediately, and the requestor polls for the result.
|
Tasks enable asynchronous request handling in MCP. Instead of blocking until an operation completes, the receiver creates a task, returns immediately, and the requestor polls for the result.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,13 @@ venv = ".venv"
|
||||||
# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source
|
# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source
|
||||||
# and refactor code that is not public.
|
# and refactor code that is not public.
|
||||||
executionEnvironments = [
|
executionEnvironments = [
|
||||||
|
# The experimental tasks API and the WebSocket transport are deprecated; the suites and
|
||||||
|
# examples that cover them intentionally use the deprecated APIs.
|
||||||
|
{ root = "tests/experimental", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false, reportDeprecated = false },
|
||||||
|
{ root = "tests/shared/test_ws.py", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false, reportDeprecated = false },
|
||||||
{ root = "tests", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false },
|
{ root = "tests", extraPaths = ["."], reportUnusedFunction = false, reportPrivateUsage = false },
|
||||||
|
{ root = "examples/servers/simple-task", reportUnusedFunction = false, reportDeprecated = false },
|
||||||
|
{ root = "examples/servers/simple-task-interactive", reportUnusedFunction = false, reportDeprecated = false },
|
||||||
{ root = "examples/servers", reportUnusedFunction = false },
|
{ root = "examples/servers", reportUnusedFunction = false },
|
||||||
{ root = "examples/snippets/clients/logging_client.py", reportUndefinedVariable = false, reportUnknownArgumentType = false },
|
{ root = "examples/snippets/clients/logging_client.py", reportUndefinedVariable = false, reportUnknownArgumentType = false },
|
||||||
{ root = "examples/snippets/clients/roots_example.py", reportUndefinedVariable = false, reportUnknownArgumentType = false, reportArgumentType = false },
|
{ root = "examples/snippets/clients/roots_example.py", reportUndefinedVariable = false, reportUnknownArgumentType = false, reportArgumentType = false },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import warnings
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Protocol, overload
|
from typing import Any, Protocol, overload
|
||||||
|
|
||||||
|
|
@ -17,6 +18,15 @@ from mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS
|
||||||
|
|
||||||
DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
|
DEFAULT_CLIENT_INFO = types.Implementation(name="mcp", version="0.1.0")
|
||||||
|
|
||||||
|
# Type checkers only surface `@deprecated` messages given as string literals, so the same text is
|
||||||
|
# repeated inline at each deprecated entry point (here, server/session.py, server/lowlevel/server.py).
|
||||||
|
# Keep those copies, the filterwarnings marks in the tasks test suites, and the pytest.warns match in
|
||||||
|
# tests/experimental/tasks/test_deprecations.py prefix-aligned when rewording.
|
||||||
|
_EXPERIMENTAL_TASKS_DEPRECATION = (
|
||||||
|
"The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed"
|
||||||
|
" from the MCP specification and are expected to return as a separate MCP extension."
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger("client")
|
logger = logging.getLogger("client")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -143,6 +153,8 @@ class ClientSession(
|
||||||
self._experimental_features: ExperimentalClientFeatures | None = None
|
self._experimental_features: ExperimentalClientFeatures | None = None
|
||||||
|
|
||||||
# Experimental: Task handlers (use defaults if not provided)
|
# Experimental: Task handlers (use defaults if not provided)
|
||||||
|
if experimental_task_handlers is not None:
|
||||||
|
warnings.warn(_EXPERIMENTAL_TASKS_DEPRECATION, DeprecationWarning, stacklevel=2)
|
||||||
self._task_handlers = experimental_task_handlers or ExperimentalTaskHandlers()
|
self._task_handlers = experimental_task_handlers or ExperimentalTaskHandlers()
|
||||||
|
|
||||||
async def initialize(self) -> types.InitializeResult:
|
async def initialize(self) -> types.InitializeResult:
|
||||||
|
|
@ -204,10 +216,16 @@ class ClientSession(
|
||||||
return self._server_capabilities
|
return self._server_capabilities
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@deprecated(
|
||||||
|
"The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed"
|
||||||
|
" from the MCP specification and are expected to return as a separate MCP extension."
|
||||||
|
)
|
||||||
def experimental(self) -> ExperimentalClientFeatures:
|
def experimental(self) -> ExperimentalClientFeatures:
|
||||||
"""Experimental APIs for tasks and other features.
|
"""Experimental APIs for tasks and other features.
|
||||||
|
|
||||||
WARNING: These APIs are experimental and may change without notice.
|
Deprecated: the experimental tasks API will be removed in mcp 2.0. Tasks
|
||||||
|
(SEP-1686) were removed from the MCP specification and are expected to
|
||||||
|
return as a separate MCP extension.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
status = await session.experimental.get_task(task_id)
|
status = await session.experimental.get_task(task_id)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from contextlib import asynccontextmanager
|
||||||
import anyio
|
import anyio
|
||||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
from typing_extensions import deprecated
|
||||||
from websockets.asyncio.client import connect as ws_connect
|
from websockets.asyncio.client import connect as ws_connect
|
||||||
from websockets.typing import Subprotocol
|
from websockets.typing import Subprotocol
|
||||||
|
|
||||||
|
|
@ -15,6 +16,10 @@ from mcp.shared.message import SessionMessage
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(
|
||||||
|
"The WebSocket client transport is deprecated and will be removed in mcp 2.0. WebSocket was never part of"
|
||||||
|
" the MCP specification; use the streamable HTTP transport (`streamable_http_client`) instead."
|
||||||
|
)
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def websocket_client(
|
async def websocket_client(
|
||||||
url: str,
|
url: str,
|
||||||
|
|
@ -25,6 +30,9 @@ async def websocket_client(
|
||||||
"""
|
"""
|
||||||
WebSocket client transport for MCP, symmetrical to the server version.
|
WebSocket client transport for MCP, symmetrical to the server version.
|
||||||
|
|
||||||
|
Deprecated: this transport will be removed in mcp 2.0. WebSocket was never
|
||||||
|
part of the MCP specification; use the streamable HTTP transport instead.
|
||||||
|
|
||||||
Connects to 'url' using the 'mcp' subprotocol, then yields:
|
Connects to 'url' using the 'mcp' subprotocol, then yields:
|
||||||
(read_stream, write_stream)
|
(read_stream, write_stream)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -252,7 +252,8 @@ class Experimental:
|
||||||
task_group = support.task_group
|
task_group = support.task_group
|
||||||
|
|
||||||
if task_id is None:
|
if task_id is None:
|
||||||
session_scope = self._session.experimental.task_session_scope
|
features = self._session._experimental # pyright: ignore[reportPrivateUsage]
|
||||||
|
session_scope = features.task_session_scope
|
||||||
if session_scope is not None:
|
if session_scope is not None:
|
||||||
task_id = scoped_task_id(session_scope)
|
task_id = scoped_task_id(session_scope)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -488,12 +488,13 @@ class ServerTaskContext:
|
||||||
create_result = CreateTaskResult.model_validate(response_data)
|
create_result = CreateTaskResult.model_validate(response_data)
|
||||||
client_task_id = create_result.task.taskId
|
client_task_id = create_result.task.taskId
|
||||||
|
|
||||||
# Poll the client's task using session.experimental
|
# Poll the client's task using the session's experimental features
|
||||||
async for _ in self._session.experimental.poll_task(client_task_id):
|
features = self._session._experimental # pyright: ignore[reportPrivateUsage]
|
||||||
|
async for _ in features.poll_task(client_task_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get final result from client
|
# Get final result from client
|
||||||
result = await self._session.experimental.get_task_result(
|
result = await features.get_task_result(
|
||||||
client_task_id,
|
client_task_id,
|
||||||
ElicitResult,
|
ElicitResult,
|
||||||
)
|
)
|
||||||
|
|
@ -594,12 +595,13 @@ class ServerTaskContext:
|
||||||
create_result = CreateTaskResult.model_validate(response_data)
|
create_result = CreateTaskResult.model_validate(response_data)
|
||||||
client_task_id = create_result.task.taskId
|
client_task_id = create_result.task.taskId
|
||||||
|
|
||||||
# Poll the client's task using session.experimental
|
# Poll the client's task using the session's experimental features
|
||||||
async for _ in self._session.experimental.poll_task(client_task_id):
|
features = self._session._experimental # pyright: ignore[reportPrivateUsage]
|
||||||
|
async for _ in features.poll_task(client_task_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get final result from client
|
# Get final result from client
|
||||||
result = await self._session.experimental.get_task_result(
|
result = await features.get_task_result(
|
||||||
client_task_id,
|
client_task_id,
|
||||||
CreateMessageResult,
|
CreateMessageResult,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,10 @@ class TaskSupport:
|
||||||
stateless: Whether the session belongs to a stateless server run
|
stateless: Whether the session belongs to a stateless server run
|
||||||
"""
|
"""
|
||||||
session.add_response_router(self.handler)
|
session.add_response_router(self.handler)
|
||||||
if not stateless and session.experimental.task_session_scope is None:
|
if not stateless:
|
||||||
session.experimental.task_session_scope = new_session_scope()
|
features = session._experimental # pyright: ignore[reportPrivateUsage]
|
||||||
|
if features.task_session_scope is None:
|
||||||
|
features.task_session_scope = new_session_scope()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def in_memory(cls) -> "TaskSupport":
|
def in_memory(cls) -> "TaskSupport":
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,8 @@ class ExperimentalHandlers:
|
||||||
|
|
||||||
def _requestor_session_scope(self) -> str | None:
|
def _requestor_session_scope(self) -> str | None:
|
||||||
"""Return the task session scope of the session making the current request."""
|
"""Return the task session scope of the session making the current request."""
|
||||||
return self._server.request_context.session.experimental.task_session_scope
|
session = self._server.request_context.session
|
||||||
|
return session._experimental.task_session_scope # pyright: ignore[reportPrivateUsage]
|
||||||
|
|
||||||
def _require_task_in_requestor_scope(self, task_id: str) -> None:
|
def _require_task_in_requestor_scope(self, task_id: str) -> None:
|
||||||
"""Reject task IDs that belong to a different session.
|
"""Reject task IDs that belong to a different session.
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ import anyio
|
||||||
import jsonschema
|
import jsonschema
|
||||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
from typing_extensions import TypeVar
|
from typing_extensions import TypeVar, deprecated
|
||||||
|
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.server.experimental.request_context import Experimental
|
from mcp.server.experimental.request_context import Experimental
|
||||||
|
|
@ -244,10 +244,16 @@ class Server(Generic[LifespanResultT, RequestT]):
|
||||||
return request_ctx.get()
|
return request_ctx.get()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@deprecated(
|
||||||
|
"The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed"
|
||||||
|
" from the MCP specification and are expected to return as a separate MCP extension."
|
||||||
|
)
|
||||||
def experimental(self) -> ExperimentalHandlers:
|
def experimental(self) -> ExperimentalHandlers:
|
||||||
"""Experimental APIs for tasks and other features.
|
"""Experimental APIs for tasks and other features.
|
||||||
|
|
||||||
WARNING: These APIs are experimental and may change without notice.
|
Deprecated: the experimental tasks API will be removed in mcp 2.0. Tasks
|
||||||
|
(SEP-1686) were removed from the MCP specification and are expected to
|
||||||
|
return as a separate MCP extension.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# We create this inline so we only add these capabilities _if_ they're actually used
|
# We create this inline so we only add these capabilities _if_ they're actually used
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import anyio
|
||||||
import anyio.lowlevel
|
import anyio.lowlevel
|
||||||
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
|
from typing_extensions import deprecated
|
||||||
|
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.server.experimental.session_features import ExperimentalServerSessionFeatures
|
from mcp.server.experimental.session_features import ExperimentalServerSessionFeatures
|
||||||
|
|
@ -108,15 +109,26 @@ class ServerSession(
|
||||||
return self._client_params # pragma: no cover
|
return self._client_params # pragma: no cover
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def experimental(self) -> ExperimentalServerSessionFeatures:
|
def _experimental(self) -> ExperimentalServerSessionFeatures:
|
||||||
"""Experimental APIs for server→client task operations.
|
"""Internal accessor for experimental features that skips the deprecation warning."""
|
||||||
|
|
||||||
WARNING: These APIs are experimental and may change without notice.
|
|
||||||
"""
|
|
||||||
if self._experimental_features is None:
|
if self._experimental_features is None:
|
||||||
self._experimental_features = ExperimentalServerSessionFeatures(self)
|
self._experimental_features = ExperimentalServerSessionFeatures(self)
|
||||||
return self._experimental_features
|
return self._experimental_features
|
||||||
|
|
||||||
|
@property
|
||||||
|
@deprecated(
|
||||||
|
"The experimental tasks API is deprecated and will be removed in mcp 2.0: tasks (SEP-1686) were removed"
|
||||||
|
" from the MCP specification and are expected to return as a separate MCP extension."
|
||||||
|
)
|
||||||
|
def experimental(self) -> ExperimentalServerSessionFeatures:
|
||||||
|
"""Experimental APIs for server→client task operations.
|
||||||
|
|
||||||
|
Deprecated: the experimental tasks API will be removed in mcp 2.0. Tasks
|
||||||
|
(SEP-1686) were removed from the MCP specification and are expected to
|
||||||
|
return as a separate MCP extension.
|
||||||
|
"""
|
||||||
|
return self._experimental
|
||||||
|
|
||||||
def check_client_capability(self, capability: types.ClientCapabilities) -> bool: # pragma: no cover
|
def check_client_capability(self, capability: types.ClientCapabilities) -> bool: # pragma: no cover
|
||||||
"""Check if the client supports a specific capability."""
|
"""Check if the client supports a specific capability."""
|
||||||
if self._client_params is None:
|
if self._client_params is None:
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStre
|
||||||
from pydantic_core import ValidationError
|
from pydantic_core import ValidationError
|
||||||
from starlette.types import Receive, Scope, Send
|
from starlette.types import Receive, Scope, Send
|
||||||
from starlette.websockets import WebSocket
|
from starlette.websockets import WebSocket
|
||||||
|
from typing_extensions import deprecated
|
||||||
|
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.shared.message import SessionMessage
|
from mcp.shared.message import SessionMessage
|
||||||
|
|
@ -13,11 +14,18 @@ from mcp.shared.message import SessionMessage
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager # pragma: no cover
|
@deprecated( # pragma: no cover
|
||||||
|
"The WebSocket server transport is deprecated and will be removed in mcp 2.0. WebSocket was never part of"
|
||||||
|
" the MCP specification; use the streamable HTTP transport instead."
|
||||||
|
)
|
||||||
|
@asynccontextmanager
|
||||||
async def websocket_server(scope: Scope, receive: Receive, send: Send):
|
async def websocket_server(scope: Scope, receive: Receive, send: Send):
|
||||||
"""
|
"""
|
||||||
WebSocket server transport for MCP. This is an ASGI application, suitable to be
|
WebSocket server transport for MCP. This is an ASGI application, suitable to be
|
||||||
used with a framework like Starlette and a server like Hypercorn.
|
used with a framework like Starlette and a server like Hypercorn.
|
||||||
|
|
||||||
|
Deprecated: this transport will be removed in mcp 2.0. WebSocket was never
|
||||||
|
part of the MCP specification; use the streamable HTTP transport instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
websocket = WebSocket(scope, receive, send)
|
websocket = WebSocket(scope, receive, send)
|
||||||
|
|
|
||||||
18
tests/experimental/tasks/conftest.py
Normal file
18
tests/experimental/tasks/conftest.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Shared configuration for the experimental tasks suite."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_HERE = Path(__file__).parent
|
||||||
|
|
||||||
|
# The tasks suite intentionally exercises the deprecated experimental tasks API.
|
||||||
|
_TASKS_DEPRECATION_IGNORE = pytest.mark.filterwarnings(
|
||||||
|
"ignore:The experimental tasks API is deprecated:DeprecationWarning"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
|
||||||
|
for item in items:
|
||||||
|
if _HERE in item.path.parents:
|
||||||
|
item.add_marker(_TASKS_DEPRECATION_IGNORE)
|
||||||
72
tests/experimental/tasks/test_deprecations.py
Normal file
72
tests/experimental/tasks/test_deprecations.py
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"""Tests for the deprecation warnings on the experimental tasks entry points."""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import mcp.types as types
|
||||||
|
from mcp.client.experimental.task_handlers import ExperimentalTaskHandlers
|
||||||
|
from mcp.client.session import ClientSession
|
||||||
|
from mcp.server.lowlevel import Server
|
||||||
|
from mcp.server.models import InitializationOptions
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
from mcp.shared.memory import create_client_server_memory_streams, create_connected_server_and_client_session
|
||||||
|
|
||||||
|
_DEPRECATION_MATCH = "The experimental tasks API is deprecated"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_experimental_property_is_deprecated() -> None:
|
||||||
|
async with create_client_server_memory_streams() as (client_streams, _):
|
||||||
|
read_stream, write_stream = client_streams
|
||||||
|
session = ClientSession(read_stream, write_stream)
|
||||||
|
with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH):
|
||||||
|
features = session.experimental
|
||||||
|
# The cached path warns as well
|
||||||
|
with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH):
|
||||||
|
assert session.experimental is features
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_client_session_experimental_task_handlers_kwarg_is_deprecated() -> None:
|
||||||
|
async with create_client_server_memory_streams() as (client_streams, _):
|
||||||
|
read_stream, write_stream = client_streams
|
||||||
|
with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH):
|
||||||
|
ClientSession(read_stream, write_stream, experimental_task_handlers=ExperimentalTaskHandlers())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_server_session_experimental_property_is_deprecated() -> None:
|
||||||
|
init_options = InitializationOptions(
|
||||||
|
server_name="test-server",
|
||||||
|
server_version="0.1.0",
|
||||||
|
capabilities=types.ServerCapabilities(),
|
||||||
|
)
|
||||||
|
async with create_client_server_memory_streams() as (_, server_streams):
|
||||||
|
read_stream, write_stream = server_streams
|
||||||
|
async with ServerSession(read_stream, write_stream, init_options) as session:
|
||||||
|
with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH):
|
||||||
|
features = session.experimental
|
||||||
|
# The cached path warns as well. coverage.py misreports the branch arcs of the
|
||||||
|
# last statement in a nested `async with` body on Python 3.11+.
|
||||||
|
with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH): # pragma: no branch
|
||||||
|
assert session.experimental is features
|
||||||
|
|
||||||
|
|
||||||
|
def test_lowlevel_server_experimental_property_is_deprecated() -> None:
|
||||||
|
server: Server = Server("test-server")
|
||||||
|
with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH):
|
||||||
|
handlers = server.experimental
|
||||||
|
# The cached path warns as well
|
||||||
|
with pytest.warns(DeprecationWarning, match=_DEPRECATION_MATCH):
|
||||||
|
assert server.experimental is handlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_plain_session_usage_does_not_warn() -> None:
|
||||||
|
"""Clients and servers that don't touch the tasks API must not see deprecation warnings."""
|
||||||
|
server: Server = Server("test-server")
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("error")
|
||||||
|
async with create_connected_server_and_client_session(server) as session:
|
||||||
|
await session.send_ping()
|
||||||
|
|
@ -181,7 +181,7 @@ async def test_run_task_with_an_explicit_task_id_emits_a_deprecation_warning() -
|
||||||
# deprecated argument has been reported.
|
# deprecated argument has been reported.
|
||||||
with pytest.raises(RuntimeError, match="Task support not enabled"):
|
with pytest.raises(RuntimeError, match="Task support not enabled"):
|
||||||
# The deliberate use of the deprecated overload is the point of this test.
|
# The deliberate use of the deprecated overload is the point of this test.
|
||||||
await exp.run_task(work, task_id="explicitly-chosen") # pyright: ignore[reportDeprecated]
|
await exp.run_task(work, task_id="explicitly-chosen")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import uvicorn
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
from starlette.routing import WebSocketRoute
|
from starlette.routing import WebSocketRoute
|
||||||
|
from starlette.types import Message
|
||||||
from starlette.websockets import WebSocket
|
from starlette.websockets import WebSocket
|
||||||
|
|
||||||
from mcp.client.session import ClientSession
|
from mcp.client.session import ClientSession
|
||||||
|
|
@ -30,6 +31,11 @@ from tests.test_helpers import wait_for_server
|
||||||
|
|
||||||
SERVER_NAME = "test_server_for_WS"
|
SERVER_NAME = "test_server_for_WS"
|
||||||
|
|
||||||
|
# This suite intentionally exercises the deprecated WebSocket transport.
|
||||||
|
pytestmark = pytest.mark.filterwarnings(
|
||||||
|
"ignore:The WebSocket (client|server) transport is deprecated:DeprecationWarning"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def server_port() -> int:
|
def server_port() -> int:
|
||||||
|
|
@ -198,3 +204,22 @@ async def test_ws_client_timeout(
|
||||||
assert len(result.contents) > 0
|
assert len(result.contents) > 0
|
||||||
assert isinstance(result.contents[0], TextResourceContents)
|
assert isinstance(result.contents[0], TextResourceContents)
|
||||||
assert result.contents[0].text == "Read example"
|
assert result.contents[0].text == "Read example"
|
||||||
|
|
||||||
|
|
||||||
|
def test_websocket_client_is_deprecated() -> None:
|
||||||
|
"""Creating the websocket_client context manager emits a DeprecationWarning."""
|
||||||
|
with pytest.warns(DeprecationWarning, match="The WebSocket client transport is deprecated"):
|
||||||
|
websocket_client("ws://127.0.0.1:1/ws")
|
||||||
|
|
||||||
|
|
||||||
|
def test_websocket_server_is_deprecated() -> None:
|
||||||
|
"""Creating the websocket_server context manager emits a DeprecationWarning."""
|
||||||
|
|
||||||
|
async def receive() -> Message:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
async def send(message: Message) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
with pytest.warns(DeprecationWarning, match="The WebSocket server transport is deprecated"):
|
||||||
|
websocket_server({"type": "websocket"}, receive, send)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue