Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions docs/experimental/index.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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).
6 changes: 4 additions & 2 deletions docs/experimental/tasks-client.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/experimental/tasks-server.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
7 changes: 4 additions & 3 deletions docs/experimental/tasks.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
20 changes: 19 additions & 1 deletion src/mcp/client/session.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import warnings
from datetime import timedelta
from typing import Any, Protocol, overload

Expand All @@ -17,6 +18,15 @@

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


Expand Down Expand Up @@ -143,6 +153,8 @@ def __init__(
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:
Expand Down Expand Up @@ -204,10 +216,16 @@ def get_server_capabilities(self) -> types.ServerCapabilities | None:
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)
Expand Down
8 changes: 8 additions & 0 deletions src/mcp/client/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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

Expand All @@ -15,6 +16,10 @@
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,
Expand All @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion src/mcp/server/experimental/request_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ async def work(task: ServerTaskContext) -> CallToolResult:
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)

Expand Down
14 changes: 8 additions & 6 deletions src/mcp/server/experimental/task_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,12 +488,13 @@ async def elicit_as_task(
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,
)
Expand Down Expand Up @@ -594,12 +595,13 @@ async def create_message_as_task(
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,
)
Expand Down
6 changes: 4 additions & 2 deletions src/mcp/server/experimental/task_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ def configure_session(self, session: ServerSession, *, stateless: bool = False)
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":
Expand Down
3 changes: 2 additions & 1 deletion src/mcp/server/lowlevel/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ def enable_tasks(

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.
Expand Down
10 changes: 8 additions & 2 deletions src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ async def main():
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
Expand Down Expand Up @@ -244,10 +244,16 @@ def request_context(
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
Expand Down
20 changes: 16 additions & 4 deletions src/mcp/server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]:
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
Expand Down Expand Up @@ -108,14 +109,25 @@ def client_params(self) -> types.InitializeRequestParams | None:
return self._client_params # pragma: no cover

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

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.
"""
if self._experimental_features is None:
self._experimental_features = ExperimentalServerSessionFeatures(self)
return self._experimental_features
return self._experimental

def check_client_capability(self, capability: types.ClientCapabilities) -> bool: # pragma: no cover
"""Check if the client supports a specific capability."""
Expand Down
10 changes: 9 additions & 1 deletion src/mcp/server/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@
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

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)
Expand Down
18 changes: 18 additions & 0 deletions tests/experimental/tasks/conftest.py
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading