Remove Tasks (SEP-1686) from the SDK (#2714)
This commit is contained in:
parent
a5b2ebb660
commit
8cc187fac0
2 changed files with 63 additions and 12963 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue