[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
!!! warning "Experimental APIs"
!!! warning "Deprecated"
The features in this section are experimental and may change without notice.
They track the evolving MCP specification and are not yet stable.
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 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
@ -36,8 +37,10 @@ async def handle_get_task(request: GetTaskRequest) -> GetTaskResult:
result = await session.experimental.call_tool_as_task("tool_name", {"arg": "value"})
```
Accessing the `.experimental` properties emits a `DeprecationWarning`.
## Providing Feedback
Since these features are experimental, feedback is especially valuable. If you encounter
issues or have suggestions, please open an issue on the
If you rely on these features and have feedback on their deprecation or the planned
MCP extension, please open an issue on the
[python-sdk repository](https://github.com/modelcontextprotocol/python-sdk/issues).

View file

@ -1,8 +1,10 @@
# 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.

View file

@ -1,8 +1,10 @@
# 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.

View file

@ -1,9 +1,10 @@
# Tasks
!!! warning "Experimental"
!!! warning "Deprecated"
Tasks are an experimental feature tracking the draft MCP specification.
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.
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
# and refactor code that is not public.
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 = "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/snippets/clients/logging_client.py", reportUndefinedVariable = false, reportUnknownArgumentType = false },
{ root = "examples/snippets/clients/roots_example.py", reportUndefinedVariable = false, reportUnknownArgumentType = false, reportArgumentType = false },

View file

@ -1,4 +1,5 @@
import logging
import warnings
from datetime import timedelta
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")
# 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")
@ -143,6 +153,8 @@ class ClientSession(
self._experimental_features: ExperimentalClientFeatures | None = None
# 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()
async def initialize(self) -> types.InitializeResult:
@ -204,10 +216,16 @@ class ClientSession(
return self._server_capabilities
@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:
"""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:
status = await session.experimental.get_task(task_id)

View file

@ -6,6 +6,7 @@ from contextlib import asynccontextmanager
import anyio
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from pydantic import ValidationError
from typing_extensions import deprecated
from websockets.asyncio.client import connect as ws_connect
from websockets.typing import Subprotocol
@ -15,6 +16,10 @@ from mcp.shared.message import SessionMessage
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
async def websocket_client(
url: str,
@ -25,6 +30,9 @@ async def websocket_client(
"""
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:
(read_stream, write_stream)

View file

@ -252,7 +252,8 @@ class Experimental:
task_group = support.task_group
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:
task_id = scoped_task_id(session_scope)

View file

@ -488,12 +488,13 @@ class ServerTaskContext:
create_result = CreateTaskResult.model_validate(response_data)
client_task_id = create_result.task.taskId
# Poll the client's task using session.experimental
async for _ in self._session.experimental.poll_task(client_task_id):
# Poll the client's task using the session's experimental features
features = self._session._experimental # pyright: ignore[reportPrivateUsage]
async for _ in features.poll_task(client_task_id):
pass
# Get final result from client
result = await self._session.experimental.get_task_result(
result = await features.get_task_result(
client_task_id,
ElicitResult,
)
@ -594,12 +595,13 @@ class ServerTaskContext:
create_result = CreateTaskResult.model_validate(response_data)
client_task_id = create_result.task.taskId
# Poll the client's task using session.experimental
async for _ in self._session.experimental.poll_task(client_task_id):
# Poll the client's task using the session's experimental features
features = self._session._experimental # pyright: ignore[reportPrivateUsage]
async for _ in features.poll_task(client_task_id):
pass
# Get final result from client
result = await self._session.experimental.get_task_result(
result = await features.get_task_result(
client_task_id,
CreateMessageResult,
)

View file

@ -106,8 +106,10 @@ class TaskSupport:
stateless: Whether the session belongs to a stateless server run
"""
session.add_response_router(self.handler)
if not stateless and session.experimental.task_session_scope is None:
session.experimental.task_session_scope = new_session_scope()
if not stateless:
features = session._experimental # pyright: ignore[reportPrivateUsage]
if features.task_session_scope is None:
features.task_session_scope = new_session_scope()
@classmethod
def in_memory(cls) -> "TaskSupport":

View file

@ -129,7 +129,8 @@ class ExperimentalHandlers:
def _requestor_session_scope(self) -> str | None:
"""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:
"""Reject task IDs that belong to a different session.

View file

@ -80,7 +80,7 @@ import anyio
import jsonschema
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from pydantic import AnyUrl
from typing_extensions import TypeVar
from typing_extensions import TypeVar, deprecated
import mcp.types as types
from mcp.server.experimental.request_context import Experimental
@ -244,10 +244,16 @@ class Server(Generic[LifespanResultT, RequestT]):
return request_ctx.get()
@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:
"""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

View file

@ -44,6 +44,7 @@ import anyio
import anyio.lowlevel
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from pydantic import AnyUrl
from typing_extensions import deprecated
import mcp.types as types
from mcp.server.experimental.session_features import ExperimentalServerSessionFeatures
@ -108,15 +109,26 @@ class ServerSession(
return self._client_params # pragma: no cover
@property
def experimental(self) -> ExperimentalServerSessionFeatures:
"""Experimental APIs for server→client task operations.
WARNING: These APIs are experimental and may change without notice.
"""
def _experimental(self) -> ExperimentalServerSessionFeatures:
"""Internal accessor for experimental features that skips the deprecation warning."""
if self._experimental_features is None:
self._experimental_features = ExperimentalServerSessionFeatures(self)
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
"""Check if the client supports a specific capability."""
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 starlette.types import Receive, Scope, Send
from starlette.websockets import WebSocket
from typing_extensions import deprecated
import mcp.types as types
from mcp.shared.message import SessionMessage
@ -13,11 +14,18 @@ from mcp.shared.message import SessionMessage
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):
"""
WebSocket server transport for MCP. This is an ASGI application, suitable to be
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)

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.
with pytest.raises(RuntimeError, match="Task support not enabled"):
# 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

View file

@ -10,6 +10,7 @@ import uvicorn
from pydantic import AnyUrl
from starlette.applications import Starlette
from starlette.routing import WebSocketRoute
from starlette.types import Message
from starlette.websockets import WebSocket
from mcp.client.session import ClientSession
@ -30,6 +31,11 @@ from tests.test_helpers import wait_for_server
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
def server_port() -> int:
@ -198,3 +204,22 @@ async def test_ws_client_timeout(
assert len(result.contents) > 0
assert isinstance(result.contents[0], TextResourceContents)
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)