"""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 TYPE_CHECKING from mcp.server.experimental.task_scope import task_in_session_scope, task_listable_in_session_scope from mcp.server.experimental.task_support import TaskSupport from mcp.server.lowlevel.func_inspection import create_call_wrapper 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, CancelTaskRequest, CancelTaskResult, ErrorData, GetTaskPayloadRequest, GetTaskPayloadResult, GetTaskRequest, GetTaskResult, ListTasksRequest, ListTasksResult, ServerCapabilities, ServerResult, ServerTasksCapability, ServerTasksRequestsCapability, Task, TasksCallCapability, TasksCancelCapability, TasksListCapability, TasksToolsCapability, ) if TYPE_CHECKING: from mcp.server.lowlevel.server import Server logger = logging.getLogger(__name__) class ExperimentalHandlers: """Experimental request/notification handlers. WARNING: These APIs are experimental and may change without notice. """ def __init__( self, server: Server, request_handlers: dict[type, Callable[..., Awaitable[ServerResult]]], notification_handlers: dict[type, Callable[..., Awaitable[None]]], ): self._server = server self._request_handlers = request_handlers self._notification_handlers = notification_handlers 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( req_type in self._request_handlers for req_type in [GetTaskRequest, ListTasksRequest, CancelTaskRequest, GetTaskPayloadRequest] ): return capabilities.tasks = ServerTasksCapability() if ListTasksRequest in self._request_handlers: capabilities.tasks.list = TasksListCapability() if CancelTaskRequest in self._request_handlers: 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, ) -> TaskSupport: """ Enable experimental task support. This sets up the task infrastructure and auto-registers default handlers for tasks/get, tasks/result, tasks/list, and tasks/cancel. Args: store: Custom TaskStore implementation (defaults to InMemoryTaskStore) queue: Custom TaskMessageQueue implementation (defaults to InMemoryTaskMessageQueue) Returns: The TaskSupport configuration object Example: # Simple in-memory setup server.experimental.enable_tasks() # Custom store/queue for distributed systems 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) # Auto-register default handlers self._register_default_task_handlers() return self._task_support def _requestor_session_scope(self) -> str | None: """Return the task session scope of the session making the current request.""" 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. Task IDs generated by `run_task()` embed the creating session's scope. The default handlers treat a task created by another session exactly like a task that does not exist, so a requestor cannot tell whether such a task exists. Task IDs without an embedded scope are accepted from any session. Raises: McpError: With INVALID_PARAMS if the task belongs to another session. """ if not task_in_session_scope(task_id, self._requestor_session_scope()): raise McpError( ErrorData( code=INVALID_PARAMS, message=f"Task not found: {task_id}", ) ) def _register_default_task_handlers(self) -> None: """Register default handlers for task operations. Each default handler only operates on tasks created by the requesting session (see `_require_task_in_requestor_scope`), and tasks/list only returns the requesting session's own tasks (see `task_listable_in_session_scope`). """ assert self._task_support is not None support = self._task_support # Register get_task handler if not already registered if GetTaskRequest not in self._request_handlers: async def _default_get_task(req: GetTaskRequest) -> ServerResult: self._require_task_in_requestor_scope(req.params.taskId) task = await support.store.get_task(req.params.taskId) if task is None: raise McpError( ErrorData( code=INVALID_PARAMS, message=f"Task not found: {req.params.taskId}", ) ) return ServerResult( GetTaskResult( taskId=task.taskId, status=task.status, statusMessage=task.statusMessage, createdAt=task.createdAt, lastUpdatedAt=task.lastUpdatedAt, ttl=task.ttl, pollInterval=task.pollInterval, ) ) self._request_handlers[GetTaskRequest] = _default_get_task # Register get_task_result handler if not already registered if GetTaskPayloadRequest not in self._request_handlers: async def _default_get_task_result(req: GetTaskPayloadRequest) -> ServerResult: self._require_task_in_requestor_scope(req.params.taskId) ctx = self._server.request_context result = await support.handler.handle(req, ctx.session, ctx.request_id) return ServerResult(result) self._request_handlers[GetTaskPayloadRequest] = _default_get_task_result # Register list_tasks handler if not already registered if ListTasksRequest not in self._request_handlers: async def _default_list_tasks(req: ListTasksRequest) -> ServerResult: requestor_scope = self._requestor_session_scope() if requestor_scope is None: # The server cannot tell this requestor apart from any # other, so there are no tasks it can be shown. return ServerResult(ListTasksResult(tasks=[])) # Return every task that belongs to the requesting session in # a single page. The store's pagination cursor is never sent # to the requestor: it is derived from the unfiltered listing, # so it could identify a task belonging to a different # session. For the same reason the request's cursor is not # forwarded to the store. own_tasks: list[Task] = [] cursor: str | None = None while True: page, cursor = await support.store.list_tasks(cursor) own_tasks.extend( task for task in page if task_listable_in_session_scope(task.taskId, requestor_scope) ) if cursor is None: return ServerResult(ListTasksResult(tasks=own_tasks)) self._request_handlers[ListTasksRequest] = _default_list_tasks # Register cancel_task handler if not already registered if CancelTaskRequest not in self._request_handlers: async def _default_cancel_task(req: CancelTaskRequest) -> ServerResult: self._require_task_in_requestor_scope(req.params.taskId) result = await cancel_task(support.store, req.params.taskId) return ServerResult(result) self._request_handlers[CancelTaskRequest] = _default_cancel_task def list_tasks( self, ) -> Callable[ [Callable[[ListTasksRequest], Awaitable[ListTasksResult]]], Callable[[ListTasksRequest], Awaitable[ListTasksResult]], ]: """Register a handler for listing tasks. WARNING: This API is experimental and may change without notice. """ def decorator( func: Callable[[ListTasksRequest], Awaitable[ListTasksResult]], ) -> Callable[[ListTasksRequest], Awaitable[ListTasksResult]]: logger.debug("Registering handler for ListTasksRequest") wrapper = create_call_wrapper(func, ListTasksRequest) async def handler(req: ListTasksRequest) -> ServerResult: result = await wrapper(req) return ServerResult(result) self._request_handlers[ListTasksRequest] = handler return func return decorator def get_task( self, ) -> Callable[ [Callable[[GetTaskRequest], Awaitable[GetTaskResult]]], Callable[[GetTaskRequest], Awaitable[GetTaskResult]] ]: """Register a handler for getting task status. WARNING: This API is experimental and may change without notice. """ def decorator( func: Callable[[GetTaskRequest], Awaitable[GetTaskResult]], ) -> Callable[[GetTaskRequest], Awaitable[GetTaskResult]]: logger.debug("Registering handler for GetTaskRequest") wrapper = create_call_wrapper(func, GetTaskRequest) async def handler(req: GetTaskRequest) -> ServerResult: result = await wrapper(req) return ServerResult(result) self._request_handlers[GetTaskRequest] = handler return func return decorator def get_task_result( self, ) -> Callable[ [Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]]], Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]], ]: """Register a handler for getting task results/payload. WARNING: This API is experimental and may change without notice. """ def decorator( func: Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]], ) -> Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]]: logger.debug("Registering handler for GetTaskPayloadRequest") wrapper = create_call_wrapper(func, GetTaskPayloadRequest) async def handler(req: GetTaskPayloadRequest) -> ServerResult: result = await wrapper(req) return ServerResult(result) self._request_handlers[GetTaskPayloadRequest] = handler return func return decorator def cancel_task( self, ) -> Callable[ [Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]]], Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]], ]: """Register a handler for cancelling tasks. WARNING: This API is experimental and may change without notice. """ def decorator( func: Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]], ) -> Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]]: logger.debug("Registering handler for CancelTaskRequest") wrapper = create_call_wrapper(func, CancelTaskRequest) async def handler(req: CancelTaskRequest) -> ServerResult: result = await wrapper(req) return ServerResult(result) self._request_handlers[CancelTaskRequest] = handler return func return decorator