Remove Tasks (SEP-1686) from the SDK (#2714)

This commit is contained in:
Max 2026-06-02 17:27:05 +01:00 • committed by GitHub
commit 8cc187fac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 63 additions and 12963 deletions

View file

@ -1,210 +0,0 @@
"""Experimental handlers for the low-level MCP server.
WARNING: These APIs are experimental and may change without notice.
"""
from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from typing import Any, Generic
from typing_extensions import TypeVar
from mcp.server.context import ServerRequestContext
from mcp.server.experimental.task_support import TaskSupport
from mcp.shared.exceptions import MCPError
from mcp.shared.experimental.tasks.helpers import cancel_task
from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore
from mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue
from mcp.shared.experimental.tasks.store import TaskStore
from mcp.types import (
INVALID_PARAMS,
CancelTaskRequestParams,
CancelTaskResult,
GetTaskPayloadRequest,
GetTaskPayloadRequestParams,
GetTaskPayloadResult,
GetTaskRequestParams,
GetTaskResult,
ListTasksResult,
PaginatedRequestParams,
ServerCapabilities,
ServerTasksCapability,
ServerTasksRequestsCapability,
TasksCallCapability,
TasksCancelCapability,
TasksListCapability,
TasksToolsCapability,
)
logger = logging.getLogger(__name__)
LifespanResultT = TypeVar("LifespanResultT", default=Any)
class ExperimentalHandlers(Generic[LifespanResultT]):
"""Experimental request/notification handlers.
WARNING: These APIs are experimental and may change without notice.
"""
def __init__(
self,
add_request_handler: Callable[
[str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]]], None
],
has_handler: Callable[[str], bool],
) -> None:
self._add_request_handler = add_request_handler
self._has_handler = has_handler
self._task_support: TaskSupport | None = None
@property
def task_support(self) -> TaskSupport | None:
"""Get the task support configuration, if enabled."""
return self._task_support
def update_capabilities(self, capabilities: ServerCapabilities) -> None:
# Only add tasks capability if handlers are registered
if not any(self._has_handler(method) for method in ["tasks/get", "tasks/list", "tasks/cancel", "tasks/result"]):
return
capabilities.tasks = ServerTasksCapability()
if self._has_handler("tasks/list"):
capabilities.tasks.list = TasksListCapability()
if self._has_handler("tasks/cancel"):
capabilities.tasks.cancel = TasksCancelCapability()
capabilities.tasks.requests = ServerTasksRequestsCapability(
tools=TasksToolsCapability(call=TasksCallCapability())
) # assuming always supported for now
def enable_tasks(
self,
store: TaskStore | None = None,
queue: TaskMessageQueue | None = None,
*,
on_get_task: Callable[[ServerRequestContext[LifespanResultT], GetTaskRequestParams], Awaitable[GetTaskResult]]
| None = None,
on_task_result: Callable[
[ServerRequestContext[LifespanResultT], GetTaskPayloadRequestParams], Awaitable[GetTaskPayloadResult]
]
| None = None,
on_list_tasks: Callable[
[ServerRequestContext[LifespanResultT], PaginatedRequestParams | None], Awaitable[ListTasksResult]
]
| None = None,
on_cancel_task: Callable[
[ServerRequestContext[LifespanResultT], CancelTaskRequestParams], Awaitable[CancelTaskResult]
]
| None = None,
) -> TaskSupport:
"""Enable experimental task support.
This sets up the task infrastructure and registers handlers for
tasks/get, tasks/result, tasks/list, and tasks/cancel. Custom handlers
can be provided via the on_* kwargs; any not provided will use defaults.
Args:
store: Custom TaskStore implementation (defaults to InMemoryTaskStore)
queue: Custom TaskMessageQueue implementation (defaults to InMemoryTaskMessageQueue)
on_get_task: Custom handler for tasks/get
on_task_result: Custom handler for tasks/result
on_list_tasks: Custom handler for tasks/list
on_cancel_task: Custom handler for tasks/cancel
Returns:
The TaskSupport configuration object
Example:
Simple in-memory setup:
```python
server.experimental.enable_tasks()
```
Custom store/queue for distributed systems:
```python
server.experimental.enable_tasks(
store=RedisTaskStore(redis_url),
queue=RedisTaskMessageQueue(redis_url),
)
```
WARNING: This API is experimental and may change without notice.
"""
if store is None:
store = InMemoryTaskStore()
if queue is None:
queue = InMemoryTaskMessageQueue()
self._task_support = TaskSupport(store=store, queue=queue)
task_support = self._task_support
# Register user-provided handlers
if on_get_task is not None:
self._add_request_handler("tasks/get", on_get_task)
if on_task_result is not None:
self._add_request_handler("tasks/result", on_task_result)
if on_list_tasks is not None:
self._add_request_handler("tasks/list", on_list_tasks)
if on_cancel_task is not None:
self._add_request_handler("tasks/cancel", on_cancel_task)
# Fill in defaults for any not provided
if not self._has_handler("tasks/get"):
async def _default_get_task(
ctx: ServerRequestContext[LifespanResultT], params: GetTaskRequestParams
) -> GetTaskResult:
task = await task_support.store.get_task(params.task_id)
if task is None:
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {params.task_id}")
return GetTaskResult(
task_id=task.task_id,
status=task.status,
status_message=task.status_message,
created_at=task.created_at,
last_updated_at=task.last_updated_at,
ttl=task.ttl,
poll_interval=task.poll_interval,
)
self._add_request_handler("tasks/get", _default_get_task)
if not self._has_handler("tasks/result"):
async def _default_get_task_result(
ctx: ServerRequestContext[LifespanResultT], params: GetTaskPayloadRequestParams
) -> GetTaskPayloadResult:
assert ctx.request_id is not None
req = GetTaskPayloadRequest(params=params)
result = await task_support.handler.handle(req, ctx.session, ctx.request_id)
return result
self._add_request_handler("tasks/result", _default_get_task_result)
if not self._has_handler("tasks/list"):
async def _default_list_tasks(
ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
) -> ListTasksResult:
cursor = params.cursor if params else None
tasks, next_cursor = await task_support.store.list_tasks(cursor)
return ListTasksResult(tasks=tasks, next_cursor=next_cursor)
self._add_request_handler("tasks/list", _default_list_tasks)
if not self._has_handler("tasks/cancel"):
async def _default_cancel_task(
ctx: ServerRequestContext[LifespanResultT], params: CancelTaskRequestParams
) -> CancelTaskResult:
result = await cancel_task(task_support.store, params.task_id)
return result
self._add_request_handler("tasks/cancel", _default_cancel_task)
return task_support

View file

@ -59,8 +59,6 @@ from mcp.server.auth.provider import OAuthAuthorizationServerProvider, TokenVeri
from mcp.server.auth.routes import build_resource_metadata_url, create_auth_routes, create_protected_resource_routes
from mcp.server.auth.settings import AuthSettings
from mcp.server.context import ServerRequestContext
from mcp.server.experimental.request_context import Experimental
from mcp.server.lowlevel.experimental import ExperimentalHandlers
from mcp.server.models import InitializationOptions
from mcp.server.session import ServerSession
from mcp.server.streamable_http import EventStore
@ -121,7 +119,7 @@ class Server(Generic[LifespanResultT]):
| None = None,
on_call_tool: Callable[
[ServerRequestContext[LifespanResultT], types.CallToolRequestParams],
Awaitable[types.CallToolResult | types.CreateTaskResult],
Awaitable[types.CallToolResult],
]
| None = None,
on_list_resources: Callable[
@ -197,7 +195,6 @@ class Server(Generic[LifespanResultT]):
self._notification_handlers: dict[
str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[None]]
] = {}
self._experimental_handlers: ExperimentalHandlers[LifespanResultT] | None = None
self._session_manager: StreamableHTTPSessionManager | None = None
logger.debug("Initializing server %r", name)
@ -242,10 +239,6 @@ class Server(Generic[LifespanResultT]):
"""Add a request handler, silently replacing any existing handler for the same method."""
self._request_handlers[method] = handler
def _has_handler(self, method: str) -> bool:
"""Check if a handler is registered for the given method."""
return method in self._request_handlers or method in self._notification_handlers
# TODO: Rethink capabilities API. Currently capabilities are derived from registered
# handlers but require NotificationOptions to be passed externally for list_changed
# flags, and experimental_capabilities as a separate dict. Consider deriving capabilities
@ -323,25 +316,8 @@ class Server(Generic[LifespanResultT]):
experimental=experimental_capabilities,
completions=completions_capability,
)
if self._experimental_handlers:
self._experimental_handlers.update_capabilities(capabilities)
return capabilities
@property
def experimental(self) -> ExperimentalHandlers[LifespanResultT]:
"""Experimental APIs for tasks and other features.
WARNING: These APIs are experimental and may change without notice.
"""
# We create this inline so we only add these capabilities _if_ they're actually used
if self._experimental_handlers is None:
self._experimental_handlers = ExperimentalHandlers(
add_request_handler=self._add_request_handler,
has_handler=self._has_handler,
)
return self._experimental_handlers
@property
def session_manager(self) -> StreamableHTTPSessionManager:
"""Get the StreamableHTTP session manager.
@ -383,12 +359,6 @@ class Server(Generic[LifespanResultT]):
)
)
# Configure task support for this session if enabled
task_support = self._experimental_handlers.task_support if self._experimental_handlers else None
if task_support is not None:
task_support.configure_session(session)
await stack.enter_async_context(task_support.run())
async with anyio.create_task_group() as tg:
try:
async for message in session.incoming_messages:
@ -476,23 +446,11 @@ class Server(Generic[LifespanResultT]):
close_sse_stream_cb = message.message_metadata.close_sse_stream
close_standalone_sse_stream_cb = message.message_metadata.close_standalone_sse_stream
client_capabilities = session.client_params.capabilities if session.client_params else None
task_support = self._experimental_handlers.task_support if self._experimental_handlers else None
# Get task metadata from request params if present
task_metadata = None
if hasattr(req, "params") and req.params is not None: # pragma: no branch
task_metadata = getattr(req.params, "task", None)
ctx = ServerRequestContext(
request_id=message.request_id,
meta=message.request_meta,
session=session,
lifespan_context=lifespan_context,
experimental=Experimental(
task_metadata=task_metadata,
_client_capabilities=client_capabilities,
_session=session,
_task_support=task_support,
),
request=request_data,
close_sse_stream=close_sse_stream_cb,
close_standalone_sse_stream=close_standalone_sse_stream_cb,
@ -543,17 +501,9 @@ class Server(Generic[LifespanResultT]):
logger.debug("Dispatching notification of type %s", type(notify).__name__)
try:
client_capabilities = session.client_params.capabilities if session.client_params else None
task_support = self._experimental_handlers.task_support if self._experimental_handlers else None
ctx = ServerRequestContext(
session=session,
lifespan_context=lifespan_context,
experimental=Experimental(
task_metadata=None,
_client_capabilities=client_capabilities,
_session=session,
_task_support=task_support,
),
)
await handler(ctx, notify.params)
except Exception: # pragma: no cover