[v1.x] Deprecate the WebSocket transport and the experimental tasks entry points (#2828)

This commit is contained in:
Max 2026-06-11 09:20:19 +01:00 • committed by GitHub
commit b8f491724c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 219 additions and 32 deletions

View file

@ -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).

View file

@ -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.

View file

@ -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.

View file

@ -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.

View file

@ -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 },

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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,
) )

View file

@ -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":

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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)

View 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)

View 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()

View file

@ -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

View file

@ -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)