diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx
index e38850ce0..32487890c 100644
--- a/docs/development/v3-notes/v3-features.mdx
+++ b/docs/development/v3-notes/v3-features.mdx
@@ -939,7 +939,7 @@ v3.0 implements MCP SEP-1686 for background task execution via Docket integratio
**Configuration** (`fastmcp_slim/fastmcp/server/tasks/config.py`):
```python
-from fastmcp.server.tasks import TaskConfig
+from fastmcp.utilities.tasks import TaskConfig
@mcp.tool(task=TaskConfig(mode="required"))
async def long_running_task():
diff --git a/docs/more/settings.mdx b/docs/more/settings.mdx
index 92553c3f7..e602f0c0d 100644
--- a/docs/more/settings.mdx
+++ b/docs/more/settings.mdx
@@ -4,7 +4,7 @@ description: Configure FastMCP behavior through environment variables or a .env
icon: gear
---
-FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file (see the [Tasks (Docket)](#tasks-docket) section for a caveat about nested settings in `.env` files).
+FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file.
```bash
# Set via environment
@@ -81,21 +81,7 @@ These control how the server listens when running with an HTTP transport.
## Tasks (Docket)
-These configure the [Docket](https://github.com/prefecthq/docket) task queue used by [server tasks](/servers/tasks). All use the `FASTMCP_DOCKET_` prefix.
-
-
-When setting Docket values in a `.env` file, use a **double** underscore: `FASTMCP_DOCKET__URL` (not `FASTMCP_DOCKET_URL`). This is because `.env` values are resolved through the parent `Settings` class, which uses `__` as its nested delimiter. As regular environment variables (e.g., `export`), the single-underscore form `FASTMCP_DOCKET_URL` works fine.
-
-
-| Environment Variable | Type | Default | Description |
-|---|---|---|---|
-| `FASTMCP_DOCKET_NAME` | `str` | `fastmcp` | Queue name. Servers and workers sharing the same name and backend URL share a task queue. |
-| `FASTMCP_DOCKET_URL` | `str` | `memory://` | Backend URL. Use `memory://` for single-process or `redis://host:port/db` for distributed workers. |
-| `FASTMCP_DOCKET_WORKER_NAME` | `str \| None` | None | Worker name. Auto-generated if unset. |
-| `FASTMCP_DOCKET_CONCURRENCY` | `int` | `10` | Maximum concurrent tasks per worker. |
-| `FASTMCP_DOCKET_REDELIVERY_TIMEOUT` | `timedelta` | `300s` | If a worker doesn't complete a task within this time, it's redelivered to another worker. |
-| `FASTMCP_DOCKET_RECONNECTION_DELAY` | `timedelta` | `5s` | Delay between reconnection attempts when the worker loses its backend connection. |
-| `FASTMCP_DOCKET_MINIMUM_CHECK_INTERVAL` | `timedelta` | `50ms` | How frequently the worker polls for new tasks. Lower values reduce latency at the cost of more CPU usage. |
+Task settings (the `FASTMCP_DOCKET_` variables) moved to the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration.
## Security
diff --git a/docs/servers/dependency-injection.mdx b/docs/servers/dependency-injection.mdx
index 213fd81b7..555cfcb07 100644
--- a/docs/servers/dependency-injection.mdx
+++ b/docs/servers/dependency-injection.mdx
@@ -282,7 +282,8 @@ For background task execution, FastMCP provides dependencies that integrate with
```python
from fastmcp import FastMCP
-from fastmcp.dependencies import CurrentDocket, CurrentWorker, Progress
+from fastmcp.dependencies import Progress
+from fastmcp_tasks.dependencies import CurrentDocket, CurrentWorker
mcp = FastMCP("Task Demo")
diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx
index e04d2495f..bd167c2fc 100644
--- a/docs/servers/tasks.mdx
+++ b/docs/servers/tasks.mdx
@@ -80,7 +80,7 @@ For fine-grained control over task execution behavior, use `TaskConfig` instead
```python
from fastmcp import FastMCP
-from fastmcp.server.tasks import TaskConfig
+from fastmcp.utilities.tasks import TaskConfig
mcp = FastMCP("MyServer")
@@ -113,7 +113,7 @@ When clients poll for task status, the server tells them how frequently to check
```python
from datetime import timedelta
from fastmcp import FastMCP
-from fastmcp.server.tasks import TaskConfig
+from fastmcp.utilities.tasks import TaskConfig
mcp = FastMCP("MyServer")
@@ -241,7 +241,8 @@ FastMCP exposes Docket's full dependency injection system within your task-enabl
```python
from docket import Docket, Worker
from fastmcp import FastMCP
-from fastmcp.dependencies import Progress, CurrentDocket, CurrentWorker
+from fastmcp.dependencies import Progress
+from fastmcp_tasks.dependencies import CurrentDocket, CurrentWorker
mcp = FastMCP("MyServer")
diff --git a/fastmcp_slim/fastmcp/__init__.py b/fastmcp_slim/fastmcp/__init__.py
index 0e1c33bf0..170ffb0f4 100644
--- a/fastmcp_slim/fastmcp/__init__.py
+++ b/fastmcp_slim/fastmcp/__init__.py
@@ -5,14 +5,10 @@ import warnings
from importlib.metadata import PackageNotFoundError, version as _version
from typing import TYPE_CHECKING
-from fastmcp import _install_hints, _sdk_patches
+from fastmcp import _install_hints
from fastmcp.settings import Settings
from fastmcp.utilities.logging import configure_logging as _configure_logging
-# Apply temporary SDK registry patches (SEP-1686 task methods) before any
-# client/server use. See fastmcp._sdk_patches for the upstream-gap rationale.
-_sdk_patches.install()
-
if TYPE_CHECKING:
from fastmcp.client import Client as Client
from fastmcp.apps.app import FastMCPApp as FastMCPApp
diff --git a/fastmcp_slim/fastmcp/_sdk_patches.py b/fastmcp_slim/fastmcp/_sdk_patches.py
deleted file mode 100644
index a77765cfc..000000000
--- a/fastmcp_slim/fastmcp/_sdk_patches.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""Temporary in-place patches for gaps in the pinned MCP SDK.
-
-## SEP-1686 task methods missing from the handshake-era method registries
-
-This shim compensates for a genuine gap in the SDK's *handshake-era*
-(2025-11-25 and earlier) task registry. In the 2025-11-25 SEP-1686 model, tasks
-are a first-class part of the core protocol: `CallToolRequestParams` carries a
-`task: TaskMetadata` field and a task-augmented `tools/call` returns a
-`CreateTaskResult`. `mcp==2.0.0b1` ships those task types (`CreateTaskResult`,
-`GetTaskResult`, `GetTaskPayloadResult`, `ListTasksResult`, `CancelTaskResult`)
-and the `task` request field, but its `mcp_types.methods` registries were never
-wired for them: there are no `tasks/*` rows, and the handshake-era `tools/call`
-result rows are a plain `CallToolResult` with no `CreateTaskResult` arm.
-
-The lowlevel server runner (`mcp.server.runner`) serializes a handler's result
-through `serialize_server_result(method, version, ...)` for any method in
-`SPEC_CLIENT_METHODS`. `tools/call` is such a method, so when a FastMCP tool is
-submitted as a background task (`client.call_tool(..., task=True)`) the handler
-returns a `CreateTaskResult`, which fails validation against the un-widened
-`tools/call` surface row -> the client sees "Handler returned an invalid
-result". The `tasks/*` methods themselves are NOT in `SPEC_CLIENT_METHODS`, so
-their handler results already bypass serialization and reach the wire
-unvalidated; we still register their result rows here for symmetry and so the
-maps are consistent if a future SDK adds them to the spec method set.
-
-## Scope: handshake-era versions only
-
-The widening + `tasks/*` registration is gated to
-`HANDSHAKE_PROTOCOL_VERSIONS` (2025-11-25 and earlier) because those are the
-versions where the 2025 SEP-1686 task model actually applies and where the
-SDK's registry has the genuine gap we compensate for.
-
-The 2026-07-28 protocol is intentionally NOT patched here. Tasks left the core
-protocol in 2026-07-28 and became the separate `io.modelcontextprotocol/tasks`
-extension; `CreateTaskResult` and the `task` field on `CallToolRequestParams`
-do not exist in that schema (a task-augmented `tools/call` was replaced by the
-mutually-recursive `CallToolResult | InputRequiredResult` result). Injecting the
-2025-era `CreateTaskResult` into the 2026 `tools/call` union would assert the
-wrong task model onto that protocol, so we leave its rows untouched.
-
-This module widens the registries IN PLACE (the maps are `MappingProxyType`
-views over private dicts, so we reach the backing dict via `gc.get_referents`
-and mutate it, which the already-bound default-argument references in
-`mcp_types.methods` observe). `install()` is idempotent.
-
-# TODO(sdk-upstream): remove when mcp>=2.0.0bX wires SEP-1686 into the
-# handshake-era method registries.
-"""
-
-from __future__ import annotations
-
-import gc
-from types import MappingProxyType, UnionType
-
-import mcp_types
-from mcp_types import methods as _methods
-from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS
-
-# Result type for each task method, keyed by the client request method name.
-_TASK_RESULT_TYPES: dict[str, type] = {
- "tasks/get": mcp_types.GetTaskResult,
- "tasks/result": mcp_types.GetTaskPayloadResult,
- "tasks/list": mcp_types.ListTasksResult,
- "tasks/cancel": mcp_types.CancelTaskResult,
-}
-
-_installed = False
-
-
-def _backing_dict(proxy: object) -> dict:
- """Return the mutable dict a MappingProxyType wraps.
-
- The `mcp_types.methods` surface maps are `MappingProxyType` views; their
- sole dict referent is the backing store the module's functions read through
- their default `surface=` arguments.
- """
- referents = [r for r in gc.get_referents(proxy) if isinstance(r, dict)]
- if len(referents) != 1:
- raise RuntimeError(
- "expected exactly one backing dict for the method registry proxy, "
- f"found {len(referents)}"
- )
- return referents[0]
-
-
-def install() -> None:
- """Widen the SDK's server-result registry for SEP-1686 task methods.
-
- Idempotent. Safe to call at import time before any client/server use.
- """
- global _installed
- if _installed:
- return
-
- if not isinstance(_methods.SERVER_RESULTS, MappingProxyType):
- # Registry shape changed upstream; the shim no longer applies.
- _installed = True
- return
-
- server_results = _backing_dict(_methods.SERVER_RESULTS)
-
- # Gate to handshake-era versions only: the 2025 SEP-1686 task model applies
- # there, and 2026-07-28 tasks are the separate io.modelcontextprotocol/tasks
- # extension (see module docstring) — its rows must stay untouched.
- versions_with_tools_call = {
- version
- for (method, version) in server_results
- if method == "tools/call" and version in HANDSHAKE_PROTOCOL_VERSIONS
- }
-
- for version in versions_with_tools_call:
- # (a) widen tools/call so a CreateTaskResult validates (task submission).
- existing = server_results[("tools/call", version)]
- arms = get_union_arms(existing)
- if mcp_types.CreateTaskResult not in arms:
- server_results[("tools/call", version)] = (
- existing | mcp_types.CreateTaskResult
- )
-
- # (b) register the tasks/* result rows for the same versions.
- for method, result_type in _TASK_RESULT_TYPES.items():
- server_results.setdefault((method, version), result_type)
-
- _installed = True
-
-
-def get_union_arms(row: type | UnionType) -> tuple[type, ...]:
- """Return the member types of a result row, whether a single type or union."""
- if isinstance(row, UnionType):
- return tuple(row.__args__)
- return (row,)
diff --git a/fastmcp_slim/fastmcp/cli/cli.py b/fastmcp_slim/fastmcp/cli/cli.py
index fe7872a57..5513e3119 100644
--- a/fastmcp_slim/fastmcp/cli/cli.py
+++ b/fastmcp_slim/fastmcp/cli/cli.py
@@ -23,7 +23,6 @@ from fastmcp.cli.auth import auth_app
from fastmcp.cli.client import call_command, discover_command, list_command
from fastmcp.cli.generate import generate_cli_command
from fastmcp.cli.install import install_app
-from fastmcp.cli.tasks import tasks_app
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
from fastmcp.utilities.inspect import (
InspectFormat,
@@ -1126,9 +1125,6 @@ app.command(project_app)
# Add install subcommands using proper Cyclopts pattern
app.command(install_app)
-# Add tasks subcommand group
-app.command(tasks_app)
-
# Add client query commands
app.command(list_command, name="list")
app.command(call_command, name="call")
diff --git a/fastmcp_slim/fastmcp/client/client.py b/fastmcp_slim/fastmcp/client/client.py
index c62429f29..74cb76055 100644
--- a/fastmcp_slim/fastmcp/client/client.py
+++ b/fastmcp_slim/fastmcp/client/client.py
@@ -7,7 +7,6 @@ import hashlib
import secrets
import ssl
import uuid
-import weakref
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping, Sequence
from contextlib import AsyncExitStack, asynccontextmanager, suppress
from dataclasses import dataclass, field
@@ -43,11 +42,6 @@ from mcp.client.extension import (
ResultClaim,
)
from mcp.client.session import ClientRequestContext, MessageHandlerFnT
-from mcp_types import (
- GetTaskResult,
- TaskStatusNotification,
- TaskStatusNotificationParams,
-)
from mcp_types.methods import validate_server_result
from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS, MODERN_PROTOCOL_VERSIONS
from pydantic import AnyUrl, ValidationError
@@ -67,7 +61,6 @@ from fastmcp.client.messages import MessageHandler, MessageHandlerT
from fastmcp.client.mixins import (
ClientPromptsMixin,
ClientResourcesMixin,
- ClientTaskManagementMixin,
ClientToolsMixin,
)
from fastmcp.client.progress import ProgressHandler, default_progress_handler
@@ -80,12 +73,6 @@ from fastmcp.client.sampling import (
SamplingHandler,
create_sampling_callback,
)
-from fastmcp.client.tasks import (
- PromptTask,
- ResourceTask,
- TaskNotificationHandler,
- ToolTask,
-)
from fastmcp.mcp_config import MCPConfig
from fastmcp.utilities.exceptions import get_catch_handlers
from fastmcp.utilities.logging import get_logger
@@ -256,7 +243,6 @@ class Client(
ClientResourcesMixin,
ClientPromptsMixin,
ClientToolsMixin,
- ClientTaskManagementMixin,
):
"""
MCP client that delegates connection management to a Transport instance.
@@ -500,12 +486,10 @@ class Client(
cache
)
- # The unwrapped base handler (default routes task notifications; a user
- # handler is preserved as-is). Retained so `new()` can rebuild the clone's
- # handler without unwrapping the cache-eviction wrapper below.
- self._base_message_handler: MessageHandlerFnT | None = (
- message_handler or TaskNotificationHandler(self)
- )
+ # The unwrapped base handler (a user handler is preserved as-is).
+ # Retained so `new()` can rebuild the clone's handler without unwrapping
+ # the cache-eviction wrapper below.
+ self._base_message_handler: MessageHandlerFnT | None = message_handler
effective_message_handler = self._base_message_handler
if self._response_cache is not None:
effective_message_handler = _evicting_message_handler(
@@ -557,15 +541,6 @@ class Client(
self._session_state = ClientSessionState()
self._transport_options: TransportOptions | None = None
- # Track task IDs submitted by this client (for list_tasks support)
- self._submitted_task_ids: set[str] = set()
-
- # Registry for routing notifications/tasks/status to Task objects
-
- self._task_registry: dict[
- str, weakref.ref[ToolTask | PromptTask | ResourceTask]
- ] = {}
-
def _build_response_cache(
self, cache: CacheConfig | bool | None
) -> ClientResponseCache | None:
@@ -724,26 +699,16 @@ class Client(
new_client._session_state = ClientSessionState()
new_client._transport_options = self._transport_options
- # Reset mutable task tracking state so new client is independent
- new_client._task_registry = {}
- new_client._submitted_task_ids = set()
-
# Give the clone its own response cache so cached entries are not shared
# across independent sessions, and rebuild the negotiated_version closure
# to point at the clone's session state.
new_client._response_cache = new_client._build_response_cache(self._cache_arg)
# Create a fresh session kwargs dict so the clone doesn't share
- # the original's mutable dict. Rebind the task notification handler
- # to the new client if the default handler is in use; preserve any
- # custom message handler the user may have set.
+ # the original's mutable dict; preserve any custom message handler the
+ # user may have set, re-wrapping with the clone's own cache if one exists.
new_client._session_kwargs = {**self._session_kwargs} # type: ignore[typeddict-item]
- # Recover the unwrapped base handler (never the cache-evicting wrapper): a
- # default (TaskNotificationHandler) rebinds to the clone; a user handler is
- # preserved. Then re-wrap with the clone's own cache if one exists.
base_handler: MessageHandlerFnT | None = self._base_message_handler
- if isinstance(base_handler, TaskNotificationHandler) or base_handler is None:
- base_handler = TaskNotificationHandler(new_client)
new_client._base_message_handler = base_handler
if new_client._response_cache is not None:
new_client._session_kwargs["message_handler"] = _evicting_message_handler(
@@ -752,8 +717,7 @@ class Client(
else:
new_client._session_kwargs["message_handler"] = base_handler
# Rebuild the extension-contributed kwargs (capability ad, result claims,
- # notification bindings) so the clone's task-status binding routes to the
- # clone while user extensions still compose with it.
+ # notification bindings) so user extensions compose on the clone.
new_client._session_kwargs.update(new_client._build_extension_kwargs())
new_client.name += f":{secrets.token_hex(2)}"
@@ -1217,41 +1181,12 @@ class Client(
max_rounds=self.input_required_max_rounds,
)
- def _handle_task_status_notification(
- self, notification: TaskStatusNotification
- ) -> None:
- """Route task status notification to appropriate Task object.
-
- Called when notifications/tasks/status is received from server.
- Updates Task object's cache and triggers events/callbacks.
- """
- self._handle_task_status_params(notification.params)
-
- def _handle_task_status_params(self, params: TaskStatusNotificationParams) -> None:
- """Route task status notification params to the matching Task object."""
- task_id = params.task_id
- if not task_id:
- return
-
- # Look up task in registry (weakref)
- task_ref = self._task_registry.get(task_id)
- if task_ref:
- task = task_ref() # Dereference weakref
- if task:
- # Convert notification params to GetTaskResult (they share the same fields via Task)
- status = GetTaskResult.model_validate(params.model_dump())
- task._handle_status_notification(status)
-
def _build_extension_kwargs(self) -> SessionKwargs:
"""Session kwargs contributed by `extensions=` / `result_claims=`.
Folds the user's `ClientExtension` instances into the capability ad, result
claims, and notification bindings the SDK `ClientSession` consumes, then
- merges in any explicitly-passed `result_claims`. The internal task-status
- binding is always prepended to the folded bindings so user extensions
- *compose* with it rather than clobbering it; a user extension that binds the
- same `notifications/tasks/status` method surfaces a duplicate-method error
- from the SDK rather than silently replacing FastMCP's routing.
+ merges in any explicitly-passed `result_claims`.
Also rebuilds `self._claim_by_model`, the model→claim index the resolution
path uses to finish a claimed `tools/call` result, covering both the folded
@@ -1269,11 +1204,7 @@ class Client(
self._claim_by_model = by_model
kwargs: SessionKwargs = {
- # The internal task binding must lead so user bindings extend it.
- "notification_bindings": [
- self._task_status_binding(),
- *(folded.bindings or ()),
- ],
+ "notification_bindings": [*(folded.bindings or ())],
}
if folded.ad:
kwargs["extensions"] = folded.ad
@@ -1309,26 +1240,6 @@ class Client(
await self.session.validate_tool_result(name, final)
return final
- def _task_status_binding(self) -> NotificationBinding[TaskStatusNotificationParams]:
- """Build a binding routing `notifications/tasks/status` to Task objects.
-
- SDK v2 drops notifications whose method is absent from the negotiated
- version's core tables before they reach the message_handler; a binding is
- the supported channel for observing such vendor notifications.
- """
- client_ref = weakref.ref(self)
-
- async def _handler(params: TaskStatusNotificationParams) -> None:
- client = client_ref()
- if client is not None:
- client._handle_task_status_params(params)
-
- return NotificationBinding(
- method="notifications/tasks/status",
- params_type=TaskStatusNotificationParams,
- handler=_handler,
- )
-
async def close(self):
await self._disconnect(force=True)
await self.transport.close()
diff --git a/fastmcp_slim/fastmcp/client/mixins/__init__.py b/fastmcp_slim/fastmcp/client/mixins/__init__.py
index 323e20991..f0c8ff85e 100644
--- a/fastmcp_slim/fastmcp/client/mixins/__init__.py
+++ b/fastmcp_slim/fastmcp/client/mixins/__init__.py
@@ -2,12 +2,10 @@
from fastmcp.client.mixins.prompts import ClientPromptsMixin
from fastmcp.client.mixins.resources import ClientResourcesMixin
-from fastmcp.client.mixins.task_management import ClientTaskManagementMixin
from fastmcp.client.mixins.tools import ClientToolsMixin
__all__ = [
"ClientPromptsMixin",
"ClientResourcesMixin",
- "ClientTaskManagementMixin",
"ClientToolsMixin",
]
diff --git a/fastmcp_slim/fastmcp/client/mixins/prompts.py b/fastmcp_slim/fastmcp/client/mixins/prompts.py
index fba8ed3fe..df38292d0 100644
--- a/fastmcp_slim/fastmcp/client/mixins/prompts.py
+++ b/fastmcp_slim/fastmcp/client/mixins/prompts.py
@@ -2,19 +2,15 @@
from __future__ import annotations
-import uuid
-import weakref
-from typing import TYPE_CHECKING, Any, Literal, cast, overload
+from typing import TYPE_CHECKING, Any, cast
import mcp_types
import pydantic_core
from mcp.client.caching import CacheMode
-from pydantic import RootModel
if TYPE_CHECKING:
from fastmcp.client.client import Client
-from fastmcp.client.tasks import PromptTask
from fastmcp.client.telemetry import client_span
from fastmcp.telemetry import inject_trace_context
from fastmcp.utilities.logging import get_logger
@@ -23,11 +19,6 @@ logger = get_logger(__name__)
AUTO_PAGINATION_MAX_PAGES = 250
-# Type alias for task response union (SEP-1686 graceful degradation)
-PromptTaskResponseUnion = RootModel[
- mcp_types.CreateTaskResult | mcp_types.GetPromptResult
-]
-
class ClientPromptsMixin:
"""Mixin providing prompt-related methods for Client."""
@@ -192,7 +183,6 @@ class ClientPromptsMixin:
)
return result
- @overload
async def get_prompt(
self: Client,
name: str,
@@ -200,33 +190,7 @@ class ClientPromptsMixin:
*,
version: str | None = None,
meta: dict[str, Any] | None = None,
- task: Literal[False] = False,
- ) -> mcp_types.GetPromptResult: ...
-
- @overload
- async def get_prompt(
- self: Client,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: str | None = None,
- meta: dict[str, Any] | None = None,
- task: Literal[True],
- task_id: str | None = None,
- ttl: int = 60000,
- ) -> PromptTask: ...
-
- async def get_prompt(
- self: Client,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: str | None = None,
- meta: dict[str, Any] | None = None,
- task: bool = False,
- task_id: str | None = None,
- ttl: int = 60000,
- ) -> mcp_types.GetPromptResult | PromptTask:
+ ) -> mcp_types.GetPromptResult:
"""Retrieve a rendered prompt message list from the server.
Args:
@@ -234,13 +198,9 @@ class ClientPromptsMixin:
arguments (dict[str, Any] | None, optional): Arguments to pass to the prompt. Defaults to None.
version (str | None, optional): Specific prompt version to get. If None, gets highest version.
meta (dict[str, Any] | None): Optional request-level metadata.
- task (bool): If True, execute as background task (SEP-1686). Defaults to False.
- task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
- ttl (int): Time to keep results available in milliseconds (default 60s).
Returns:
- mcp_types.GetPromptResult | PromptTask: The complete response object if task=False,
- or a PromptTask object if task=True.
+ mcp_types.GetPromptResult: The complete response object.
Raises:
RuntimeError: If called while the client is not connected.
@@ -254,94 +214,7 @@ class ClientPromptsMixin:
"version": version,
}
- if task:
- return await self._get_prompt_as_task(
- name, arguments, task_id, ttl, meta=request_meta or None
- )
-
result = await self.get_prompt_mcp(
name=name, arguments=arguments, meta=request_meta or None
)
return result
-
- async def _get_prompt_as_task(
- self: Client,
- name: str,
- arguments: dict[str, Any] | None = None,
- task_id: str | None = None,
- ttl: int = 60000,
- meta: dict[str, Any] | None = None,
- ) -> PromptTask:
- """Get a prompt for background execution (SEP-1686).
-
- Returns a PromptTask object that handles both background and immediate execution.
-
- Args:
- name: Prompt name to get
- arguments: Prompt arguments
- task_id: Optional client-provided task ID (ignored, for backward compatibility)
- ttl: Time to keep results available in milliseconds (default 60s)
- meta: Optional request metadata (e.g., version info)
-
- Returns:
- PromptTask: Future-like object for accessing task status and results
- """
- # Per SEP-1686 final spec: client sends only ttl, server generates taskId
- # Inject trace context into meta for propagation to server.
- # SDK v2: request `_meta` is `RequestParamsMeta` (a TypedDict), not
- # the old `RequestParams.Meta` nested model.
- propagated_meta = inject_trace_context(meta)
- request_meta = cast(
- "mcp_types.RequestParamsMeta | None",
- propagated_meta if propagated_meta else None,
- )
-
- # Serialize arguments for MCP protocol
- serialized_arguments: dict[str, str] | None = None
- if arguments:
- serialized_arguments = {}
- for key, value in arguments.items():
- if isinstance(value, str):
- serialized_arguments[key] = value
- else:
- serialized_arguments[key] = pydantic_core.to_json(value).decode(
- "utf-8"
- )
-
- # SDK v2: GetPromptRequestParams has no `task` field, so this request
- # cannot carry task metadata over the wire and the server graceful-
- # degrades to immediate execution (sdk-feedback #3). `ttl` is retained on
- # the public API but has no wire representation here.
- request = mcp_types.GetPromptRequest(
- params=mcp_types.GetPromptRequestParams(
- name=name,
- arguments=serialized_arguments,
- _meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
- )
- )
-
- # Server returns CreateTaskResult (task accepted) or GetPromptResult (graceful degradation)
- wrapped_result = await self._await_with_session_monitoring(
- self.session.send_request(
- request=request, # type: ignore[arg-type]
- result_type=PromptTaskResponseUnion,
- )
- )
- raw_result = wrapped_result.root
-
- if isinstance(raw_result, mcp_types.CreateTaskResult):
- # Task was accepted - extract task info from CreateTaskResult
- server_task_id = raw_result.task.task_id
- self._submitted_task_ids.add(server_task_id)
-
- task_obj = PromptTask(
- self, server_task_id, prompt_name=name, immediate_result=None
- )
- self._task_registry[server_task_id] = weakref.ref(task_obj)
- return task_obj
- else:
- # Graceful degradation - server returned GetPromptResult
- synthetic_task_id = task_id or str(uuid.uuid4())
- return PromptTask(
- self, synthetic_task_id, prompt_name=name, immediate_result=raw_result
- )
diff --git a/fastmcp_slim/fastmcp/client/mixins/resources.py b/fastmcp_slim/fastmcp/client/mixins/resources.py
index 7bbbd84ed..c480b0687 100644
--- a/fastmcp_slim/fastmcp/client/mixins/resources.py
+++ b/fastmcp_slim/fastmcp/client/mixins/resources.py
@@ -2,18 +2,15 @@
from __future__ import annotations
-import uuid
-import weakref
-from typing import TYPE_CHECKING, Any, Literal, cast, overload
+from typing import TYPE_CHECKING, Any, cast
import mcp_types
from mcp.client.caching import CacheMode
-from pydantic import AnyUrl, RootModel
+from pydantic import AnyUrl
if TYPE_CHECKING:
from fastmcp.client.client import Client
-from fastmcp.client.tasks import ResourceTask
from fastmcp.client.telemetry import client_span
from fastmcp.telemetry import inject_trace_context
from fastmcp.utilities.logging import get_logger
@@ -22,11 +19,6 @@ logger = get_logger(__name__)
AUTO_PAGINATION_MAX_PAGES = 250
-# Type alias for task response union (SEP-1686 graceful degradation)
-ResourceTaskResponseUnion = RootModel[
- mcp_types.CreateTaskResult | mcp_types.ReadResourceResult
-]
-
class ClientResourcesMixin:
"""Mixin providing resource-related methods for Client."""
@@ -272,54 +264,23 @@ class ClientResourcesMixin:
)
return result
- @overload
async def read_resource(
self: Client,
uri: AnyUrl | str,
*,
version: str | None = None,
meta: dict[str, Any] | None = None,
- task: Literal[False] = False,
- ) -> list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents]: ...
-
- @overload
- async def read_resource(
- self: Client,
- uri: AnyUrl | str,
- *,
- version: str | None = None,
- meta: dict[str, Any] | None = None,
- task: Literal[True],
- task_id: str | None = None,
- ttl: int = 60000,
- ) -> ResourceTask: ...
-
- async def read_resource(
- self: Client,
- uri: AnyUrl | str,
- *,
- version: str | None = None,
- meta: dict[str, Any] | None = None,
- task: bool = False,
- task_id: str | None = None,
- ttl: int = 60000,
- ) -> (
- list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents]
- | ResourceTask
- ):
+ ) -> list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents]:
"""Read the contents of a resource or resolved template.
Args:
uri (AnyUrl | str): The URI of the resource to read. Can be a string or an AnyUrl object.
version (str | None): Specific version to read. If None, reads highest version.
meta (dict[str, Any] | None): Optional request-level metadata.
- task (bool): If True, execute as background task (SEP-1686). Defaults to False.
- task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
- ttl (int): Time to keep results available in milliseconds (default 60s).
Returns:
- list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents] | ResourceTask:
- A list of content objects if task=False, or a ResourceTask object if task=True.
+ list[mcp_types.TextResourceContents | mcp_types.BlobResourceContents]:
+ A list of content objects.
Raises:
RuntimeError: If called while the client is not connected.
@@ -333,11 +294,6 @@ class ClientResourcesMixin:
"version": version,
}
- if task:
- return await self._read_resource_as_task(
- uri, task_id, ttl, meta=request_meta or None
- )
-
if isinstance(uri, str):
try:
uri = AnyUrl(uri) # Ensure AnyUrl
@@ -347,77 +303,3 @@ class ClientResourcesMixin:
) from e
result = await self.read_resource_mcp(uri, meta=request_meta or None)
return result.contents
-
- async def _read_resource_as_task(
- self: Client,
- uri: AnyUrl | str,
- task_id: str | None = None,
- ttl: int = 60000,
- meta: dict[str, Any] | None = None,
- ) -> ResourceTask:
- """Read a resource for background execution (SEP-1686).
-
- Returns a ResourceTask object that handles both background and immediate execution.
-
- Args:
- uri: Resource URI to read
- task_id: Optional client-provided task ID (ignored, for backward compatibility)
- ttl: Time to keep results available in milliseconds (default 60s)
- meta: Optional metadata to pass with the request (e.g., version info)
-
- Returns:
- ResourceTask: Future-like object for accessing task status and results
- """
- # Per SEP-1686 final spec: client sends only ttl, server generates taskId
- # Inject trace context into meta for propagation to server.
- # SDK v2: request `_meta` is `RequestParamsMeta` (a TypedDict), not
- # the old `RequestParams.Meta` nested model.
- propagated_meta = inject_trace_context(meta)
- request_meta = cast(
- "mcp_types.RequestParamsMeta | None",
- propagated_meta if propagated_meta else None,
- )
-
- # SDK v2: ReadResourceRequestParams.uri is a plain string, but resources
- # are stored under the AnyUrl-normalized form, so normalize to match.
- uri_str = str(AnyUrl(uri)) if isinstance(uri, str) else str(uri)
-
- # SDK v2: ReadResourceRequestParams has no `task` field, so this request
- # cannot carry task metadata over the wire and the server graceful-
- # degrades to immediate execution (sdk-feedback #3). `ttl` is retained on
- # the public API but has no wire representation here.
- request = mcp_types.ReadResourceRequest(
- params=mcp_types.ReadResourceRequestParams(
- uri=uri_str,
- _meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
- )
- )
-
- # Server returns CreateTaskResult (task accepted) or ReadResourceResult (graceful degradation)
- wrapped_result = await self._await_with_session_monitoring(
- self.session.send_request(
- request=request, # type: ignore[arg-type]
- result_type=ResourceTaskResponseUnion,
- )
- )
- raw_result = wrapped_result.root
-
- if isinstance(raw_result, mcp_types.CreateTaskResult):
- # Task was accepted - extract task info from CreateTaskResult
- server_task_id = raw_result.task.task_id
- self._submitted_task_ids.add(server_task_id)
-
- task_obj = ResourceTask(
- self, server_task_id, uri=str(uri), immediate_result=None
- )
- self._task_registry[server_task_id] = weakref.ref(task_obj)
- return task_obj
- else:
- # Graceful degradation - server returned ReadResourceResult
- synthetic_task_id = task_id or str(uuid.uuid4())
- return ResourceTask(
- self,
- synthetic_task_id,
- uri=str(uri),
- immediate_result=raw_result.contents,
- )
diff --git a/fastmcp_slim/fastmcp/client/mixins/tools.py b/fastmcp_slim/fastmcp/client/mixins/tools.py
index d52e90cb9..40db1f21c 100644
--- a/fastmcp_slim/fastmcp/client/mixins/tools.py
+++ b/fastmcp_slim/fastmcp/client/mixins/tools.py
@@ -2,21 +2,17 @@
from __future__ import annotations
-import uuid
-import weakref
-from typing import TYPE_CHECKING, Any, Literal, cast, overload
+from typing import TYPE_CHECKING, Any, cast
import mcp_types
from mcp.client.caching import CacheMode
from opentelemetry.trace import Status, StatusCode
-from pydantic import RootModel
if TYPE_CHECKING:
import datetime
from fastmcp.client.client import CallToolResult, Client
from fastmcp.client.progress import ProgressHandler
-from fastmcp.client.tasks import ToolTask
from fastmcp.client.telemetry import client_span
from fastmcp.exceptions import ToolError
from fastmcp.telemetry import inject_trace_context
@@ -29,9 +25,6 @@ logger = get_logger(__name__)
AUTO_PAGINATION_MAX_PAGES = 250
-# Type alias for task response union (SEP-1686 graceful degradation)
-ToolTaskResponseUnion = RootModel[mcp_types.CreateTaskResult | mcp_types.CallToolResult]
-
class ClientToolsMixin:
"""Mixin providing tool-related methods for Client."""
@@ -278,7 +271,6 @@ class ClientToolsMixin:
raise_on_error=raise_on_error,
)
- @overload
async def call_tool(
self: Client,
name: str,
@@ -289,39 +281,7 @@ class ClientToolsMixin:
progress_handler: ProgressHandler | None = None,
raise_on_error: bool = True,
meta: dict[str, Any] | None = None,
- task: Literal[False] = False,
- ) -> CallToolResult: ...
-
- @overload
- async def call_tool(
- self: Client,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: str | None = None,
- timeout: datetime.timedelta | float | int | None = None,
- progress_handler: ProgressHandler | None = None,
- raise_on_error: bool = True,
- meta: dict[str, Any] | None = None,
- task: Literal[True],
- task_id: str | None = None,
- ttl: int = 60000,
- ) -> ToolTask: ...
-
- async def call_tool(
- self: Client,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: str | None = None,
- timeout: datetime.timedelta | float | int | None = None,
- progress_handler: ProgressHandler | None = None,
- raise_on_error: bool = True,
- meta: dict[str, Any] | None = None,
- task: bool = False,
- task_id: str | None = None,
- ttl: int = 60000,
- ) -> CallToolResult | ToolTask:
+ ) -> CallToolResult:
"""Call a tool on the server.
Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
@@ -337,15 +297,11 @@ class ClientToolsMixin:
This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
that shouldn't be tool arguments but may influence server-side processing. The server
can access this via `context.request_context.meta`. Defaults to None.
- task (bool): If True, execute as background task (SEP-1686). Defaults to False.
- task_id (str | None): Optional client-provided task ID (auto-generated if not provided).
- ttl (int): Time to keep results available in milliseconds (default 60s).
Returns:
- CallToolResult | ToolTask: The content returned by the tool if task=False,
- or a ToolTask object if task=True. If the tool returns structured
- outputs, they are returned as a dataclass (if an output schema
- is available) or a dictionary; otherwise, a list of content
+ CallToolResult: The content returned by the tool. If the tool returns
+ structured outputs, they are returned as a dataclass (if an output
+ schema is available) or a dictionary; otherwise, a list of content
blocks is returned. Note: to receive both structured and
unstructured outputs, use call_tool_mcp instead and access the
raw result object.
@@ -363,16 +319,6 @@ class ClientToolsMixin:
"version": version,
}
- if task:
- return await self._call_tool_as_task(
- name,
- arguments,
- task_id,
- ttl,
- raise_on_error=raise_on_error,
- meta=request_meta or None,
- )
-
result = await self.call_tool_mcp(
name=name,
arguments=arguments or {},
@@ -384,85 +330,6 @@ class ClientToolsMixin:
name, result, raise_on_error=raise_on_error
)
- async def _call_tool_as_task(
- self: Client,
- name: str,
- arguments: dict[str, Any] | None = None,
- task_id: str | None = None,
- ttl: int = 60000,
- raise_on_error: bool = True,
- meta: dict[str, Any] | None = None,
- ) -> ToolTask:
- """Call a tool for background execution (SEP-1686).
-
- Returns a ToolTask object that handles both background and immediate execution.
- If the server accepts background execution, ToolTask will poll for results.
- If the server declines (graceful degradation), ToolTask wraps the immediate result.
-
- Args:
- name: Tool name to call
- arguments: Tool arguments
- task_id: Optional client-provided task ID (ignored, for backward compatibility)
- ttl: Time to keep results available in milliseconds (default 60s)
- raise_on_error: Whether task.result() should raise ToolError on errors
- meta: Optional request metadata (e.g., version info)
-
- Returns:
- ToolTask: Future-like object for accessing task status and results
- """
- # Per SEP-1686 final spec: client sends only ttl, server generates taskId
- # Inject trace context into meta for propagation to server
- propagated_meta = inject_trace_context(meta)
- # SDK v2: request `_meta` is `RequestParamsMeta` (a TypedDict), not the
- # old `RequestParams.Meta` nested model.
- request_meta = cast(mcp_types.RequestParamsMeta | None, propagated_meta)
-
- # Build request with task metadata
- request = mcp_types.CallToolRequest(
- params=mcp_types.CallToolRequestParams(
- name=name,
- arguments=arguments or {},
- task=mcp_types.TaskMetadata(ttl=ttl),
- _meta=request_meta, # type: ignore[unknown-argument] # pydantic alias
- )
- )
-
- # Server returns CreateTaskResult (task accepted) or CallToolResult (graceful degradation)
- # Use RootModel with Union to handle both response types (SDK calls model_validate)
- wrapped_result = await self._await_with_session_monitoring(
- self.session.send_request(
- request=request, # type: ignore[arg-type]
- result_type=ToolTaskResponseUnion,
- )
- )
- raw_result = wrapped_result.root
-
- if isinstance(raw_result, mcp_types.CreateTaskResult):
- # Task was accepted - extract task info from CreateTaskResult
- server_task_id = raw_result.task.task_id
- self._submitted_task_ids.add(server_task_id)
-
- task_obj = ToolTask(
- self,
- server_task_id,
- tool_name=name,
- immediate_result=None,
- raise_on_error=raise_on_error,
- )
- self._task_registry[server_task_id] = weakref.ref(task_obj)
- return task_obj
- else:
- # Graceful degradation - server returned CallToolResult
- parsed_result = await self._parse_call_tool_result(name, raw_result)
- synthetic_task_id = task_id or str(uuid.uuid4())
- return ToolTask(
- self,
- synthetic_task_id,
- tool_name=name,
- immediate_result=parsed_result,
- raise_on_error=raise_on_error,
- )
-
async def _parse_call_tool_result(
name: str,
diff --git a/fastmcp_slim/fastmcp/decorators.py b/fastmcp_slim/fastmcp/decorators.py
index 75dff25ac..b61a90c62 100644
--- a/fastmcp_slim/fastmcp/decorators.py
+++ b/fastmcp_slim/fastmcp/decorators.py
@@ -8,8 +8,8 @@ from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
if TYPE_CHECKING:
from fastmcp.prompts.function_prompt import PromptMeta
from fastmcp.resources.function_resource import ResourceMeta
- from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.function_tool import ToolMeta
+ from fastmcp.utilities.tasks import TaskConfig
FastMCPMeta = ToolMeta | ResourceMeta | PromptMeta
diff --git a/fastmcp_slim/fastmcp/dependencies.py b/fastmcp_slim/fastmcp/dependencies.py
index 2aa8c145a..138486f88 100644
--- a/fastmcp_slim/fastmcp/dependencies.py
+++ b/fastmcp_slim/fastmcp/dependencies.py
@@ -4,20 +4,21 @@ This module re-exports dependency injection symbols to provide a clean,
centralized import location for all dependency-related functionality.
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
-using the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,
-CurrentWorker) and background task execution require fastmcp[tasks].
+using the uncalled-for DI engine. The docket-specific dependencies
+(``CurrentDocket``, ``CurrentWorker``) live in the ``fastmcp-tasks`` package
+(``fastmcp_tasks.dependencies``).
"""
+from typing import Any
+
from uncalled_for import Dependency, Depends, Shared
from fastmcp.server.dependencies import (
CurrentAccessToken,
CurrentContext,
- CurrentDocket,
CurrentFastMCP,
CurrentHeaders,
CurrentRequest,
- CurrentWorker,
Progress,
ProgressLike,
TokenClaim,
@@ -26,11 +27,9 @@ from fastmcp.server.dependencies import (
__all__ = [
"CurrentAccessToken",
"CurrentContext",
- "CurrentDocket",
"CurrentFastMCP",
"CurrentHeaders",
"CurrentRequest",
- "CurrentWorker",
"Dependency",
"Depends",
"Progress",
@@ -38,3 +37,17 @@ __all__ = [
"Shared",
"TokenClaim",
]
+
+# Docket-specific dependencies moved to the fastmcp-tasks package. Point users
+# there instead of raising a bare AttributeError.
+_MOVED_TO_TASKS = {"CurrentDocket", "CurrentWorker"}
+
+
+def __getattr__(name: str) -> Any:
+ if name in _MOVED_TO_TASKS:
+ raise ImportError(
+ f"{name!r} moved to the fastmcp-tasks package. Install it with "
+ f"`pip install 'fastmcp[tasks]'` and import from "
+ f"`fastmcp_tasks.dependencies`."
+ )
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/fastmcp_slim/fastmcp/prompts/base.py b/fastmcp_slim/fastmcp/prompts/base.py
index f56243e57..d23ab0e0f 100644
--- a/fastmcp_slim/fastmcp/prompts/base.py
+++ b/fastmcp_slim/fastmcp/prompts/base.py
@@ -3,17 +3,13 @@
from __future__ import annotations as _annotations
from collections.abc import Callable
-from typing import TYPE_CHECKING, Any, ClassVar, Literal, overload
+from typing import TYPE_CHECKING, Any, ClassVar, Literal
import pydantic
import pydantic_core
if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
-
from fastmcp.prompts.function_prompt import FunctionPrompt
-import mcp_types
from mcp import GetPromptResult
from mcp_types import (
AudioContent,
@@ -31,7 +27,6 @@ from pydantic.json_schema import SkipJsonSchema
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
-from fastmcp.utilities.tasks import TaskConfig, TaskMeta
from fastmcp.utilities.types import (
FastMCPBaseModel,
)
@@ -242,7 +237,6 @@ class Prompt(FastMCPComponent):
icons: list[Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionPrompt:
"""Create a Prompt from a function.
@@ -263,7 +257,6 @@ class Prompt(FastMCPComponent):
icons=icons,
tags=tags,
meta=meta,
- task=task,
auth=auth,
)
@@ -316,89 +309,19 @@ class Prompt(FastMCPComponent):
f"got {type(raw_value).__name__}"
)
- @overload
async def _render(
self,
arguments: dict[str, Any] | None = None,
- task_meta: None = None,
- ) -> PromptResult: ...
+ ) -> PromptResult:
+ """Server entry point for prompt renders.
- @overload
- async def _render(
- self,
- arguments: dict[str, Any] | None,
- task_meta: TaskMeta,
- ) -> mcp_types.CreateTaskResult: ...
-
- async def _render(
- self,
- arguments: dict[str, Any] | None = None,
- task_meta: TaskMeta | None = None,
- ) -> PromptResult | mcp_types.CreateTaskResult:
- """Server entry point that handles task routing.
-
- This allows ANY Prompt subclass to support background execution by setting
- task_config.mode to "supported" or "required". The server calls this
- method instead of render() directly.
-
- Args:
- arguments: Prompt arguments
- task_meta: If provided, execute as background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return PromptResult.
-
- Returns:
- PromptResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
-
- Subclasses can override this to customize task routing behavior.
- For example, FastMCPProviderPrompt overrides to delegate to child
- middleware without submitting to Docket.
+ The server calls this method instead of render() directly so that
+ subclasses can customize dispatch. For example, FastMCPProviderPrompt
+ overrides this to delegate to child-server middleware.
"""
- from fastmcp.server.tasks.routing import check_background_task
-
- task_result = await check_background_task(
- component=self,
- task_type="prompt",
- arguments=arguments,
- task_meta=task_meta,
- )
- if task_result:
- return task_result
-
- # Synchronous execution
result = await self.render(arguments)
return self.convert_result(result)
- def register_with_docket(self, docket: Docket) -> None:
- """Register this prompt with docket for background execution."""
- if not self.task_config.supports_tasks():
- return
- docket.register(self.render, names=[self.key])
-
- async def add_to_docket( # type: ignore[override]
- self,
- docket: Docket,
- arguments: dict[str, Any] | None,
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this prompt for background execution via docket.
-
- Args:
- docket: The Docket instance
- arguments: Prompt arguments
- fn_key: Function lookup key in Docket registry (defaults to self.key)
- task_key: Redis storage key for the result
- **kwargs: Additional kwargs passed to docket.add()
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)(arguments)
-
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
"fastmcp.component.type": "prompt",
diff --git a/fastmcp_slim/fastmcp/prompts/function_prompt.py b/fastmcp_slim/fastmcp/prompts/function_prompt.py
index 959bebd06..9685630b1 100644
--- a/fastmcp_slim/fastmcp/prompts/function_prompt.py
+++ b/fastmcp_slim/fastmcp/prompts/function_prompt.py
@@ -9,7 +9,6 @@ from collections.abc import Callable
from dataclasses import dataclass, field
from types import MethodType
from typing import (
- TYPE_CHECKING,
Any,
Literal,
Protocol,
@@ -33,13 +32,8 @@ from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.docstring_parsing import ParsedDocstring, parse_docstring
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.logging import get_logger
-from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import get_cached_typeadapter
-if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
-
F = TypeVar("F", bound=Callable[..., Any])
logger = get_logger(__name__)
@@ -66,7 +60,6 @@ class PromptMeta:
icons: list[Icon] | None = None
tags: set[str] | None = None
meta: dict[str, Any] | None = None
- task: bool | TaskConfig | None = None
auth: AuthCheck | list[AuthCheck] | None = None
enabled: bool = True
@@ -90,7 +83,6 @@ class FunctionPrompt(Prompt):
icons: list[Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionPrompt:
"""Create a Prompt from a function.
@@ -110,7 +102,7 @@ class FunctionPrompt(Prompt):
# Check mutual exclusion
individual_params_provided = any(
x is not None
- for x in [name, version, title, description, icons, tags, meta, task, auth]
+ for x in [name, version, title, description, icons, tags, meta, auth]
)
if metadata is not None and individual_params_provided:
@@ -129,7 +121,6 @@ class FunctionPrompt(Prompt):
icons=icons,
tags=tags,
meta=meta,
- task=task,
auth=auth,
)
@@ -152,16 +143,6 @@ class FunctionPrompt(Prompt):
# docstring as the prompt description for callable class instances.
outer_docstring = parse_docstring(fn)
- # Normalize task to TaskConfig and validate
- task_value = metadata.task
- if task_value is None:
- task_config = TaskConfig(mode="forbidden")
- elif isinstance(task_value, bool):
- task_config = TaskConfig.from_bool(task_value)
- else:
- task_config = task_value
- task_config.validate_function(fn, func_name)
-
# if the fn is a callable class, we need to get the __call__ method from here out
if not inspect.isroutine(fn) and not isinstance(fn, functools.partial):
fn = fn.__call__
@@ -267,7 +248,6 @@ class FunctionPrompt(Prompt):
tags=metadata.tags or set(),
fn=wrapped_fn,
meta=metadata.meta,
- task_config=task_config,
auth=metadata.auth,
)
@@ -367,37 +347,6 @@ class FunctionPrompt(Prompt):
logger.exception(f"Error rendering prompt {self.name}")
raise PromptError(f"Error rendering prompt {self.name!r}: {e}") from e
- def register_with_docket(self, docket: Docket) -> None:
- """Register this prompt with docket for background execution."""
- if not self.task_config.supports_tasks():
- return
- docket.register(self.fn, names=[self.key])
-
- async def add_to_docket(
- self,
- docket: Docket,
- arguments: dict[str, Any] | None,
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this prompt for background execution via docket.
-
- FunctionPrompt splats the arguments dict since .fn expects **kwargs.
-
- Args:
- docket: The Docket instance
- arguments: Prompt arguments
- fn_key: Function lookup key in Docket registry (defaults to self.key)
- task_key: Redis storage key for the result
- **kwargs: Additional kwargs passed to docket.add()
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)(**(arguments or {}))
-
@overload
def prompt(fn: F) -> F: ...
@@ -411,7 +360,6 @@ def prompt(
icons: list[Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[F], F]: ...
@overload
@@ -425,7 +373,6 @@ def prompt(
icons: list[Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[F], F]: ...
@@ -440,7 +387,6 @@ def prompt(
icons: list[Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Any:
"""Standalone decorator to mark a function as an MCP prompt.
@@ -463,7 +409,6 @@ def prompt(
icons=icons,
tags=tags,
meta=meta,
- task=task,
auth=auth,
)
target = fn.__func__ if isinstance(fn, staticmethod | MethodType) else fn
diff --git a/fastmcp_slim/fastmcp/resources/base.py b/fastmcp_slim/fastmcp/resources/base.py
index abb835bc9..7210a8e63 100644
--- a/fastmcp_slim/fastmcp/resources/base.py
+++ b/fastmcp_slim/fastmcp/resources/base.py
@@ -5,14 +5,11 @@ from __future__ import annotations
import base64
import json
from collections.abc import Callable
-from typing import TYPE_CHECKING, Annotated, Any, ClassVar, overload
+from typing import TYPE_CHECKING, Annotated, Any, ClassVar
import mcp_types
if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
-
from fastmcp.resources.function_resource import FunctionResource
import pydantic
@@ -32,7 +29,6 @@ from typing_extensions import Self
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
-from fastmcp.utilities.tasks import TaskConfig, TaskMeta
class ResourceContent(pydantic.BaseModel):
@@ -339,7 +335,6 @@ class Resource(FastMCPComponent):
tags: set[str] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionResource:
from fastmcp.resources.function_resource import (
@@ -358,7 +353,6 @@ class Resource(FastMCPComponent):
tags=tags,
annotations=annotations,
meta=meta,
- task=task,
auth=auth,
)
@@ -414,43 +408,14 @@ class Resource(FastMCPComponent):
raw_value, mime_type=self.mime_type, meta=self.meta
)
- @overload
- async def _read(self, task_meta: None = None) -> ResourceResult: ...
+ async def _read(self) -> ResourceResult:
+ """Server entry point for resource reads.
- @overload
- async def _read(self, task_meta: TaskMeta) -> mcp_types.CreateTaskResult: ...
-
- async def _read(
- self, task_meta: TaskMeta | None = None
- ) -> ResourceResult | mcp_types.CreateTaskResult:
- """Server entry point that handles task routing.
-
- This allows ANY Resource subclass to support background execution by setting
- task_config.mode to "supported" or "required". The server calls this
- method instead of read() directly.
-
- Args:
- task_meta: If provided, execute as a background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return ResourceResult.
-
- Returns:
- ResourceResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
-
- Subclasses can override this to customize task routing behavior.
- For example, FastMCPProviderResource overrides to delegate to child
- middleware without submitting to Docket.
+ The server calls this method instead of ``read()`` directly so that
+ subclasses can customize dispatch. For example,
+ ``FastMCPProviderResource`` overrides this to delegate to child-server
+ middleware.
"""
- from fastmcp.server.tasks.routing import check_background_task
-
- task_result = await check_background_task(
- component=self, task_type="resource", arguments=None, task_meta=task_meta
- )
- if task_result:
- return task_result
-
- # Synchronous execution - convert result to ResourceResult
result = await self.read()
return self.convert_result(result)
@@ -482,33 +447,6 @@ class Resource(FastMCPComponent):
base_key = self.make_key(str(self.uri))
return f"{base_key}@{self.version or ''}"
- def register_with_docket(self, docket: Docket) -> None:
- """Register this resource with docket for background execution."""
- if not self.task_config.supports_tasks():
- return
- docket.register(self.read, names=[self.key])
-
- async def add_to_docket( # type: ignore[override]
- self,
- docket: Docket,
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this resource for background execution via docket.
-
- Args:
- docket: The Docket instance
- fn_key: Function lookup key in Docket registry (defaults to self.key)
- task_key: Redis storage key for the result
- **kwargs: Additional kwargs passed to docket.add()
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)()
-
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
"fastmcp.component.type": "resource",
diff --git a/fastmcp_slim/fastmcp/resources/function_resource.py b/fastmcp_slim/fastmcp/resources/function_resource.py
index aa71508d9..6d7612939 100644
--- a/fastmcp_slim/fastmcp/resources/function_resource.py
+++ b/fastmcp_slim/fastmcp/resources/function_resource.py
@@ -8,7 +8,6 @@ from collections.abc import Callable
from dataclasses import dataclass, field
from types import MethodType
from typing import (
- TYPE_CHECKING,
Any,
Literal,
Protocol,
@@ -33,11 +32,6 @@ from fastmcp.utilities.async_utils import (
)
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.mime import resolve_ui_mime_type
-from fastmcp.utilities.tasks import TaskConfig
-
-if TYPE_CHECKING:
- from docket import Docket
-
F = TypeVar("F", bound=Callable[..., Any])
@@ -66,7 +60,6 @@ class ResourceMeta:
mime_type: str | None = None
annotations: Annotations | None = None
meta: dict[str, Any] | None = None
- task: bool | TaskConfig | None = None
auth: AuthCheck | list[AuthCheck] | None = None
enabled: bool = True
security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY
@@ -104,7 +97,6 @@ class FunctionResource(Resource):
tags: set[str] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> FunctionResource:
"""Create a FunctionResource from a function.
@@ -131,7 +123,6 @@ class FunctionResource(Resource):
tags,
annotations,
meta,
- task,
auth,
]
)
@@ -159,7 +150,6 @@ class FunctionResource(Resource):
mime_type=mime_type,
annotations=annotations,
meta=meta,
- task=task,
auth=auth,
)
@@ -170,16 +160,6 @@ class FunctionResource(Resource):
metadata.name or getattr(fn, "__name__", None) or fn.__class__.__name__
)
- # Normalize task to TaskConfig and validate
- task_value = metadata.task
- if task_value is None:
- task_config = TaskConfig(mode="forbidden")
- elif isinstance(task_value, bool):
- task_config = TaskConfig.from_bool(task_value)
- else:
- task_config = task_value
- task_config.validate_function(fn, func_name)
-
# if the fn is a callable class, we need to get the __call__ method from here out
if not inspect.isroutine(fn) and not isinstance(fn, functools.partial):
fn = fn.__call__
@@ -215,7 +195,6 @@ class FunctionResource(Resource):
tags=metadata.tags or set(),
annotations=metadata.annotations,
meta=metadata.meta,
- task_config=task_config,
auth=metadata.auth,
)
@@ -240,12 +219,6 @@ class FunctionResource(Resource):
return result
- def register_with_docket(self, docket: Docket) -> None:
- """Register this resource with docket for background execution."""
- if not self.task_config.supports_tasks():
- return
- docket.register(self.fn, names=[self.key])
-
def resource(
uri: str,
@@ -259,7 +232,6 @@ def resource(
tags: set[str] | None = None,
annotations: Annotations | dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY,
) -> Callable[[F], F]:
@@ -289,7 +261,6 @@ def resource(
mime_type=mime_type,
annotations=annotations,
meta=meta,
- task=task,
auth=auth,
security=security,
)
diff --git a/fastmcp_slim/fastmcp/resources/template.py b/fastmcp_slim/fastmcp/resources/template.py
index 2a6622b40..866eb940a 100644
--- a/fastmcp_slim/fastmcp/resources/template.py
+++ b/fastmcp_slim/fastmcp/resources/template.py
@@ -6,22 +6,17 @@ import functools
import inspect
import re
from collections.abc import Callable
-from typing import TYPE_CHECKING, Any, ClassVar, overload
+from typing import Any, ClassVar
from urllib.parse import parse_qs, quote, unquote
-import mcp_types
from mcp_types import Annotations, Icon
-from pydantic.json_schema import SkipJsonSchema
-
-if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
from mcp_types import ResourceTemplate as SDKResourceTemplate
from pydantic import (
Field,
field_validator,
validate_call,
)
+from pydantic.json_schema import SkipJsonSchema
from fastmcp.resources.base import (
Resource,
@@ -37,7 +32,6 @@ from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.json_schema import compress_schema
from fastmcp.utilities.mime import resolve_ui_mime_type
-from fastmcp.utilities.tasks import TaskConfig, TaskMeta
from fastmcp.utilities.types import get_cached_typeadapter
@@ -235,7 +229,6 @@ class ResourceTemplate(FastMCPComponent):
tags: set[str] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY,
) -> FunctionResourceTemplate:
@@ -251,7 +244,6 @@ class ResourceTemplate(FastMCPComponent):
tags=tags,
annotations=annotations,
meta=meta,
- task=task,
auth=auth,
security=security,
)
@@ -290,50 +282,13 @@ class ResourceTemplate(FastMCPComponent):
raw_value, mime_type=self.mime_type, meta=self.meta
)
- @overload
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: None = None
- ) -> ResourceResult: ...
+ async def _read(self, uri: str, params: dict[str, Any]) -> ResourceResult:
+ """Server entry point for template reads.
- @overload
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: TaskMeta
- ) -> mcp_types.CreateTaskResult: ...
-
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
- ) -> ResourceResult | mcp_types.CreateTaskResult:
- """Server entry point that handles task routing.
-
- This allows ANY ResourceTemplate subclass to support background execution
- by setting task_config.mode to "supported" or "required". The server calls
- this method instead of create_resource()/read() directly.
-
- Args:
- uri: The concrete URI being read
- params: Template parameters extracted from the URI
- task_meta: If provided, execute as a background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return ResourceResult.
-
- Returns:
- ResourceResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
-
- Subclasses can override this to customize task routing behavior.
- For example, FastMCPProviderResourceTemplate overrides to delegate to child
- middleware without submitting to Docket.
+ The server calls this instead of create_resource()/read() directly so
+ subclasses can customize dispatch (e.g. FastMCPProviderResourceTemplate
+ delegates to child-server middleware).
"""
- from fastmcp.server.tasks.routing import check_background_task
-
- task_result = await check_background_task(
- component=self, task_type="template", arguments=params, task_meta=task_meta
- )
- if task_result:
- return task_result
-
- # Synchronous execution - create resource and read directly
- # Call resource.read() not resource._read() to avoid task routing on ephemeral resource
resource = await self.create_resource(uri, params)
result = await resource.read()
return self.convert_result(result)
@@ -387,35 +342,6 @@ class ResourceTemplate(FastMCPComponent):
base_key = self.make_key(self.uri_template)
return f"{base_key}@{self.version or ''}"
- def register_with_docket(self, docket: Docket) -> None:
- """Register this template with docket for background execution."""
- if not self.task_config.supports_tasks():
- return
- docket.register(self.read, names=[self.key])
-
- async def add_to_docket( # type: ignore[override]
- self,
- docket: Docket,
- params: dict[str, Any],
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this template for background execution via docket.
-
- Args:
- docket: The Docket instance
- params: Template parameters
- fn_key: Function lookup key in Docket registry (defaults to self.key)
- task_key: Redis storage key for the result
- **kwargs: Additional kwargs passed to docket.add()
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)(params)
-
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
"fastmcp.component.type": "resource_template",
@@ -428,44 +354,13 @@ class FunctionResourceTemplate(ResourceTemplate):
fn: SkipJsonSchema[Callable[..., Any]]
- @overload
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: None = None
- ) -> ResourceResult: ...
-
- @overload
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: TaskMeta
- ) -> mcp_types.CreateTaskResult: ...
-
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
- ) -> ResourceResult | mcp_types.CreateTaskResult:
+ async def _read(self, uri: str, params: dict[str, Any]) -> ResourceResult:
"""Optimized server entry point that skips ephemeral resource creation.
For FunctionResourceTemplate, we can call read() directly instead of
creating a temporary resource, which is more efficient.
-
- Args:
- uri: The concrete URI being read
- params: Template parameters extracted from the URI
- task_meta: If provided, execute as a background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return ResourceResult.
-
- Returns:
- ResourceResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
"""
- from fastmcp.server.tasks.routing import check_background_task
-
- task_result = await check_background_task(
- component=self, task_type="template", arguments=params, task_meta=task_meta
- )
- if task_result:
- return task_result
-
- # Synchronous execution - call read() directly, skip resource creation
+ # Call read() directly, skip resource creation
result = await self.read(arguments=params)
return self.convert_result(result)
@@ -488,7 +383,6 @@ class FunctionResourceTemplate(ResourceTemplate):
meta=self.meta,
title=self.title,
icons=self.icons,
- task=self.task_config,
auth=self.auth,
)
@@ -531,37 +425,6 @@ class FunctionResourceTemplate(ResourceTemplate):
return result
- def register_with_docket(self, docket: Docket) -> None:
- """Register this template with docket for background execution."""
- if not self.task_config.supports_tasks():
- return
- docket.register(self.fn, names=[self.key])
-
- async def add_to_docket(
- self,
- docket: Docket,
- params: dict[str, Any],
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this template for background execution via docket.
-
- FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
-
- Args:
- docket: The Docket instance
- params: Template parameters
- fn_key: Function lookup key in Docket registry (defaults to self.key)
- task_key: Redis storage key for the result
- **kwargs: Additional kwargs passed to docket.add()
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)(**params)
-
@classmethod
def from_function(
cls,
@@ -576,7 +439,6 @@ class FunctionResourceTemplate(ResourceTemplate):
tags: set[str] | None = None,
annotations: Annotations | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY,
) -> FunctionResourceTemplate:
@@ -673,15 +535,6 @@ class FunctionResourceTemplate(ResourceTemplate):
description = description if description is not None else inspect.getdoc(fn)
- # Normalize task to TaskConfig and validate
- if task is None:
- task_config = TaskConfig(mode="forbidden")
- elif isinstance(task, bool):
- task_config = TaskConfig.from_bool(task)
- else:
- task_config = task
- task_config.validate_function(fn, func_name)
-
# if the fn is a callable class, we need to get the __call__ method from here out
if not inspect.isroutine(fn) and not isinstance(fn, functools.partial):
fn = fn.__call__
@@ -716,7 +569,6 @@ class FunctionResourceTemplate(ResourceTemplate):
tags=tags or set(),
annotations=annotations,
meta=meta,
- task_config=task_config,
auth=auth,
security=security,
)
diff --git a/fastmcp_slim/fastmcp/server/context.py b/fastmcp_slim/fastmcp/server/context.py
index ea4d2b228..594b59c00 100644
--- a/fastmcp_slim/fastmcp/server/context.py
+++ b/fastmcp_slim/fastmcp/server/context.py
@@ -308,26 +308,10 @@ class Context:
self._tokens.append(token)
# Set current server for dependency injection (use weakref to avoid reference cycles)
- from fastmcp.server.dependencies import (
- _current_docket,
- _current_server,
- _current_worker,
- is_docket_available,
- )
+ from fastmcp.server.dependencies import _current_server, is_docket_available
self._server_token = _current_server.set(weakref.ref(self.fastmcp))
- # Re-set docket/worker from the server instance so mounted children
- # inherit the parent's Docket via the ContextVar. Only servers that
- # own the Docket (the parent) have _docket set; children skip this,
- # leaving the parent's value in place.
- if is_docket_available():
- server = self.fastmcp
- if server._docket is not None:
- self._docket_token = _current_docket.set(server._docket)
- if server._worker is not None:
- self._worker_token = _current_worker.set(server._worker)
-
if not is_docket_available():
# Without docket, the lifespan won't provide a SharedContext,
# so create one scoped to this Context for Shared() dependencies.
@@ -338,18 +322,8 @@ class Context:
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
"""Exit the context manager and reset the most recent token."""
- from fastmcp.server.dependencies import (
- _current_docket,
- _current_server,
- _current_worker,
- )
+ from fastmcp.server.dependencies import _current_server
- if hasattr(self, "_worker_token"):
- _current_worker.reset(self._worker_token)
- del self._worker_token
- if hasattr(self, "_docket_token"):
- _current_docket.reset(self._docket_token)
- del self._docket_token
if hasattr(self, "_shared_context"):
await self._shared_context.__aexit__(exc_type, exc_val, exc_tb)
del self._shared_context
@@ -1409,15 +1383,13 @@ class Context:
"_elicit_for_task called but not in a background task context"
)
- # Import here to avoid circular imports and optional dependency issues
- from fastmcp.server.tasks.elicitation import elicit_for_task
-
- return await elicit_for_task(
- task_id=self._task_id, # type: ignore[arg-type] # ty:ignore[invalid-argument-type]
- session=self._session,
- message=message,
- schema=schema,
- fastmcp=self.fastmcp,
+ # In-task elicitation is provided by the tasks extension (SEP-2663)
+ # from the `fastmcp-tasks` package. Core no longer ships the SEP-1686
+ # push relay this used to call.
+ raise RuntimeError(
+ "In-task elicitation requires the tasks extension. Install "
+ "'fastmcp[tasks]' and register the tasks extension via "
+ "mcp.add_extension(...)."
)
def _make_state_key(self, key: str) -> str:
diff --git a/fastmcp_slim/fastmcp/server/dependencies.py b/fastmcp_slim/fastmcp/server/dependencies.py
index b39f36e20..f0e260cd1 100644
--- a/fastmcp_slim/fastmcp/server/dependencies.py
+++ b/fastmcp_slim/fastmcp/server/dependencies.py
@@ -1,8 +1,9 @@
"""Dependency injection for FastMCP.
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
-using the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,
-CurrentWorker) and background task execution require fastmcp[tasks].
+using the uncalled-for DI engine. The docket-specific dependencies
+(``CurrentDocket``, ``CurrentWorker``) and background task execution live in the
+``fastmcp-tasks`` package.
"""
from __future__ import annotations
@@ -14,7 +15,6 @@ from collections.abc import AsyncGenerator, Callable, Generator, Mapping
from contextlib import AsyncExitStack, asynccontextmanager, contextmanager
from contextvars import ContextVar
from dataclasses import dataclass
-from datetime import datetime, timezone
from functools import lru_cache
from types import TracebackType
from typing import TYPE_CHECKING, Any, Protocol, cast, get_type_hints, runtime_checkable
@@ -43,9 +43,6 @@ from fastmcp.utilities.async_utils import (
from fastmcp.utilities.types import find_kwarg_by_type, is_class_member_of_type
if TYPE_CHECKING:
- from docket import Docket
- from docket.worker import Worker
-
from fastmcp.server.context import Context
from fastmcp.server.server import FastMCP
@@ -143,15 +140,11 @@ __all__ = [
"AccessToken",
"CurrentAccessToken",
"CurrentContext",
- "CurrentDocket",
"CurrentFastMCP",
"CurrentHeaders",
"CurrentRequest",
- "CurrentWorker",
"FastMCPRequestContext",
"Progress",
- "TaskContextInfo",
- "TaskContextSnapshot",
"TokenClaim",
"bind_request_context",
"extract_version_spec",
@@ -161,38 +154,17 @@ __all__ = [
"get_http_headers",
"get_http_request",
"get_server",
- "get_task_context",
- "get_task_session",
"is_docket_available",
- "register_task_server",
- "register_task_session",
- "require_docket",
"resolve_dependencies",
"transform_context_annotations",
"without_injected_parameters",
]
-# Task context lives in fastmcp.server.tasks.context; public symbols are
-# re-exported here so existing imports from dependencies continue to work.
-from fastmcp.server.tasks.context import ( # noqa: E402
- TaskContextInfo,
- TaskContextSnapshot,
- _recall_snapshot,
- get_task_context,
- get_task_server,
- get_task_session,
- register_task_server,
- register_task_session,
-)
-
_current_server: ContextVar[weakref.ref[FastMCP] | None] = ContextVar(
"server", default=None
)
-_current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None)
-_current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None)
-
# --- Docket availability check ---
@@ -231,43 +203,6 @@ def is_docket_available() -> bool:
return _DOCKET_AVAILABLE
-def require_docket(feature: str) -> None:
- """Raise ImportError with install instructions if docket not available.
-
- Args:
- feature: Description of what requires docket (e.g., "`task=True`",
- "CurrentDocket()"). Will be included in the error message.
- """
- if is_docket_available():
- return
-
- try:
- installed = importlib.metadata.version("pydocket")
- except importlib.metadata.PackageNotFoundError:
- installed = None
-
- if installed is None:
- detail = (
- "FastMCP background tasks require the `tasks` extra. "
- "Install with: pip install 'fastmcp[tasks]'."
- )
- else:
- detail = (
- f"FastMCP background tasks require pydocket>={_MIN_DOCKET_VERSION}, "
- f"but pydocket {installed} is installed (likely pulled in by another "
- f"package). Upgrade with: pip install -U 'pydocket>={_MIN_DOCKET_VERSION}'."
- )
-
- raise ImportError(f"{detail} (Triggered by {feature})")
-
-
-# Import Progress separately — it's docket-specific, not part of uncalled-for
-try:
- from docket.dependencies import Progress as DocketProgress
-except ImportError:
- DocketProgress = None # type: ignore[assignment] # ty:ignore[invalid-assignment]
-
-
# --- Context utilities ---
@@ -425,24 +360,12 @@ def get_context() -> Context:
def get_server() -> FastMCP:
"""Get the current FastMCP server instance directly.
- In a background-task worker, checks the task-server map first so that
- mounted-child tasks resolve to the child server (not the parent that
- started the worker).
-
Returns:
The active FastMCP server
Raises:
RuntimeError: If no server in context
"""
- # In a task context, prefer the task-specific server mapping.
- # This handles mounted-child tasks where _current_server is the parent.
- task_info = get_task_context()
- if task_info is not None:
- task_server = get_task_server(task_info.task_id)
- if task_server is not None:
- return task_server
-
server_ref = _current_server.get()
if server_ref is None:
raise RuntimeError("No FastMCP server instance in context")
@@ -456,8 +379,6 @@ def get_http_request() -> Request:
"""Get the current HTTP request.
Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.
- In background tasks, returns a synthetic request populated with the
- snapshotted headers from the originating HTTP request.
"""
# Try FastMCP's request context first (set during normal MCP request handling)
request = None
@@ -470,33 +391,6 @@ def get_http_request() -> Request:
if request is None:
request = _current_http_request.get()
- # In Docket workers, restore a minimal request from the snapshotted
- # headers. The snapshot is preloaded by restore_task_snapshot before
- # user code runs, so this is a pure ContextVar read.
- if request is None:
- task_info = get_task_context()
- snapshot = _recall_snapshot(task_info.task_id) if task_info else None
- task_headers = snapshot.http_headers if snapshot else None
- if task_headers:
- request = Request(
- {
- "type": "http",
- "http_version": "1.1",
- "method": "POST",
- "scheme": "http",
- "path": "/",
- "raw_path": b"/",
- "query_string": b"",
- "headers": [
- (name.encode("latin-1"), value.encode("latin-1"))
- for name, value in task_headers.items()
- ],
- "client": None,
- "server": None,
- "root_path": "",
- }
- )
-
if request is None:
raise RuntimeError("No active HTTP request found.")
return request
@@ -565,8 +459,7 @@ def get_access_token() -> AccessToken | None:
This function first tries to get the token from the current HTTP request's scope,
which is more reliable for long-lived connections where the SDK's auth_context_var
may become stale after token refresh. Falls back to the SDK's context var if no
- request is available. In background tasks (Docket workers), falls back to the
- token snapshot stored in Redis at task submission time.
+ request is available.
Returns:
The access token if an authenticated user is available, None otherwise.
@@ -589,19 +482,6 @@ def get_access_token() -> AccessToken | None:
if access_token is None:
access_token = _sdk_get_access_token()
- # Fall back to background task snapshot (#3095). In Docket workers,
- # neither the HTTP request nor the SDK context var is available; the
- # snapshot is preloaded by restore_task_snapshot before user code runs.
- if access_token is None:
- task_info = get_task_context()
- snapshot = _recall_snapshot(task_info.task_id) if task_info else None
- if snapshot is not None and snapshot.access_token_json is not None:
- task_token = AccessToken.model_validate_json(snapshot.access_token_json)
- if task_token.expires_at is not None:
- if task_token.expires_at < int(datetime.now(timezone.utc).timestamp()):
- return None
- return task_token
-
if access_token is None or isinstance(access_token, AccessToken):
return access_token
@@ -843,53 +723,24 @@ async def resolve_dependencies(
class _CurrentContext(Dependency["Context"]):
"""Async context manager for Context dependency.
- In foreground (request) mode: returns the active context from _current_context.
- In background (Docket worker) mode: creates a task-aware Context with task_id
- and loads the unified task snapshot from Redis.
+ Returns the active context from _current_context (normal MCP request).
The shared default instance is a stateless factory. All per-invocation
- state lives on the returned Context or in task-local ContextVars, so
- concurrent tasks never share mutable state.
+ state lives on the returned Context, so concurrent calls never share
+ mutable state.
"""
async def __aenter__(self) -> Context:
- from fastmcp.server.context import Context, _current_context
+ from fastmcp.server.context import _current_context
# Try foreground context first (normal MCP request)
context = _current_context.get()
if context is not None:
return context
- # Check if we're in a Docket worker context
- task_info = get_task_context()
- if task_info is not None:
- server = get_server()
-
- # The snapshot is preloaded by restore_task_snapshot (worker-level
- # Docket dependency) before any task code runs, so this is a pure
- # ContextVar read — no Redis I/O here.
- snapshot = _recall_snapshot(task_info.task_id)
- origin_request_id = snapshot.origin_request_id if snapshot else None
-
- # Session ID is stored in the snapshot for notification delivery
- snapshot_session_id = snapshot.session_id if snapshot else None
- session = (
- get_task_session(snapshot_session_id) if snapshot_session_id else None
- )
-
- ctx = Context(
- fastmcp=server,
- session=session,
- task_id=task_info.task_id,
- origin_request_id=origin_request_id,
- )
- await ctx.__aenter__()
- return ctx
-
raise RuntimeError(
"No active context found. This can happen if:\n"
" - Called outside an MCP request handler\n"
- " - Called in a background task before session was registered\n"
"Check `context.request_context` for None before accessing."
)
@@ -966,118 +817,6 @@ def OptionalCurrentContext() -> Context | None:
return cast("Context | None", _OptionalCurrentContext())
-class _CurrentDocket(Dependency["Docket"]):
- """Async context manager for Docket dependency."""
-
- async def __aenter__(self) -> Docket:
- require_docket("CurrentDocket()")
- # Check server instance first, fall back to ContextVar for mounted children
- # whose parent owns the Docket
- try:
- docket = get_server()._docket
- except RuntimeError:
- docket = None
- if docket is None:
- docket = _current_docket.get()
- if docket is None:
- raise RuntimeError(
- "No Docket instance found. Docket is only initialized when there are "
- "task-enabled components (task=True). Add task=True to a component "
- "to enable Docket infrastructure."
- )
- return docket
-
- async def __aexit__(
- self,
- exc_type: type[BaseException] | None,
- exc_value: BaseException | None,
- traceback: TracebackType | None,
- ) -> None:
- pass
-
-
-def CurrentDocket() -> Docket:
- """Get the current Docket instance managed by FastMCP.
-
- This dependency provides access to the Docket instance that FastMCP
- automatically creates for background task scheduling.
-
- Returns:
- A dependency that resolves to the active Docket instance
-
- Raises:
- RuntimeError: If not within a FastMCP server context
- ImportError: If fastmcp[tasks] not installed
-
- Example:
- ```python
- from fastmcp.dependencies import CurrentDocket
-
- @mcp.tool()
- async def schedule_task(docket: Docket = CurrentDocket()) -> str:
- await docket.add(some_function)(arg1, arg2)
- return "Scheduled"
- ```
- """
- require_docket("CurrentDocket()")
- return cast("Docket", _CurrentDocket())
-
-
-class _CurrentWorker(Dependency["Worker"]):
- """Async context manager for Worker dependency."""
-
- async def __aenter__(self) -> Worker:
- require_docket("CurrentWorker()")
- # Check server instance first, fall back to ContextVar for mounted children
- try:
- worker = get_server()._worker
- except RuntimeError:
- worker = None
- if worker is None:
- worker = _current_worker.get()
- if worker is None:
- raise RuntimeError(
- "No Worker instance found. Worker is only initialized when there are "
- "task-enabled components (task=True). Add task=True to a component "
- "to enable Docket infrastructure."
- )
- return worker
-
- async def __aexit__(
- self,
- exc_type: type[BaseException] | None,
- exc_value: BaseException | None,
- traceback: TracebackType | None,
- ) -> None:
- pass
-
-
-def CurrentWorker() -> Worker:
- """Get the current Docket Worker instance managed by FastMCP.
-
- This dependency provides access to the Worker instance that FastMCP
- automatically creates for background task processing.
-
- Returns:
- A dependency that resolves to the active Worker instance
-
- Raises:
- RuntimeError: If not within a FastMCP server context
- ImportError: If fastmcp[tasks] not installed
-
- Example:
- ```python
- from fastmcp.dependencies import CurrentWorker
-
- @mcp.tool()
- async def check_worker_status(worker: Worker = CurrentWorker()) -> str:
- return f"Worker: {worker.name}"
- ```
- """
- require_docket("CurrentWorker()")
- return cast("Worker", _CurrentWorker())
-
-
class _CurrentFastMCP(Dependency["FastMCP"]):
"""Async context manager for FastMCP server dependency."""
diff --git a/fastmcp_slim/fastmcp/server/low_level.py b/fastmcp_slim/fastmcp/server/low_level.py
index 1a9362649..851bd74a7 100644
--- a/fastmcp_slim/fastmcp/server/low_level.py
+++ b/fastmcp_slim/fastmcp/server/low_level.py
@@ -475,11 +475,10 @@ class LowLevelServer(_Server[LifespanResultT]):
*,
protocol_version: str | None = None,
) -> mcp_types.ServerCapabilities:
- """Override to set capabilities.tasks as a first-class field per SEP-1686
- and advertise the MCP Apps UI extension.
+ """Override to advertise registered extensions and the MCP Apps UI extension.
- ``ServerCapabilities.tasks`` and ``ServerCapabilities.extensions`` are
- real declared fields in v2, so we update them directly. The
+ ``ServerCapabilities.extensions`` is a real declared field in v2, so we
+ update it directly. The
`FastMCP(experimental_capabilities=...)` merge also lives here rather
than in `create_initialization_options`: the modern `server/discover`
handler calls this directly, without going through
@@ -487,8 +486,6 @@ class LowLevelServer(_Server[LifespanResultT]):
the handshake-era `initialize` response and silently dropped
constructor-configured experimental capabilities from `discover`.
"""
- from fastmcp.server.tasks.capabilities import get_task_capabilities
-
merged_experimental = {
**self.fastmcp.experimental_capabilities,
**(experimental_capabilities or {}),
@@ -513,7 +510,6 @@ class LowLevelServer(_Server[LifespanResultT]):
}
return capabilities.model_copy(
update={
- "tasks": get_task_capabilities(),
"extensions": {
**existing_extensions,
UI_EXTENSION_ID: {},
diff --git a/fastmcp_slim/fastmcp/server/mixins/lifespan.py b/fastmcp_slim/fastmcp/server/mixins/lifespan.py
index 5ea62a1fb..f460e1872 100644
--- a/fastmcp_slim/fastmcp/server/mixins/lifespan.py
+++ b/fastmcp_slim/fastmcp/server/mixins/lifespan.py
@@ -1,18 +1,16 @@
-"""Lifespan and Docket task infrastructure for FastMCP Server."""
+"""Lifespan infrastructure for FastMCP Server."""
from __future__ import annotations
-import asyncio
import weakref
from collections.abc import AsyncIterator
-from contextlib import AsyncExitStack, asynccontextmanager, suppress
+from contextlib import AsyncExitStack, asynccontextmanager
from contextvars import ContextVar
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
import anyio
from uncalled_for import SharedContext
-import fastmcp
from fastmcp.utilities.logging import get_logger
if TYPE_CHECKING:
@@ -25,169 +23,64 @@ logger = get_logger(__name__)
# Set True by `FastMCPProvider.lifespan` immediately before it enters the
# wrapped (mounted) server's `_lifespan_manager`, and reset on exit. The
-# mounted server's `_docket_lifespan` reads this and becomes a no-op so that
-# Docket / Worker / SharedContext are not re-initialized — there's one set
-# per runtime tree, owned by the root.
+# mounted server's `_shared_context_lifespan` reads this and becomes a no-op so
+# that SharedContext and the server ContextVar are not re-initialized — there's
+# one set per runtime tree, owned by the root. Extension lifespans (e.g. the
+# tasks extension's Docket/Worker) defer to the root the same way.
#
# Independent servers entered as siblings (e.g. via `AsyncExitStack` in the
# same async context) are NOT in a parent/child relationship; the flag is not
-# set in that case, so each independently establishes its own Docket and
-# server context.
+# set in that case, so each independently establishes its own server context.
_lifespan_root_active: ContextVar[bool] = ContextVar(
"fastmcp_lifespan_root_active", default=False
)
class LifespanMixin:
- """Mixin providing lifespan and Docket task infrastructure for FastMCP."""
+ """Mixin providing lifespan infrastructure for FastMCP."""
@property
def docket(self: FastMCP) -> Docket | None:
- """The Docket instance owned by this server.
+ """The Docket instance owned by this server, if the tasks extension is active.
- Returns the Docket that this server initialized as the root of a
- runtime tree. Mounted children do not own their own Docket — they
- share the root's via ``_current_docket`` ContextVar inheritance —
- so accessing ``.docket`` on a mounted child returns None even while
- its tasks run on the root's Docket. For "the Docket in scope right
- now," prefer reading ``_current_docket`` directly or use the
- ``CurrentDocket`` dependency injection.
+ Returns the Docket that the tasks extension initialized as the root of a
+ runtime tree, or None when no task backend is running. Mounted children do
+ not own their own Docket — they share the root's via ``_current_docket``
+ ContextVar inheritance — so accessing ``.docket`` on a mounted child
+ returns None even while its tasks run on the root's Docket.
"""
return self._docket
@asynccontextmanager
- async def _docket_lifespan(self: FastMCP) -> AsyncIterator[None]:
- """Manage Docket instance and Worker for background task execution.
+ async def _shared_context_lifespan(self: FastMCP) -> AsyncIterator[None]:
+ """Set up the process-level ``SharedContext`` and server ContextVar.
- Docket is process-level, not server-level: only the first server in a
- runtime tree starts Docket and the Worker. Mounted children entered
- via ``FastMCPProvider.lifespan`` see ``_lifespan_root_active=True``
- (set by the provider before delegating to ``_lifespan_manager``) and
- become no-ops, sharing the root's Docket via ``_current_docket``.
+ ``SharedContext`` backs app-scoped ``Shared()`` dependencies and is
+ process-level, not server-level: only the first server in a runtime tree
+ establishes it. Mounted children entered via ``FastMCPProvider.lifespan``
+ see ``_lifespan_root_active=True`` (set by the provider before delegating
+ to ``_lifespan_manager``) and become no-ops, sharing the root's context
+ via ContextVars.
Independent servers entered as siblings — for example two unrelated
- ``FastMCP`` instances each entered through ``AsyncExitStack`` in the
- same async context — are not in a parent/child relationship; no
- provider has set the flag for them, so each runs the full root setup.
-
- Docket infrastructure is only initialized at the root if:
- 1. pydocket is installed (fastmcp[tasks] extra)
- 2. There are task-enabled components (task_config.mode != 'forbidden')
-
- Users with pydocket installed but no task-enabled components won't spin
- up Docket / Worker infrastructure even at the root.
+ ``FastMCP`` instances each entered through ``AsyncExitStack`` in the same
+ async context — are not in a parent/child relationship; no provider has
+ set the flag for them, so each runs the full root setup.
"""
- # Nested entry: a parent in this runtime tree already owns Docket and
- # SharedContext (the FastMCPProvider that mounted us set the flag).
- # Stay out of their way and inherit via ContextVars.
if _lifespan_root_active.get():
yield
return
- async with self._docket_lifespan_root():
- yield
-
- @asynccontextmanager
- async def _docket_lifespan_root(self: FastMCP) -> AsyncIterator[None]:
- """Root-only Docket lifecycle. See _docket_lifespan for the dispatch."""
- from fastmcp.server.dependencies import _current_server, is_docket_available
+ from fastmcp.server.dependencies import _current_server
# Set FastMCP server in ContextVar so CurrentFastMCP can access it
# (use weakref to avoid reference cycles)
server_token = _current_server.set(weakref.ref(self))
-
try:
- # If docket is not available, skip task infrastructure but still
- # set up SharedContext so Shared() dependencies work.
- if not is_docket_available():
- async with SharedContext():
- self._capture_shared_context()
- yield
- return
-
- # Collect task-enabled components at startup with all transforms applied.
- # Components must be available now to be registered with Docket workers;
- # dynamically added components after startup won't be registered.
- try:
- task_components = list(await self.get_tasks())
- except Exception as e:
- logger.warning(f"Failed to get tasks: {e}")
- if fastmcp.settings.mounted_components_raise_on_load_error:
- raise
- task_components = []
-
- # If no task-enabled components, skip Docket infrastructure but still
- # set up SharedContext so Shared() dependencies work.
- if not task_components:
- async with SharedContext():
- self._capture_shared_context()
- yield
- return
-
- # Docket is available AND there are task-enabled components
- from docket import Depends, Docket, Worker
-
- from fastmcp import settings
- from fastmcp.server.dependencies import (
- _current_docket,
- _current_worker,
- )
- from fastmcp.server.tasks.context import restore_task_snapshot
-
- # Create Docket instance using configured name and URL
- async with (
- SharedContext(),
- Docket(
- name=settings.docket.name,
- url=settings.docket.url,
- ) as docket,
- ):
+ async with SharedContext():
self._capture_shared_context()
- self._docket = docket
-
- # Register task-enabled components with Docket
- for component in task_components:
- component.register_with_docket(docket)
-
- docket_token = _current_docket.set(docket)
- try:
- # Build worker kwargs from settings
- worker_kwargs: dict[str, Any] = {
- "concurrency": settings.docket.concurrency,
- "redelivery_timeout": settings.docket.redelivery_timeout,
- "reconnection_delay": settings.docket.reconnection_delay,
- "minimum_check_interval": settings.docket.minimum_check_interval,
- }
- if settings.docket.worker_name:
- worker_kwargs["name"] = settings.docket.worker_name
-
- # Create and start Worker. The restore_task_snapshot
- # worker-level dependency runs before every task so the
- # per-task snapshot ContextVar is populated before user
- # code or task-scoped dependencies observe it.
- async with Worker(
- docket,
- dependencies=[Depends(restore_task_snapshot)],
- **worker_kwargs,
- ) as worker:
- self._worker = worker
- worker_token = _current_worker.set(worker)
- try:
- worker_task = asyncio.create_task(worker.run_forever())
- try:
- yield
- finally:
- worker_task.cancel()
- with suppress(asyncio.CancelledError):
- await worker_task
- finally:
- _current_worker.reset(worker_token)
- self._worker = None
- finally:
- _current_docket.reset(docket_token)
- self._docket = None
+ yield
finally:
- # Reset server ContextVar
_current_server.reset(server_token)
@asynccontextmanager
@@ -196,11 +89,10 @@ class LifespanMixin:
Extension lifespans are entered once per runtime tree, at the root. A
mounted child sees ``_lifespan_root_active`` set by its
- ``FastMCPProvider`` and defers to the root, exactly as
- ``_docket_lifespan`` does for the shared Docket: an extension whose
- lifespan starts shared infrastructure (a task-queue backend and worker,
- say) is therefore owned by the tree root, and mounted children reach it
- through the same context rather than starting a second copy.
+ ``FastMCPProvider`` and defers to the root: an extension whose lifespan
+ starts shared infrastructure (a task-queue backend and worker, say) is
+ therefore owned by the tree root, and mounted children reach it through
+ the same context rather than starting a second copy.
Extensions are entered in registration order; the ``AsyncExitStack``
exits them in reverse on teardown.
@@ -214,6 +106,39 @@ class LifespanMixin:
await stack.enter_async_context(extension.lifespan())
yield
+ async def _validate_task_extension_registered(self: FastMCP) -> None:
+ """Fail loudly if a task-enabled tool has no tasks extension registered.
+
+ `task=True` on a tool is only an intent declaration; the engine that runs
+ it lives in the `fastmcp-tasks` package and is installed by registering a
+ `ServerExtension` whose identifier is `TASKS_EXTENSION_ID`
+ (`mcp.add_extension(...)`). A task-configured tool serving without that
+ extension would silently never run as a task — a correctness bug — so we
+ raise at serve time instead.
+ """
+ from fastmcp.utilities.tasks import TASKS_EXTENSION_ID
+
+ if TASKS_EXTENSION_ID in self._extensions:
+ return
+
+ candidates = list(await self.get_tasks())
+
+ # ``get_tasks()`` applies server-level transforms, which can inject
+ # non-task tools (e.g. ResourcesAsTools' synthetic list/read tools) into
+ # the result, so re-filter by the actual task config here — mirroring the
+ # guard the old per-component docket registration applied.
+ task_components = [c for c in candidates if c.task_config.supports_tasks()]
+ if not task_components:
+ return
+
+ names = ", ".join(sorted(c.name for c in task_components))
+ raise RuntimeError(
+ f"Task-enabled tools ({names}) require the tasks extension, but no "
+ f"extension with identifier {TASKS_EXTENSION_ID!r} is registered. "
+ "Install it with `pip install 'fastmcp[tasks]'` and register it via "
+ "`mcp.add_extension(TasksExtension(...))`."
+ )
+
def _capture_shared_context(self: FastMCP) -> None:
"""Snapshot the live ``SharedContext`` ContextVar values.
@@ -261,7 +186,7 @@ class LifespanMixin:
stack = AsyncExitStack()
try:
user_lifespan_result = await stack.enter_async_context(self._lifespan(self))
- await stack.enter_async_context(self._docket_lifespan())
+ await stack.enter_async_context(self._shared_context_lifespan())
await stack.enter_async_context(self._extensions_lifespan())
self._lifespan_result = user_lifespan_result
@@ -271,6 +196,8 @@ class LifespanMixin:
for provider in self.providers:
await stack.enter_async_context(provider.lifespan())
+ await self._validate_task_extension_registered()
+
self._started.set()
try:
yield
@@ -286,74 +213,3 @@ class LifespanMixin:
if self._lifespan_ref_count == 0:
self._lifespan_result_set = False
self._lifespan_result = None
-
- def _setup_task_protocol_handlers(self: FastMCP) -> None:
- """Register SEP-1686 task protocol handlers with SDK.
-
- Only registers handlers if docket is installed. Without docket,
- task protocol requests will return "method not found" errors.
- """
- from fastmcp.server.dependencies import is_docket_available
-
- if not is_docket_available():
- return
-
- from mcp.server.context import ServerRequestContext
- from mcp_types import (
- CancelTaskRequestParams,
- GetTaskPayloadRequestParams,
- GetTaskRequestParams,
- PaginatedRequestParams,
- )
-
- from fastmcp.server.dependencies import bind_request_context
- from fastmcp.server.tasks.requests import (
- tasks_cancel_handler,
- tasks_get_handler,
- tasks_list_handler,
- tasks_result_handler,
- )
-
- # v2 handlers take (ctx, params) and return the bare result model.
-
- async def handle_get_task(
- ctx: ServerRequestContext, params: GetTaskRequestParams
- ) -> Any:
- with bind_request_context(ctx):
- p = params.model_dump(by_alias=True, exclude_none=True)
- return await tasks_get_handler(self, p)
-
- async def handle_get_task_result(
- ctx: ServerRequestContext, params: GetTaskPayloadRequestParams
- ) -> Any:
- with bind_request_context(ctx):
- p = params.model_dump(by_alias=True, exclude_none=True)
- return await tasks_result_handler(self, p)
-
- async def handle_list_tasks(
- ctx: ServerRequestContext, params: PaginatedRequestParams | None
- ) -> Any:
- with bind_request_context(ctx):
- p = (
- params.model_dump(by_alias=True, exclude_none=True)
- if params
- else {}
- )
- return await tasks_list_handler(self, p)
-
- async def handle_cancel_task(
- ctx: ServerRequestContext, params: CancelTaskRequestParams
- ) -> Any:
- with bind_request_context(ctx):
- p = params.model_dump(by_alias=True, exclude_none=True)
- return await tasks_cancel_handler(self, p)
-
- s = self._mcp_server
- s.add_request_handler("tasks/get", GetTaskRequestParams, handle_get_task)
- s.add_request_handler(
- "tasks/result", GetTaskPayloadRequestParams, handle_get_task_result
- )
- s.add_request_handler("tasks/list", PaginatedRequestParams, handle_list_tasks)
- s.add_request_handler(
- "tasks/cancel", CancelTaskRequestParams, handle_cancel_task
- )
diff --git a/fastmcp_slim/fastmcp/server/mixins/mcp_operations.py b/fastmcp_slim/fastmcp/server/mixins/mcp_operations.py
index f0d245d50..3bc8e96ba 100644
--- a/fastmcp_slim/fastmcp/server/mixins/mcp_operations.py
+++ b/fastmcp_slim/fastmcp/server/mixins/mcp_operations.py
@@ -29,7 +29,6 @@ from fastmcp.exceptions import (
)
from fastmcp.server.completions import CompletionValues, normalize_completion
from fastmcp.server.dependencies import bind_request_context, extract_version_spec
-from fastmcp.server.tasks.config import TaskMeta
from fastmcp.tools.base import InputRequiredToolResult
from fastmcp.utilities.async_utils import (
call_sync_fn_in_threadpool,
@@ -127,9 +126,6 @@ class MCPOperationsMixin:
"logging/setLevel", SetLevelRequestParams, self._on_set_logging_level
)
- # Register SEP-1686 task protocol handlers
- self._setup_task_protocol_handlers()
-
async def _on_list_tools(
self: FastMCP,
ctx: ServerRequestContext,
@@ -220,17 +216,9 @@ class MCPOperationsMixin:
self: FastMCP,
ctx: ServerRequestContext,
params: CallToolRequestParams,
- ) -> (
- mcp_types.CallToolResult
- | mcp_types.InputRequiredResult
- | mcp_types.CreateTaskResult
- ):
+ ) -> mcp_types.CallToolResult | mcp_types.InputRequiredResult:
"""Handle MCP 'tools/call' requests.
- Task metadata is a first-class params field (``params.task``); its
- presence triggers backgrounding. The tool's ``_run()`` handles the
- backgrounding decision so middleware runs before Docket.
-
A guard tool (SEP-2322 multi-round-trip) requests client input by
returning an ``InputRequiredResult`` from its body; the run machinery
wraps that in an ``InputRequiredToolResult`` (a ``ToolResult``
@@ -250,14 +238,9 @@ class MCPOperationsMixin:
)
version = _version_from_ctx(ctx)
- task_meta = (
- TaskMeta(ttl=params.task.ttl) if params.task is not None else None
- )
try:
- result = await self.call_tool(
- key, arguments, version=version, task_meta=task_meta
- )
+ result = await self.call_tool(key, arguments, version=version)
except (DisabledError, NotFoundError):
# Unknown/disabled tool: return an error result (matching the
# v1 SDK's call_tool behavior) so the client surfaces a
@@ -280,8 +263,6 @@ class MCPOperationsMixin:
is_error=True,
)
- if isinstance(result, mcp_types.CreateTaskResult):
- return result
if isinstance(result, InputRequiredToolResult):
# A guard tool requested client input (SEP-2322). The
# multi-round-trip result type only exists at 2026-07-28; on an
@@ -305,14 +286,8 @@ class MCPOperationsMixin:
self: FastMCP,
ctx: ServerRequestContext,
params: ReadResourceRequestParams,
- ) -> mcp_types.ReadResourceResult | mcp_types.CreateTaskResult:
- """Handle MCP 'resources/read' requests.
-
- Note: ``ReadResourceRequestParams`` has no ``task`` field in this SDK
- version, so resource task submission over the wire is not expressible;
- ``task_meta`` is always None here. The CreateTaskResult return branch is
- retained harmlessly pending an upstream ``task`` field on these params.
- """
+ ) -> mcp_types.ReadResourceResult:
+ """Handle MCP 'resources/read' requests."""
with bind_request_context(ctx):
uri = params.uri
logger.debug(f"[{self.name}] Handler called: read_resource %s", uri)
@@ -336,21 +311,14 @@ class MCPOperationsMixin:
# already happened inside read_resource.
raise to_mcp_error(e) from e
- if isinstance(result, mcp_types.CreateTaskResult):
- return result
return result.to_mcp_result(uri)
async def _on_get_prompt(
self: FastMCP,
ctx: ServerRequestContext,
params: GetPromptRequestParams,
- ) -> mcp_types.GetPromptResult | mcp_types.CreateTaskResult:
- """Handle MCP 'prompts/get' requests.
-
- Note: ``GetPromptRequestParams`` has no ``task`` field in this SDK
- version, so prompt task submission over the wire is not expressible;
- ``task_meta`` is always None here.
- """
+ ) -> mcp_types.GetPromptResult:
+ """Handle MCP 'prompts/get' requests."""
with bind_request_context(ctx):
name = params.name
arguments = params.arguments
@@ -374,8 +342,6 @@ class MCPOperationsMixin:
# Masking already happened inside render_prompt.
raise to_mcp_error(e) from e
- if isinstance(result, mcp_types.CreateTaskResult):
- return result
return result.to_mcp_prompt_result()
async def _on_set_logging_level(
diff --git a/fastmcp_slim/fastmcp/server/providers/fastmcp_provider.py b/fastmcp_slim/fastmcp/server/providers/fastmcp_provider.py
index 0f05c798e..7cb9d9213 100644
--- a/fastmcp_slim/fastmcp/server/providers/fastmcp_provider.py
+++ b/fastmcp_slim/fastmcp/server/providers/fastmcp_provider.py
@@ -12,25 +12,20 @@ from __future__ import annotations
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
-from typing import TYPE_CHECKING, Any, overload
+from typing import TYPE_CHECKING, Any
-import mcp_types
from pydantic import AnyUrl
from fastmcp.prompts.base import Prompt, PromptResult
from fastmcp.resources.base import Resource, ResourceResult
from fastmcp.resources.template import ResourceTemplate, expand_uri_template
from fastmcp.server.providers.base import Provider
-from fastmcp.server.tasks.config import TaskMeta
from fastmcp.server.telemetry import delegate_span
from fastmcp.tools.base import Tool, ToolResult
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.versions import VersionSpec
if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
-
from fastmcp.server.server import FastMCP
@@ -80,32 +75,13 @@ class FastMCPProviderTool(Tool):
icons=tool.icons,
)
- @overload
- async def _run(
- self,
- arguments: dict[str, Any],
- task_meta: None = None,
- ) -> ToolResult: ...
+ async def _run(self, arguments: dict[str, Any]) -> ToolResult:
+ """Delegate to the child server's call_tool().
- @overload
- async def _run(
- self,
- arguments: dict[str, Any],
- task_meta: TaskMeta,
- ) -> mcp_types.CreateTaskResult: ...
-
- async def _run(
- self,
- arguments: dict[str, Any],
- task_meta: TaskMeta | None = None,
- ) -> ToolResult | mcp_types.CreateTaskResult:
- """Delegate to child server's call_tool() with task_meta.
-
- Passes task_meta through to the child server so it can handle
- backgrounding appropriately. fn_key is already set by the parent
- server before calling this method. A child tool that requests client
- input (SEP-2322) returns an `InputRequiredToolResult`, which forwards
- through this delegation to the parent's wire handler unchanged.
+ fn_key is already set by the parent server before calling this method. A
+ child tool that requests client input (SEP-2322) returns an
+ `InputRequiredToolResult`, which forwards through this delegation to the
+ parent's wire handler unchanged.
"""
# Pass exact version so child executes the correct version
version = VersionSpec(eq=self.version) if self.version else None
@@ -120,27 +96,20 @@ class FastMCPProviderTool(Tool):
self._original_name,
arguments,
version=version,
- task_meta=task_meta,
)
async def run(self, arguments: dict[str, Any]) -> ToolResult:
- """Delegate to child server's call_tool() without task_meta.
+ """Delegate to the child server's call_tool().
This is called when the tool is used within a TransformedTool
- forwarding function or other contexts where task_meta is not available.
+ forwarding function or other contexts.
"""
# Pass exact version so child executes the correct version
version = VersionSpec(eq=self.version) if self.version else None
- result = await self._server.call_tool(
+ return await self._server.call_tool(
self._original_name, arguments, version=version
)
- # Result from call_tool should always be ToolResult when no task_meta.
- if isinstance(result, mcp_types.CreateTaskResult):
- raise RuntimeError(
- "Unexpected CreateTaskResult from call_tool without task_meta"
- )
- return result
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
@@ -188,20 +157,10 @@ class FastMCPProviderResource(Resource):
icons=resource.icons,
)
- @overload
- async def _read(self, task_meta: None = None) -> ResourceResult: ...
+ async def _read(self) -> ResourceResult:
+ """Delegate to the child server's read_resource().
- @overload
- async def _read(self, task_meta: TaskMeta) -> mcp_types.CreateTaskResult: ...
-
- async def _read(
- self, task_meta: TaskMeta | None = None
- ) -> ResourceResult | mcp_types.CreateTaskResult:
- """Delegate to child server's read_resource() with task_meta.
-
- Passes task_meta through to the child server so it can handle
- backgrounding appropriately. fn_key is already set by the parent
- server before calling this method.
+ fn_key is already set by the parent server before calling this method.
"""
# Pass exact version so child reads the correct version
version = VersionSpec(eq=self.version) if self.version else None
@@ -212,9 +171,7 @@ class FastMCPProviderResource(Resource):
self._original_uri or "",
method="resources/read",
):
- return await self._server.read_resource(
- self._original_uri, version=version, task_meta=task_meta
- )
+ return await self._server.read_resource(self._original_uri, version=version)
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
@@ -260,30 +217,10 @@ class FastMCPProviderPrompt(Prompt):
icons=prompt.icons,
)
- @overload
- async def _render(
- self,
- arguments: dict[str, Any] | None = None,
- task_meta: None = None,
- ) -> PromptResult: ...
+ async def _render(self, arguments: dict[str, Any] | None = None) -> PromptResult:
+ """Delegate to the child server's render_prompt().
- @overload
- async def _render(
- self,
- arguments: dict[str, Any] | None,
- task_meta: TaskMeta,
- ) -> mcp_types.CreateTaskResult: ...
-
- async def _render(
- self,
- arguments: dict[str, Any] | None = None,
- task_meta: TaskMeta | None = None,
- ) -> PromptResult | mcp_types.CreateTaskResult:
- """Delegate to child server's render_prompt() with task_meta.
-
- Passes task_meta through to the child server so it can handle
- backgrounding appropriately. fn_key is already set by the parent
- server before calling this method.
+ fn_key is already set by the parent server before calling this method.
"""
# Pass exact version so child renders the correct version
version = VersionSpec(eq=self.version) if self.version else None
@@ -295,27 +232,21 @@ class FastMCPProviderPrompt(Prompt):
method="prompts/get",
):
return await self._server.render_prompt(
- self._original_name, arguments, version=version, task_meta=task_meta
+ self._original_name, arguments, version=version
)
async def render(self, arguments: dict[str, Any] | None = None) -> PromptResult:
- """Delegate to child server's render_prompt() without task_meta.
+ """Delegate to the child server's render_prompt().
This is called when the prompt is used within a transformed context
- or other contexts where task_meta is not available.
+ or other contexts.
"""
# Pass exact version so child renders the correct version
version = VersionSpec(eq=self.version) if self.version else None
- result = await self._server.render_prompt(
+ return await self._server.render_prompt(
self._original_name, arguments, version=version
)
- # Result from render_prompt should always be PromptResult when no task_meta
- if isinstance(result, mcp_types.CreateTaskResult):
- raise RuntimeError(
- "Unexpected CreateTaskResult from render_prompt without task_meta"
- )
- return result
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
@@ -391,24 +322,10 @@ class FastMCPProviderResourceTemplate(ResourceTemplate):
icons=self.icons,
)
- @overload
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: None = None
- ) -> ResourceResult: ...
+ async def _read(self, uri: str, params: dict[str, Any]) -> ResourceResult:
+ """Delegate to the child server's read_resource().
- @overload
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: TaskMeta
- ) -> mcp_types.CreateTaskResult: ...
-
- async def _read(
- self, uri: str, params: dict[str, Any], task_meta: TaskMeta | None = None
- ) -> ResourceResult | mcp_types.CreateTaskResult:
- """Delegate to child server's read_resource() with task_meta.
-
- Passes task_meta through to the child server so it can handle
- backgrounding appropriately. fn_key is already set by the parent
- server before calling this method.
+ fn_key is already set by the parent server before calling this method.
"""
# Expand the original template with params to get internal URI
original_uri = expand_uri_template(self._original_uri_template or "", params)
@@ -422,50 +339,7 @@ class FastMCPProviderResourceTemplate(ResourceTemplate):
self._original_uri_template or "",
method="resources/read",
):
- return await self._server.read_resource(
- original_uri, version=version, task_meta=task_meta
- )
-
- async def read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult:
- """Read the resource content for background task execution.
-
- Reads the resource via the wrapped server and returns the ResourceResult.
- This method is called by Docket during background task execution.
- """
- # Expand the original template with arguments to get internal URI
- original_uri = expand_uri_template(self._original_uri_template or "", arguments)
-
- # Pass exact version so child reads the correct version
- version = VersionSpec(eq=self.version) if self.version else None
-
- # Read from the wrapped server
- result = await self._server.read_resource(original_uri, version=version)
- if isinstance(result, mcp_types.CreateTaskResult):
- raise RuntimeError("Unexpected CreateTaskResult during Docket execution")
-
- return result
-
- def register_with_docket(self, docket: Docket) -> None:
- """No-op: the child's actual template is registered via get_tasks()."""
-
- async def add_to_docket(
- self,
- docket: Docket,
- params: dict[str, Any],
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this template for background execution via docket.
-
- The child's FunctionResourceTemplate.fn is registered (via get_tasks),
- and it expects splatted **kwargs, so we splat params here.
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)(**params)
+ return await self._server.read_resource(original_uri, version=version)
def get_span_attributes(self) -> dict[str, Any]:
return super().get_span_attributes() | {
diff --git a/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py b/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py
index 0c1340ef5..56c55109a 100644
--- a/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py
+++ b/fastmcp_slim/fastmcp/server/providers/filesystem_discovery.py
@@ -349,7 +349,6 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
)
components.append(tool)
elif isinstance(meta, ResourceMeta):
- resolved_task = meta.task if meta.task is not None else False
has_uri_params = "{" in meta.uri and "}" in meta.uri
wrapper_fn = without_injected_parameters(obj)
has_func_params = bool(inspect.signature(wrapper_fn).parameters)
@@ -367,7 +366,6 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
tags=meta.tags,
annotations=meta.annotations,
meta=meta.meta,
- task=resolved_task,
auth=meta.auth,
)
else:
@@ -383,12 +381,10 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
tags=meta.tags,
annotations=meta.annotations,
meta=meta.meta,
- task=resolved_task,
auth=meta.auth,
)
components.append(resource)
elif isinstance(meta, PromptMeta):
- resolved_task = meta.task if meta.task is not None else False
prompt = Prompt.from_function(
obj,
name=meta.name,
@@ -398,7 +394,6 @@ def extract_components(module: ModuleType) -> list[FastMCPComponent]:
icons=meta.icons,
tags=meta.tags,
meta=meta.meta,
- task=resolved_task,
auth=meta.auth,
)
components.append(prompt)
diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py
index ba1621875..53e4a7080 100644
--- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py
+++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py
@@ -16,7 +16,6 @@ import mcp_types
from fastmcp.prompts.base import Prompt
from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.server.auth.authorization import AuthCheck
-from fastmcp.server.tasks.config import TaskConfig
from fastmcp.utilities.types import AnyFunction
if TYPE_CHECKING:
@@ -45,7 +44,6 @@ class PromptDecoratorMixin:
meta = get_fastmcp_meta(prompt)
if meta is not None and isinstance(meta, PromptMeta):
- resolved_task = meta.task if meta.task is not None else False
enabled = meta.enabled
prompt = Prompt.from_function(
prompt,
@@ -56,7 +54,6 @@ class PromptDecoratorMixin:
icons=meta.icons,
tags=meta.tags,
meta=meta.meta,
- task=resolved_task,
auth=meta.auth,
)
else:
@@ -82,7 +79,6 @@ class PromptDecoratorMixin:
tags: set[str] | None = None,
enabled: bool = True,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> F: ...
@@ -99,7 +95,6 @@ class PromptDecoratorMixin:
tags: set[str] | None = None,
enabled: bool = True,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[F], F]: ...
@@ -115,7 +110,6 @@ class PromptDecoratorMixin:
tags: set[str] | None = None,
enabled: bool = True,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> (
Callable[[AnyFunction], FunctionPrompt]
@@ -140,7 +134,6 @@ class PromptDecoratorMixin:
tags: Optional set of tags for categorizing the prompt
enabled: Whether the prompt is enabled (default True). If False, adds to blocklist.
meta: Optional meta information about the prompt
- task: Optional task configuration for background execution
auth: Optional authorization checks for the prompt
Returns:
@@ -198,7 +191,6 @@ class PromptDecoratorMixin:
icons=icons,
tags=tags,
meta=meta,
- task=task,
auth=auth,
enabled=enabled,
)
@@ -232,6 +224,5 @@ class PromptDecoratorMixin:
tags=tags,
meta=meta,
enabled=enabled,
- task=task,
auth=auth,
)
diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py
index 75d23a967..477833c11 100644
--- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py
+++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py
@@ -21,7 +21,6 @@ from fastmcp.resources.security import (
)
from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.auth.authorization import AuthCheck
-from fastmcp.server.tasks.config import TaskConfig
from fastmcp.utilities.types import AnyFunction
if TYPE_CHECKING:
@@ -54,7 +53,6 @@ class ResourceDecoratorMixin:
meta = get_fastmcp_meta(resource)
if meta is not None and isinstance(meta, ResourceMeta):
- resolved_task = meta.task if meta.task is not None else False
enabled = meta.enabled
has_uri_params = "{" in meta.uri and "}" in meta.uri
wrapper_fn = without_injected_parameters(resource)
@@ -73,7 +71,6 @@ class ResourceDecoratorMixin:
tags=meta.tags,
annotations=meta.annotations,
meta=meta.meta,
- task=resolved_task,
auth=meta.auth,
security=meta.security,
)
@@ -90,7 +87,6 @@ class ResourceDecoratorMixin:
tags=meta.tags,
annotations=meta.annotations,
meta=meta.meta,
- task=resolved_task,
auth=meta.auth,
)
else:
@@ -123,7 +119,6 @@ class ResourceDecoratorMixin:
enabled: bool = True,
annotations: Annotations | dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY,
) -> Callable[[F], F]:
@@ -143,7 +138,6 @@ class ResourceDecoratorMixin:
enabled: Whether the resource is enabled (default True). If False, adds to blocklist.
annotations: Optional annotations about the resource's behavior
meta: Optional meta information about the resource
- task: Optional task configuration for background execution
auth: Optional authorization checks for the resource
Returns:
@@ -206,7 +200,6 @@ class ResourceDecoratorMixin:
mime_type=mime_type,
annotations=annotations,
meta=meta,
- task=task,
auth=auth,
enabled=enabled,
security=security,
diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py
index dcf1c0a2d..cc82b7dec 100644
--- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py
+++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py
@@ -26,9 +26,9 @@ import mcp_types
from mcp_types import ToolAnnotations
from fastmcp.server.auth.authorization import AuthCheck
-from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.base import Tool
from fastmcp.tools.function_tool import FunctionTool
+from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import AnyFunction, NotSet, NotSetT
try:
diff --git a/fastmcp_slim/fastmcp/server/providers/openapi/components.py b/fastmcp_slim/fastmcp/server/providers/openapi/components.py
index 3b981226e..3cb36abcf 100644
--- a/fastmcp_slim/fastmcp/server/providers/openapi/components.py
+++ b/fastmcp_slim/fastmcp/server/providers/openapi/components.py
@@ -17,7 +17,6 @@ from fastmcp.resources import (
ResourceTemplate,
)
from fastmcp.server.dependencies import get_http_headers
-from fastmcp.server.tasks.config import TaskConfig
from fastmcp.tools.base import Tool, ToolResult
from fastmcp.utilities.exceptions import (
HTTP_STATUS_ERRORS,
@@ -27,6 +26,7 @@ from fastmcp.utilities.exceptions import (
from fastmcp.utilities.logging import get_logger
from fastmcp.utilities.openapi import HTTPRoute
from fastmcp.utilities.openapi.director import RequestDirector
+from fastmcp.utilities.tasks import TaskConfig
if TYPE_CHECKING:
from fastmcp.server import Context
diff --git a/fastmcp_slim/fastmcp/server/providers/proxy.py b/fastmcp_slim/fastmcp/server/providers/proxy.py
index 78c45d434..866322177 100644
--- a/fastmcp_slim/fastmcp/server/providers/proxy.py
+++ b/fastmcp_slim/fastmcp/server/providers/proxy.py
@@ -51,11 +51,11 @@ from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.server.providers.aggregate import ProviderErrorStrategy
from fastmcp.server.providers.base import Provider
from fastmcp.server.server import FastMCP
-from fastmcp.server.tasks.config import TaskConfig
from fastmcp.telemetry import inject_trace_context
from fastmcp.tools.base import InputRequiredToolResult, Tool, ToolResult
from fastmcp.utilities.components import FastMCPComponent, get_fastmcp_metadata
from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.versions import VersionSpec, version_sort_key
if TYPE_CHECKING:
diff --git a/fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py b/fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py
index 0550e9042..747fecb1c 100644
--- a/fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py
+++ b/fastmcp_slim/fastmcp/server/providers/skills/skill_provider.py
@@ -97,16 +97,12 @@ class SkillFileTemplate(ResourceTemplate):
else:
return full_path.read_bytes()
- async def _read( # type: ignore[override]
+ async def _read(
self,
uri: str,
params: dict[str, Any],
- task_meta: Any = None,
- ) -> ResourceResult: # ty:ignore[invalid-method-override]
- """Server entry point - read file directly without creating ephemeral resource.
-
- Note: task_meta is ignored - this template doesn't support background tasks.
- """
+ ) -> ResourceResult:
+ """Server entry point - read file directly without creating ephemeral resource."""
# Call read() directly and convert to ResourceResult
result = await self.read(arguments=params)
return self.convert_result(result)
diff --git a/fastmcp_slim/fastmcp/server/server.py b/fastmcp_slim/fastmcp/server/server.py
index fce62550b..1308013ad 100644
--- a/fastmcp_slim/fastmcp/server/server.py
+++ b/fastmcp_slim/fastmcp/server/server.py
@@ -14,7 +14,6 @@ from contextlib import (
AbstractAsyncContextManager,
asynccontextmanager,
)
-from dataclasses import replace
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, cast, overload
@@ -80,7 +79,6 @@ from fastmcp.server.middleware.middleware import (
from fastmcp.server.mixins import LifespanMixin, MCPOperationsMixin, TransportMixin
from fastmcp.server.providers import LocalProvider, Provider
from fastmcp.server.providers.aggregate import AggregateProvider
-from fastmcp.server.tasks.config import TaskConfig, TaskMeta
from fastmcp.server.telemetry import server_span
from fastmcp.server.transforms import (
ToolTransform,
@@ -94,6 +92,7 @@ from fastmcp.tools.tool_transform import ToolTransformConfig
from fastmcp.utilities.components import FastMCPComponent, _coerce_version
from fastmcp.utilities.exceptions import HTTP_STATUS_ERRORS, TIMEOUT_ERRORS
from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import AnyFunction, FastMCPBaseModel, NotSet, NotSetT
from fastmcp.utilities.versions import (
VersionSpec,
@@ -1316,7 +1315,6 @@ class FastMCP(
return None
return max(authorized, key=version_sort_key)
- @overload
async def call_tool(
self,
name: str,
@@ -1324,29 +1322,7 @@ class FastMCP(
*,
version: VersionSpec | None = None,
run_middleware: bool = True,
- task_meta: None = None,
- ) -> ToolResult: ...
-
- @overload
- async def call_tool(
- self,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: VersionSpec | None = None,
- run_middleware: bool = True,
- task_meta: TaskMeta,
- ) -> mcp_types.CreateTaskResult: ...
-
- async def call_tool(
- self,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: VersionSpec | None = None,
- run_middleware: bool = True,
- task_meta: TaskMeta | None = None,
- ) -> ToolResult | mcp_types.CreateTaskResult:
+ ) -> ToolResult:
"""Call a tool by name.
This is the public API for executing tools. By default, middleware is applied.
@@ -1357,13 +1333,9 @@ class FastMCP(
version: Specific version to call. If None, calls highest version.
run_middleware: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
- task_meta: If provided, execute as a background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return ToolResult.
Returns:
- ToolResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
+ ToolResult.
A guard tool that requests client input (SEP-2322 multi-round-trip)
returns an ``InputRequiredToolResult`` (a ``ToolResult`` subclass); it
@@ -1425,7 +1397,6 @@ class FastMCP(
context.message.arguments or {},
version=version,
run_middleware=False,
- task_meta=task_meta,
)
),
)
@@ -1467,10 +1438,8 @@ class FastMCP(
if tool is None:
raise NotFoundError(f"Unknown tool: {name!r}")
span.set_attributes(tool.get_span_attributes())
- if task_meta is not None and task_meta.fn_key is None:
- task_meta = replace(task_meta, fn_key=tool.key)
try:
- return await tool._run(arguments or {}, task_meta=task_meta)
+ return await tool._run(arguments or {})
except ValidationError as e:
# Argument-validation failure (a bad call). FunctionTool
# converts pydantic's call-validation error into fastmcp's
@@ -1521,34 +1490,13 @@ class FastMCP(
raise ToolError(f"Error calling tool {name!r}") from e
raise ToolError(f"Error calling tool {name!r}: {e}") from e
- @overload
async def read_resource(
self,
uri: str,
*,
version: VersionSpec | None = None,
run_middleware: bool = True,
- task_meta: None = None,
- ) -> ResourceResult: ...
-
- @overload
- async def read_resource(
- self,
- uri: str,
- *,
- version: VersionSpec | None = None,
- run_middleware: bool = True,
- task_meta: TaskMeta,
- ) -> mcp_types.CreateTaskResult: ...
-
- async def read_resource(
- self,
- uri: str,
- *,
- version: VersionSpec | None = None,
- run_middleware: bool = True,
- task_meta: TaskMeta | None = None,
- ) -> ResourceResult | mcp_types.CreateTaskResult:
+ ) -> ResourceResult:
"""Read a resource by URI.
This is the public API for reading resources. By default, middleware is applied.
@@ -1559,25 +1507,14 @@ class FastMCP(
version: Specific version to read. If None, reads highest version.
run_middleware: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
- task_meta: If provided, execute as a background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return ResourceResult.
Returns:
- ResourceResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
+ ResourceResult.
Raises:
NotFoundError: If resource not found or disabled
ResourceError: If resource read fails
"""
- # Note: fn_key enrichment happens here after finding the resource/template.
- # Resources and templates use different key formats:
- # - Resources use resource.key (derived from the concrete URI)
- # - Templates use template.key (the template pattern)
- # For mounted servers, the parent's provider sets fn_key to the
- # namespaced key before delegating, ensuring correct Docket routing.
-
async with fastmcp.server.context.Context(fastmcp=self) as ctx:
if run_middleware:
mw_context = MiddlewareContext(
@@ -1596,7 +1533,6 @@ class FastMCP(
str(context.message.uri),
version=version,
run_middleware=False,
- task_meta=task_meta,
),
)
@@ -1619,16 +1555,14 @@ class FastMCP(
synthesized = await synthesize_prefab_resource_by_uri(self, uri)
if synthesized is not None:
span.set_attributes(synthesized.get_span_attributes())
- return await synthesized._read(task_meta=task_meta)
+ return await synthesized._read()
# Try concrete resources first (transforms + auth via _get_resource)
resource = await self.get_resource(uri, version=version)
if resource is not None:
span.set_attributes(resource.get_span_attributes())
- if task_meta is not None and task_meta.fn_key is None:
- task_meta = replace(task_meta, fn_key=resource.key)
try:
- return await resource._read(task_meta=task_meta)
+ return await resource._read()
except FastMCPError as e:
logger.log(
e.log_level,
@@ -1692,10 +1626,8 @@ class FastMCP(
)
raise ResourceSecurityError(f"Unknown resource: {uri!r}")
- if task_meta is not None and task_meta.fn_key is None:
- task_meta = replace(task_meta, fn_key=template.key)
try:
- return await template._read(uri, params, task_meta=task_meta)
+ return await template._read(uri, params)
except FastMCPError as e:
logger.log(
e.log_level, f"Error reading resource {uri!r}", exc_info=True
@@ -1724,7 +1656,6 @@ class FastMCP(
raise ResourceError(f"Error reading resource {uri!r}") from e
raise ResourceError(f"Error reading resource {uri!r}: {e}") from e
- @overload
async def render_prompt(
self,
name: str,
@@ -1732,29 +1663,7 @@ class FastMCP(
*,
version: VersionSpec | None = None,
run_middleware: bool = True,
- task_meta: None = None,
- ) -> PromptResult: ...
-
- @overload
- async def render_prompt(
- self,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: VersionSpec | None = None,
- run_middleware: bool = True,
- task_meta: TaskMeta,
- ) -> mcp_types.CreateTaskResult: ...
-
- async def render_prompt(
- self,
- name: str,
- arguments: dict[str, Any] | None = None,
- *,
- version: VersionSpec | None = None,
- run_middleware: bool = True,
- task_meta: TaskMeta | None = None,
- ) -> PromptResult | mcp_types.CreateTaskResult:
+ ) -> PromptResult:
"""Render a prompt by name.
This is the public API for rendering prompts. By default, middleware is applied.
@@ -1766,13 +1675,9 @@ class FastMCP(
version: Specific version to render. If None, renders highest version.
run_middleware: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
- task_meta: If provided, execute as a background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return PromptResult.
Returns:
- PromptResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
+ PromptResult.
Raises:
NotFoundError: If prompt not found or disabled
@@ -1798,7 +1703,6 @@ class FastMCP(
context.message.arguments,
version=version,
run_middleware=False,
- task_meta=task_meta,
),
)
@@ -1816,10 +1720,8 @@ class FastMCP(
if prompt is None:
raise NotFoundError(f"Unknown prompt: {name!r}")
span.set_attributes(prompt.get_span_attributes())
- if task_meta is not None and task_meta.fn_key is None:
- task_meta = replace(task_meta, fn_key=prompt.key)
try:
- return await prompt._render(arguments, task_meta=task_meta)
+ return await prompt._render(arguments)
except FastMCPError as e:
logger.log(
e.log_level, f"Error rendering prompt {name!r}", exc_info=True
@@ -2025,7 +1927,6 @@ class FastMCP(
annotations: Annotations | dict[str, Any] | None = None,
meta: dict[str, Any] | None = None,
app: AppConfig | dict[str, Any] | bool | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
security: ResourceSecurity | None | InheritSecurity = INHERIT_SECURITY,
) -> Callable[[F], F]:
@@ -2125,7 +2026,6 @@ class FastMCP(
tags=tags,
annotations=annotations,
meta=meta,
- task=task if task is not None else self._support_tasks_by_default,
auth=auth,
security=security,
)
@@ -2155,7 +2055,6 @@ class FastMCP(
icons: list[mcp_types.Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> F: ...
@@ -2171,7 +2070,6 @@ class FastMCP(
icons: list[mcp_types.Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> Callable[[F], F]: ...
@@ -2186,7 +2084,6 @@ class FastMCP(
icons: list[mcp_types.Icon] | None = None,
tags: set[str] | None = None,
meta: dict[str, Any] | None = None,
- task: bool | TaskConfig | None = None,
auth: AuthCheck | list[AuthCheck] | None = None,
) -> (
Callable[[AnyFunction], FunctionPrompt]
@@ -2271,7 +2168,6 @@ class FastMCP(
icons=icons,
tags=tags,
meta=meta,
- task=task if task is not None else self._support_tasks_by_default,
auth=auth,
)
diff --git a/fastmcp_slim/fastmcp/server/tasks/__init__.py b/fastmcp_slim/fastmcp/server/tasks/__init__.py
deleted file mode 100644
index 008332db5..000000000
--- a/fastmcp_slim/fastmcp/server/tasks/__init__.py
+++ /dev/null
@@ -1,38 +0,0 @@
-"""MCP SEP-1686 background tasks support.
-
-This module implements protocol-level background task execution for MCP servers.
-"""
-
-from fastmcp.server.tasks.capabilities import get_task_capabilities
-from fastmcp.server.tasks.config import TaskConfig, TaskMeta, TaskMode
-from fastmcp.server.tasks.elicitation import (
- elicit_for_task,
- handle_task_input,
- relay_elicitation,
-)
-from fastmcp.server.tasks.keys import (
- build_task_key,
- get_client_task_id_from_key,
- parse_task_key,
-)
-from fastmcp.server.tasks.notifications import (
- ensure_subscriber_running,
- push_notification,
- stop_subscriber,
-)
-
-__all__ = [
- "TaskConfig",
- "TaskMeta",
- "TaskMode",
- "build_task_key",
- "elicit_for_task",
- "ensure_subscriber_running",
- "get_client_task_id_from_key",
- "get_task_capabilities",
- "handle_task_input",
- "parse_task_key",
- "push_notification",
- "relay_elicitation",
- "stop_subscriber",
-]
diff --git a/fastmcp_slim/fastmcp/server/tasks/config.py b/fastmcp_slim/fastmcp/server/tasks/config.py
deleted file mode 100644
index b7fe2c50b..000000000
--- a/fastmcp_slim/fastmcp/server/tasks/config.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""Backward-compatible exports for task configuration primitives."""
-
-from fastmcp.utilities.tasks import (
- DEFAULT_POLL_INTERVAL,
- DEFAULT_POLL_INTERVAL_MS,
- DEFAULT_TTL_MS,
- TaskConfig,
- TaskMeta,
- TaskMode,
-)
-
-__all__ = [
- "DEFAULT_POLL_INTERVAL",
- "DEFAULT_POLL_INTERVAL_MS",
- "DEFAULT_TTL_MS",
- "TaskConfig",
- "TaskMeta",
- "TaskMode",
-]
diff --git a/fastmcp_slim/fastmcp/settings.py b/fastmcp_slim/fastmcp/settings.py
index 312309a52..17cd884c8 100644
--- a/fastmcp_slim/fastmcp/settings.py
+++ b/fastmcp_slim/fastmcp/settings.py
@@ -2,7 +2,6 @@ from __future__ import annotations as _annotations
import inspect
import os
-from datetime import timedelta
from pathlib import Path
from typing import Annotated, Any, Literal
@@ -30,109 +29,6 @@ DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
TEN_MB_IN_BYTES = 1024 * 1024 * 10
-class DocketSettings(BaseSettings):
- """Docket worker configuration."""
-
- model_config = SettingsConfigDict(
- env_prefix="FASTMCP_DOCKET_",
- extra="ignore",
- )
-
- name: Annotated[
- str,
- Field(
- description=inspect.cleandoc(
- """
- Name for the Docket queue. All servers/workers sharing the same name
- and backend URL will share a task queue.
- """
- ),
- ),
- ] = "fastmcp"
-
- url: Annotated[
- str,
- Field(
- description=inspect.cleandoc(
- """
- URL for the Docket backend. Supports:
- - memory:// - In-memory backend (single process only)
- - redis://host:port/db - Redis/Valkey backend (distributed, multi-process)
-
- Example: redis://localhost:6379/0
-
- Default is memory:// for single-process scenarios. Use Redis or Valkey
- when coordinating tasks across multiple processes (e.g., additional
- workers via the fastmcp tasks CLI).
- """
- ),
- ),
- ] = "memory://"
-
- worker_name: Annotated[
- str | None,
- Field(
- description=inspect.cleandoc(
- """
- Name for the Docket worker. If None, Docket will auto-generate
- a unique worker name.
- """
- ),
- ),
- ] = None
-
- concurrency: Annotated[
- int,
- Field(
- description=inspect.cleandoc(
- """
- Maximum number of tasks the worker can process concurrently.
- """
- ),
- ),
- ] = 10
-
- redelivery_timeout: Annotated[
- timedelta,
- Field(
- description=inspect.cleandoc(
- """
- Task redelivery timeout. If a worker doesn't complete
- a task within this time, the task will be redelivered to another
- worker.
- """
- ),
- ),
- ] = timedelta(seconds=300)
-
- reconnection_delay: Annotated[
- timedelta,
- Field(
- description=inspect.cleandoc(
- """
- Delay between reconnection attempts when the worker
- loses connection to the Docket backend.
- """
- ),
- ),
- ] = timedelta(seconds=5)
-
- minimum_check_interval: Annotated[
- timedelta,
- Field(
- description=inspect.cleandoc(
- """
- How frequently the worker polls for new tasks. Lower
- values reduce latency for task pickup at the cost of
- more CPU usage. The default of 50ms is a good balance;
- increase for high-volume production deployments where
- tasks are long-running.
- """
- ),
- ),
- ] = timedelta(milliseconds=50)
-
-
class Settings(BaseSettings):
"""FastMCP settings."""
@@ -185,8 +81,6 @@ class Settings(BaseSettings):
return v.upper()
return v
- docket: DocketSettings = DocketSettings()
-
enable_rich_logging: Annotated[
bool,
Field(
@@ -287,6 +181,8 @@ class Settings(BaseSettings):
),
] = 5
+ # May move to the fastmcp-tasks package alongside the client task senders
+ # when client task support is rebuilt on the SEP-2663 extension.
client_task_poll_interval: Annotated[
float,
Field(
diff --git a/fastmcp_slim/fastmcp/tools/base.py b/fastmcp_slim/fastmcp/tools/base.py
index 9a5f9c6a2..e41a39583 100644
--- a/fastmcp_slim/fastmcp/tools/base.py
+++ b/fastmcp_slim/fastmcp/tools/base.py
@@ -6,7 +6,6 @@ from typing import (
Annotated,
Any,
ClassVar,
- overload,
)
import mcp_types
@@ -27,7 +26,7 @@ from pydantic.json_schema import SkipJsonSchema
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.components import FastMCPComponent
from fastmcp.utilities.logging import get_logger
-from fastmcp.utilities.tasks import TaskConfig, TaskMeta
+from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import (
Audio,
File,
@@ -45,9 +44,6 @@ except ImportError:
_HAS_PREFAB = False
if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
-
from fastmcp.tools.function_tool import FunctionTool
from fastmcp.tools.tool_transform import ArgTransform, TransformedTool
@@ -396,87 +392,15 @@ class Tool(FastMCPComponent):
meta={"fastmcp": {"wrap_result": True}} if wrap_result else None,
)
- @overload
- async def _run(
- self,
- arguments: dict[str, Any],
- task_meta: None = None,
- ) -> ToolResult: ...
+ async def _run(self, arguments: dict[str, Any]) -> ToolResult:
+ """Server entry point for tool execution.
- @overload
- async def _run(
- self,
- arguments: dict[str, Any],
- task_meta: TaskMeta,
- ) -> mcp_types.CreateTaskResult: ...
-
- async def _run(
- self,
- arguments: dict[str, Any],
- task_meta: TaskMeta | None = None,
- ) -> ToolResult | mcp_types.CreateTaskResult:
- """Server entry point that handles task routing.
-
- This allows ANY Tool subclass to support background execution by setting
- task_config.mode to "supported" or "required". The server calls this
- method instead of run() directly.
-
- Args:
- arguments: Tool arguments
- task_meta: If provided, execute as background task and return
- CreateTaskResult. If None (default), execute synchronously and
- return ToolResult.
-
- Returns:
- ToolResult when task_meta is None.
- CreateTaskResult when task_meta is provided.
-
- Subclasses can override this to customize task routing behavior.
- For example, FastMCPProviderTool overrides to delegate to child
- middleware without submitting to Docket.
+ The server calls this method instead of ``run()`` directly so that
+ subclasses can customize dispatch. For example, ``FastMCPProviderTool``
+ overrides this to delegate to child-server middleware.
"""
- from fastmcp.server.tasks.routing import check_background_task
-
- task_result = await check_background_task(
- component=self,
- task_type="tool",
- arguments=arguments,
- task_meta=task_meta,
- )
- if task_result:
- return task_result
-
return await self.run(arguments)
- def register_with_docket(self, docket: Docket) -> None:
- """Register this tool with docket for background execution."""
- if not self.task_config.supports_tasks():
- return
- docket.register(self.run, names=[self.key])
-
- async def add_to_docket( # type: ignore[override]
- self,
- docket: Docket,
- arguments: dict[str, Any],
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this tool for background execution via docket.
-
- Args:
- docket: The Docket instance
- arguments: Tool arguments
- fn_key: Function lookup key in Docket registry (defaults to self.key)
- task_key: Redis storage key for the result
- **kwargs: Additional kwargs passed to docket.add()
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)(arguments)
-
@classmethod
def from_tool(
cls,
diff --git a/fastmcp_slim/fastmcp/tools/function_tool.py b/fastmcp_slim/fastmcp/tools/function_tool.py
index 70719d3f9..1e2e2b6a2 100644
--- a/fastmcp_slim/fastmcp/tools/function_tool.py
+++ b/fastmcp_slim/fastmcp/tools/function_tool.py
@@ -10,7 +10,6 @@ from dataclasses import dataclass, field
from functools import lru_cache
from types import MethodType
from typing import (
- TYPE_CHECKING,
Annotated,
Any,
Literal,
@@ -53,10 +52,6 @@ from fastmcp.utilities.types import (
logger = get_logger(__name__)
-if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
-
class _ToolBodyError(Exception):
"""Marks a ``pydantic.ValidationError`` raised while executing a tool's body.
@@ -496,88 +491,6 @@ class FunctionTool(Tool):
return list(result)
return result
- def register_with_docket(self, docket: Docket) -> None:
- """Register this tool with docket for background execution.
-
- Registers the raw function so Docket sees and resolves ALL
- dependencies — both FastMCP's (CurrentContext, Progress) and
- Docket-native ones (Retry, Timeout, ConcurrencyLimit).
- """
- if not self.task_config.supports_tasks():
- return
- docket.register(self.fn, names=[self.key])
-
- async def add_to_docket(
- self,
- docket: Docket,
- arguments: dict[str, Any],
- *,
- fn_key: str | None = None,
- task_key: str | None = None,
- **kwargs: Any,
- ) -> Execution:
- """Schedule this tool for background execution via docket.
-
- FunctionTool splats the arguments dict since .fn expects **kwargs.
-
- Args:
- docket: The Docket instance
- arguments: Tool arguments
- fn_key: Function lookup key in Docket registry (defaults to self.key)
- task_key: Redis storage key for the result
- **kwargs: Additional kwargs passed to docket.add()
- """
- lookup_key = fn_key or self.key
- if task_key:
- kwargs["key"] = task_key
- return await docket.add(lookup_key, **kwargs)(**arguments)
-
- def coerce_task_arguments(
- self, arguments: dict[str, Any], *, strict: bool = False
- ) -> dict[str, Any]:
- """Validate client arguments against their declared parameter types.
-
- The synchronous ``run()`` path validates arguments through the
- function's Pydantic TypeAdapter, so a parameter typed as a model
- arrives as a model instance. The task path hands the raw arguments to
- Docket, which binds them to the function signature without coercion —
- so without this a model-typed parameter would reach the function as a
- raw dict (#4349). ``submit_to_docket`` calls this up front so coerced
- values are what get queued, and validation errors surface before any
- task state is created. Coerced values survive the trip to the worker
- because Docket serializes task arguments with cloudpickle.
-
- ``strict`` mirrors the synchronous path's ``strict_input_validation``
- handling: when set, arguments are validated in strict mode so lax
- coercions (e.g. the string ``"1"`` into an ``int``) are rejected at
- submission rather than silently coerced and queued.
-
- Injected dependency parameters (Context, Depends()) are excluded via
- the same wrapper used by the synchronous path, so only client-supplied
- arguments are coerced and Docket's dependency resolution is untouched.
- """
- from fastmcp.server.dependencies import without_injected_parameters
-
- wrapper_fn = without_injected_parameters(
- self.fn, run_in_thread=self.run_in_thread
- )
- hints = _resolve_param_hints(wrapper_fn)
-
- coerced = dict(arguments)
- for name, value in arguments.items():
- annotation = hints.get(name)
- if annotation is None:
- continue
- adapter = get_cached_typeadapter(annotation)
- try:
- coerced[name] = adapter.validate_python(value, strict=strict)
- except PydanticValidationError as e:
- # Argument coercion failure on the task path is a bad call, just
- # like the synchronous path — surface it as fastmcp's
- # ValidationError so it is classified consistently (see #4128).
- raise ValidationError(str(e), log_level=logging.WARNING) from e
- return coerced
-
@overload
def tool(fn: F) -> F: ...
diff --git a/fastmcp_slim/fastmcp/utilities/components.py b/fastmcp_slim/fastmcp/utilities/components.py
index 0fc8ea4bd..b59ac9b73 100644
--- a/fastmcp_slim/fastmcp/utilities/components.py
+++ b/fastmcp_slim/fastmcp/utilities/components.py
@@ -1,7 +1,7 @@
from __future__ import annotations
from collections.abc import Sequence
-from typing import TYPE_CHECKING, Annotated, Any, ClassVar, TypedDict, cast
+from typing import Annotated, Any, ClassVar, TypedDict, cast
from mcp_types import Icon
from pydantic import BeforeValidator, Field
@@ -10,10 +10,6 @@ from typing_extensions import Self, TypeVar
from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import FastMCPBaseModel
-if TYPE_CHECKING:
- from docket import Docket
- from docket.execution import Execution
-
T = TypeVar("T", default=Any)
@@ -118,7 +114,11 @@ class FastMCPComponent(FastMCPBaseModel):
)
task_config: Annotated[
TaskConfig,
- Field(description="Background task execution configuration (SEP-1686)."),
+ Field(
+ description="Background task execution configuration (SEP-2663). "
+ "Only tools support task execution; other component types always "
+ "carry the default 'forbidden' config."
+ ),
] = Field(default_factory=lambda: TaskConfig(mode="forbidden"))
@classmethod
@@ -224,56 +224,6 @@ class FastMCPComponent(FastMCPBaseModel):
"""Create a copy of the component."""
return self.model_copy()
- def register_with_docket(self, docket: Docket) -> None:
- """Register this component with docket for background execution.
-
- No-ops if task_config.mode is "forbidden". Subclasses override to
- register their callable (self.run, self.read, self.render, or self.fn).
- """
- # Base implementation: no-op (subclasses override)
-
- def coerce_task_arguments(
- self, arguments: dict[str, Any], *, strict: bool = False
- ) -> dict[str, Any]:
- """Validate and coerce task arguments before any task state is created.
-
- Called by ``submit_to_docket`` up front, so invalid inputs raise before
- the task's Redis metadata and initial status notification exist —
- otherwise a coercion failure during queueing would orphan a task the
- client has already observed. The base implementation is a no-op;
- components that splat arguments into a typed Python callable (e.g.
- ``FunctionTool``) override this to mirror the synchronous validation
- path.
-
- When ``strict`` is set (server-level ``strict_input_validation``),
- overrides validate in strict mode so the task path rejects lax
- coercions (e.g. the string ``"1"`` into an ``int``) exactly as the
- synchronous call path does.
- """
- return arguments
-
- async def add_to_docket(
- self, docket: Docket, *args: Any, **kwargs: Any
- ) -> Execution:
- """Schedule this component for background execution via docket.
-
- Subclasses override this to handle their specific calling conventions:
- - Tool: add_to_docket(docket, arguments: dict, **kwargs)
- - Resource: add_to_docket(docket, **kwargs)
- - ResourceTemplate: add_to_docket(docket, params: dict, **kwargs)
- - Prompt: add_to_docket(docket, arguments: dict | None, **kwargs)
-
- The **kwargs are passed through to docket.add() (e.g., key=task_key).
- """
- if not self.task_config.supports_tasks():
- raise RuntimeError(
- f"Cannot add {self.__class__.__name__} '{self.name}' to docket: "
- f"task execution not supported"
- )
- raise NotImplementedError(
- f"{self.__class__.__name__} does not implement add_to_docket()"
- )
-
def get_span_attributes(self) -> dict[str, Any]:
"""Return span attributes for telemetry.
diff --git a/fastmcp_slim/fastmcp/utilities/tasks.py b/fastmcp_slim/fastmcp/utilities/tasks.py
index 0886dacbb..b6cd44e84 100644
--- a/fastmcp_slim/fastmcp/utilities/tasks.py
+++ b/fastmcp_slim/fastmcp/utilities/tasks.py
@@ -13,6 +13,13 @@ from fastmcp.utilities.async_utils import is_coroutine_function
TaskMode = Literal["forbidden", "optional", "required"]
+#: Reverse-DNS identifier of the SEP-2663 tasks extension. A tool declared with
+#: ``task=True`` requires an extension with this identifier to be registered on
+#: the server (``mcp.add_extension(...)``); the ``fastmcp-tasks`` package
+#: provides it. Kept here as pure declaration so core can check for the
+#: extension without importing the tasks package.
+TASKS_EXTENSION_ID = "io.modelcontextprotocol/tasks"
+
DEFAULT_POLL_INTERVAL = timedelta(seconds=5)
DEFAULT_POLL_INTERVAL_MS = int(DEFAULT_POLL_INTERVAL.total_seconds() * 1000)
DEFAULT_TTL_MS = 60_000
@@ -59,10 +66,6 @@ class TaskConfig:
if not self.supports_tasks():
return
- from fastmcp.server.dependencies import require_docket
-
- require_docket(f"`task=True` on function '{name}'")
-
fn_to_check = fn
if (
not inspect.isroutine(fn)
diff --git a/fastmcp_slim/fastmcp/client/mixins/task_management.py b/fastmcp_tasks/fastmcp_tasks/_client_task_management.py
similarity index 99%
rename from fastmcp_slim/fastmcp/client/mixins/task_management.py
rename to fastmcp_tasks/fastmcp_tasks/_client_task_management.py
index 634a47435..8b3617d3f 100644
--- a/fastmcp_slim/fastmcp/client/mixins/task_management.py
+++ b/fastmcp_tasks/fastmcp_tasks/_client_task_management.py
@@ -183,9 +183,9 @@ class ClientTaskManagementMixin:
# Server returned empty - fall back to client-side tracking
tasks = []
- for task_id in list(self._submitted_task_ids)[:limit]:
+ for task_id in list(self._submitted_task_ids)[:limit]: # ty: ignore[unresolved-attribute]
try:
- status = await self.get_task_status(task_id)
+ status = await self.get_task_status(task_id) # ty: ignore[unresolved-attribute]
tasks.append(status.model_dump(by_alias=True))
except MCPError:
# Task may have expired or been deleted, skip it
diff --git a/fastmcp_tasks/fastmcp_tasks/_legacy_wire/__init__.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/__init__.py
new file mode 100644
index 000000000..57bbff5ee
--- /dev/null
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/__init__.py
@@ -0,0 +1,14 @@
+"""SEP-1686 wire layer, moved intact and awaiting Phase 3 adaptation.
+
+Every module in this subpackage is the original SEP-1686-shaped wire code:
+the four CRUD request handlers (`requests.py`), the task-submission handler
+(`handlers.py`), the Docket-subscription status relay (`subscriptions.py`), the
+Redis push relay for elicitation (`elicitation.py`, `notifications.py`), the
+capability declaration (`capabilities.py`), and the mode-routing dispatcher
+(`routing.py`).
+
+It is disconnected from core — nothing wires these handlers onto a server after
+Phase 2. Phase 3 adapts this code in place to the SEP-2663 `tasks/get|update|cancel`
+shape under its ported tests. Do not "improve" it here; the point of keeping it is
+that it embodies operational lessons the rewrite must preserve.
+"""
diff --git a/fastmcp_slim/fastmcp/server/tasks/capabilities.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/capabilities.py
similarity index 86%
rename from fastmcp_slim/fastmcp/server/tasks/capabilities.py
rename to fastmcp_tasks/fastmcp_tasks/_legacy_wire/capabilities.py
index d2ed14ff4..42367668c 100644
--- a/fastmcp_slim/fastmcp/server/tasks/capabilities.py
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/capabilities.py
@@ -31,10 +31,7 @@ def get_task_capabilities() -> ServerTasksCapability | None:
silently run synchronously. Restore them here once the SDK adds task
metadata to those request params.
"""
- # Function-local import to avoid a circular import at module load time:
- # fastmcp.server.tasks.__init__ pulls in this module, and dependencies
- # transitively reaches back into fastmcp.server.tasks.keys.
- from fastmcp.server.dependencies import is_docket_available
+ from fastmcp_tasks.dependencies import is_docket_available
if not is_docket_available():
return None
diff --git a/fastmcp_slim/fastmcp/server/tasks/elicitation.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/elicitation.py
similarity index 98%
rename from fastmcp_slim/fastmcp/server/tasks/elicitation.py
rename to fastmcp_tasks/fastmcp_tasks/_legacy_wire/elicitation.py
index d9a6e6df2..95798e1f1 100644
--- a/fastmcp_slim/fastmcp/server/tasks/elicitation.py
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/elicitation.py
@@ -24,9 +24,9 @@ from typing import TYPE_CHECKING, Any
import mcp_types
from mcp import ServerSession
-from fastmcp.server.tasks.context import get_task_context, get_task_session_id
-from fastmcp.server.tasks.keys import task_redis_prefix
-from fastmcp.server.tasks.notifications import push_notification
+from fastmcp_tasks._legacy_wire.notifications import push_notification
+from fastmcp_tasks.context import get_task_context, get_task_session_id
+from fastmcp_tasks.keys import task_redis_prefix
logger = logging.getLogger(__name__)
diff --git a/fastmcp_slim/fastmcp/server/tasks/handlers.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/handlers.py
similarity index 92%
rename from fastmcp_slim/fastmcp/server/tasks/handlers.py
rename to fastmcp_tasks/fastmcp_tasks/_legacy_wire/handlers.py
index 4b2ac1740..2ff531c66 100644
--- a/fastmcp_slim/fastmcp/server/tasks/handlers.py
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/handlers.py
@@ -15,20 +15,19 @@ import mcp_types
from mcp.shared.exceptions import MCPError
from mcp_types import INTERNAL_ERROR
-from fastmcp.server.dependencies import (
- _current_docket,
- get_context,
-)
-from fastmcp.server.tasks.config import TaskMeta
-from fastmcp.server.tasks.context import (
+from fastmcp.server.dependencies import get_context
+from fastmcp.tools.function_tool import _strict_input_validation
+from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.tasks import TaskMeta
+from fastmcp_tasks.components import add_component_to_docket, coerce_task_arguments
+from fastmcp_tasks.context import (
TaskContextSnapshot,
get_task_scope,
register_task_server,
register_task_session,
)
-from fastmcp.server.tasks.keys import build_task_key, task_redis_prefix
-from fastmcp.tools.function_tool import _strict_input_validation
-from fastmcp.utilities.logging import get_logger
+from fastmcp_tasks.dependencies import _current_docket
+from fastmcp_tasks.keys import build_task_key, task_redis_prefix
if TYPE_CHECKING:
from fastmcp.prompts.base import Prompt
@@ -78,8 +77,8 @@ async def submit_to_docket(
# it does on the synchronous call path — otherwise task=True would bypass
# strict validation entirely.
if arguments is not None:
- arguments = component.coerce_task_arguments(
- arguments, strict=_strict_input_validation()
+ arguments = coerce_task_arguments(
+ component, arguments, strict=_strict_input_validation()
)
# Generate server-side task ID per SEP-1686 final spec (line 375-377)
@@ -185,16 +184,20 @@ async def submit_to_docket(
# `task_key` is the task result key (e.g., "fastmcp:task:{task_scope}:{task_id}:tool:child_multiply")
# Resources don't take arguments; tools/prompts/templates always pass arguments (even if None/empty)
if task_type == "resource":
- await component.add_to_docket(docket, fn_key=key, task_key=task_key) # type: ignore[call-arg] # ty:ignore[missing-argument]
+ await add_component_to_docket(
+ component, docket, None, fn_key=key, task_key=task_key
+ )
else:
- await component.add_to_docket(docket, arguments, fn_key=key, task_key=task_key) # type: ignore[call-arg] # ty:ignore[invalid-argument-type, too-many-positional-arguments]
+ await add_component_to_docket(
+ component, docket, arguments, fn_key=key, task_key=task_key
+ )
# Spawn subscription task to send status notifications (SEP-1686 optional feature).
# SDK v2 constructs a ServerSession per request and exposes no per-connection
# task group, so the subscription runs as a standalone asyncio task that
# outlives the submitting request; it is cancelled when the connection closes.
# Deferred: subscriptions and notifications depend on docket at import time
- from fastmcp.server.tasks.subscriptions import subscribe_to_task_updates
+ from fastmcp_tasks._legacy_wire.subscriptions import subscribe_to_task_updates
subscription_task = asyncio.create_task(
subscribe_to_task_updates(
@@ -218,7 +221,7 @@ async def submit_to_docket(
connection.exit_stack.push_async_callback(_cancel_subscription)
# Deferred: notifications depends on docket at import time
- from fastmcp.server.tasks.notifications import (
+ from fastmcp_tasks._legacy_wire.notifications import (
ensure_subscriber_running,
stop_subscriber,
)
diff --git a/fastmcp_slim/fastmcp/server/tasks/notifications.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/notifications.py
similarity index 99%
rename from fastmcp_slim/fastmcp/server/tasks/notifications.py
rename to fastmcp_tasks/fastmcp_tasks/_legacy_wire/notifications.py
index 9a662cd95..1affd89af 100644
--- a/fastmcp_slim/fastmcp/server/tasks/notifications.py
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/notifications.py
@@ -223,7 +223,7 @@ async def _send_mcp_notification(
)
return
task_scope = related_task["task_scope"]
- from fastmcp.server.tasks.elicitation import relay_elicitation
+ from fastmcp_tasks._legacy_wire.elicitation import relay_elicitation
task = asyncio.create_task(
relay_elicitation(session, task_scope, task_id, elicitation, fastmcp),
diff --git a/fastmcp_slim/fastmcp/server/tasks/requests.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/requests.py
similarity index 98%
rename from fastmcp_slim/fastmcp/server/tasks/requests.py
rename to fastmcp_tasks/fastmcp_tasks/_legacy_wire/requests.py
index d04d1f25d..5f1f72a0f 100644
--- a/fastmcp_slim/fastmcp/server/tasks/requests.py
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/requests.py
@@ -27,11 +27,11 @@ from fastmcp.exceptions import NotFoundError
from fastmcp.prompts.base import Prompt
from fastmcp.resources.base import Resource
from fastmcp.resources.template import ResourceTemplate
-from fastmcp.server.tasks.config import DEFAULT_POLL_INTERVAL_MS, DEFAULT_TTL_MS
-from fastmcp.server.tasks.context import get_task_scope
-from fastmcp.server.tasks.keys import parse_task_key, task_redis_prefix
from fastmcp.tools.base import InputRequiredToolResult, Tool
+from fastmcp.utilities.tasks import DEFAULT_POLL_INTERVAL_MS, DEFAULT_TTL_MS
from fastmcp.utilities.versions import VersionSpec
+from fastmcp_tasks.context import get_task_scope
+from fastmcp_tasks.keys import parse_task_key, task_redis_prefix
if TYPE_CHECKING:
from fastmcp.server.server import FastMCP
diff --git a/fastmcp_slim/fastmcp/server/tasks/routing.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/routing.py
similarity index 95%
rename from fastmcp_slim/fastmcp/server/tasks/routing.py
rename to fastmcp_tasks/fastmcp_tasks/_legacy_wire/routing.py
index 97839eff3..b8d7f0f4d 100644
--- a/fastmcp_slim/fastmcp/server/tasks/routing.py
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/routing.py
@@ -11,8 +11,8 @@ import mcp_types
from mcp.shared.exceptions import MCPError
from mcp_types import METHOD_NOT_FOUND
-from fastmcp.server.tasks.config import TaskMeta
-from fastmcp.server.tasks.handlers import submit_to_docket
+from fastmcp.utilities.tasks import TaskMeta
+from fastmcp_tasks._legacy_wire.handlers import submit_to_docket
if TYPE_CHECKING:
from fastmcp.prompts.base import Prompt
diff --git a/fastmcp_slim/fastmcp/server/tasks/subscriptions.py b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/subscriptions.py
similarity index 98%
rename from fastmcp_slim/fastmcp/server/tasks/subscriptions.py
rename to fastmcp_tasks/fastmcp_tasks/_legacy_wire/subscriptions.py
index f227f9667..05526a6f4 100644
--- a/fastmcp_slim/fastmcp/server/tasks/subscriptions.py
+++ b/fastmcp_tasks/fastmcp_tasks/_legacy_wire/subscriptions.py
@@ -16,10 +16,10 @@ from typing import TYPE_CHECKING
from docket.execution import ExecutionState
from mcp_types import TaskStatusNotification, TaskStatusNotificationParams
-from fastmcp.server.tasks.config import DEFAULT_TTL_MS
-from fastmcp.server.tasks.keys import parse_task_key, task_redis_prefix
-from fastmcp.server.tasks.requests import DOCKET_TO_MCP_STATE
from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.tasks import DEFAULT_TTL_MS
+from fastmcp_tasks._legacy_wire.requests import DOCKET_TO_MCP_STATE
+from fastmcp_tasks.keys import parse_task_key, task_redis_prefix
if TYPE_CHECKING:
from docket import Docket
diff --git a/fastmcp_slim/fastmcp/client/tasks.py b/fastmcp_tasks/fastmcp_tasks/client.py
similarity index 98%
rename from fastmcp_slim/fastmcp/client/tasks.py
rename to fastmcp_tasks/fastmcp_tasks/client.py
index ead182372..5e4c0502d 100644
--- a/fastmcp_slim/fastmcp/client/tasks.py
+++ b/fastmcp_tasks/fastmcp_tasks/client.py
@@ -45,7 +45,7 @@ class TaskNotificationHandler(MessageHandler):
if isinstance(message, TaskStatusNotification):
client = self._client_ref()
if client:
- client._handle_task_status_notification(message)
+ client._handle_task_status_notification(message) # ty: ignore[unresolved-attribute]
await super().dispatch(message)
@@ -205,7 +205,7 @@ class Task(abc.ABC, Generic[TaskResultT]):
return cached
# Query server and cache the result
- self._status_cache = await self._client.get_task_status(self._task_id)
+ self._status_cache = await self._client.get_task_status(self._task_id) # ty: ignore[unresolved-attribute]
return self._status_cache
@abc.abstractmethod
@@ -287,7 +287,7 @@ class Task(abc.ABC, Generic[TaskResultT]):
self._status_event.clear()
except asyncio.TimeoutError:
# Fallback: poll server (notification didn't arrive in time)
- self._status_cache = await self._client.get_task_status(self._task_id)
+ self._status_cache = await self._client.get_task_status(self._task_id) # ty: ignore[unresolved-attribute]
def _next_poll_delay(self, backoff: float) -> tuple[float, float]:
"""Delay before the next fallback poll, plus the backoff for the round after.
@@ -340,7 +340,7 @@ class Task(abc.ABC, Generic[TaskResultT]):
# No server-side task to cancel
return
self._check_client_connected()
- await self._client.cancel_task(self._task_id)
+ await self._client.cancel_task(self._task_id) # ty: ignore[unresolved-attribute]
# Invalidate cache to force fresh status fetch
self._status_cache = None
@@ -426,7 +426,7 @@ class ToolTask(Task["CallToolResult"]):
await self._wait_terminal()
# Get the raw result (dict or CallToolResult)
- raw_result = await self._client.get_task_result(self._task_id)
+ raw_result = await self._client.get_task_result(self._task_id) # ty: ignore[unresolved-attribute]
# Convert to CallToolResult if needed and parse
if isinstance(raw_result, dict):
@@ -523,7 +523,7 @@ class PromptTask(Task[mcp_types.GetPromptResult]):
await self._wait_terminal()
# Get the raw MCP result
- mcp_result = await self._client.get_task_result(self._task_id)
+ mcp_result = await self._client.get_task_result(self._task_id) # ty: ignore[unresolved-attribute]
# Parse as GetPromptResult
result = mcp_types.GetPromptResult.model_validate(mcp_result)
@@ -595,7 +595,7 @@ class ResourceTask(
await self._wait_terminal()
# Get the raw MCP result
- mcp_result = await self._client.get_task_result(self._task_id)
+ mcp_result = await self._client.get_task_result(self._task_id) # ty: ignore[unresolved-attribute]
# Parse as ReadResourceResult or extract contents
if isinstance(mcp_result, mcp_types.ReadResourceResult):
diff --git a/fastmcp_tasks/fastmcp_tasks/components.py b/fastmcp_tasks/fastmcp_tasks/components.py
new file mode 100644
index 000000000..537e6e645
--- /dev/null
+++ b/fastmcp_tasks/fastmcp_tasks/components.py
@@ -0,0 +1,169 @@
+"""Docket-touching component logic relocated from core component classes.
+
+During the SEP-1686 -> SEP-2663 migration the ``register_with_docket`` /
+``add_to_docket`` / ``coerce_task_arguments`` methods were removed from the core
+``FastMCPComponent`` classes (Tool, Resource, ResourceTemplate, Prompt). Their
+bodies are preserved here verbatim as type-dispatched functions so Phase 3 can
+wire them into ``TasksExtension`` without reconstructing the calling conventions.
+
+The functions dispatch on the concrete component type because each type splats
+its arguments differently into the Docket-registered callable:
+
+- ``FunctionTool``/``FunctionResource``/``FunctionResourceTemplate``/``FunctionPrompt``
+ register the raw ``fn`` so Docket resolves ALL dependencies (FastMCP's and
+ Docket-native), and splat their arguments (``**kwargs``) into it.
+- Base ``Tool``/``Resource``/``ResourceTemplate``/``Prompt`` register their
+ ``run``/``read``/``render`` entry point and pass arguments positionally.
+
+Only tools carry a task-capable ``task_config`` after the migration (SEP-2663 is
+tools-only); the resource/prompt/template branches are retained for engine
+completeness and Phase 3's decision, not because core still declares them.
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import TYPE_CHECKING, Any
+
+from pydantic import ValidationError as PydanticValidationError
+
+from fastmcp.exceptions import ValidationError
+from fastmcp.prompts.base import Prompt
+from fastmcp.prompts.function_prompt import FunctionPrompt
+from fastmcp.resources.base import Resource
+from fastmcp.resources.function_resource import FunctionResource
+from fastmcp.resources.template import FunctionResourceTemplate, ResourceTemplate
+from fastmcp.tools.base import Tool
+from fastmcp.tools.function_tool import FunctionTool, _resolve_param_hints
+from fastmcp.utilities.components import FastMCPComponent
+from fastmcp.utilities.types import get_cached_typeadapter
+
+if TYPE_CHECKING:
+ from docket import Docket
+ from docket.execution import Execution
+
+
+def register_component_with_docket(component: FastMCPComponent, docket: Docket) -> None:
+ """Register a component's callable with Docket for background execution.
+
+ No-ops if ``task_config.mode`` is ``forbidden``. Function-backed components
+ register their raw ``fn`` (so Docket resolves all dependencies); base
+ components register their ``run``/``read``/``render`` entry point.
+ """
+ if not component.task_config.supports_tasks():
+ return
+
+ if isinstance(component, FunctionTool):
+ docket.register(component.fn, names=[component.key])
+ elif isinstance(component, Tool):
+ docket.register(component.run, names=[component.key])
+ elif isinstance(component, FunctionResource):
+ docket.register(component.fn, names=[component.key])
+ elif isinstance(component, FunctionResourceTemplate):
+ docket.register(component.fn, names=[component.key])
+ elif isinstance(component, ResourceTemplate):
+ docket.register(component.read, names=[component.key])
+ elif isinstance(component, Resource):
+ docket.register(component.read, names=[component.key])
+ elif isinstance(component, FunctionPrompt):
+ docket.register(component.fn, names=[component.key])
+ elif isinstance(component, Prompt):
+ docket.register(component.render, names=[component.key])
+ else:
+ raise NotImplementedError(
+ f"{type(component).__name__} does not support Docket registration"
+ )
+
+
+async def add_component_to_docket(
+ component: FastMCPComponent,
+ docket: Docket,
+ arguments: dict[str, Any] | None,
+ *,
+ fn_key: str | None = None,
+ task_key: str | None = None,
+ **kwargs: Any,
+) -> Execution:
+ """Schedule a component for background execution via Docket.
+
+ Handles each component type's calling convention:
+
+ - ``FunctionTool``: splats the arguments dict (``.fn`` expects ``**kwargs``).
+ - base ``Tool``: passes the arguments dict positionally.
+ - ``Resource`` (any): no arguments.
+ - ``FunctionResourceTemplate``: splats the params dict.
+ - base ``ResourceTemplate``: passes params positionally.
+ - ``FunctionPrompt``: splats the arguments dict (or empty).
+ - base ``Prompt``: passes arguments positionally.
+ """
+ if not component.task_config.supports_tasks():
+ raise RuntimeError(
+ f"Cannot add {type(component).__name__} '{component.name}' to docket: "
+ f"task execution not supported"
+ )
+
+ lookup_key = fn_key or component.key
+ if task_key:
+ kwargs["key"] = task_key
+ adder = docket.add(lookup_key, **kwargs)
+
+ if isinstance(component, FunctionTool):
+ return await adder(**(arguments or {}))
+ elif isinstance(component, Tool):
+ return await adder(arguments)
+ elif isinstance(component, Resource):
+ return await adder()
+ elif isinstance(component, FunctionResourceTemplate):
+ return await adder(**(arguments or {}))
+ elif isinstance(component, ResourceTemplate):
+ return await adder(arguments)
+ elif isinstance(component, FunctionPrompt):
+ return await adder(**(arguments or {}))
+ elif isinstance(component, Prompt):
+ return await adder(arguments)
+ else:
+ raise NotImplementedError(
+ f"{type(component).__name__} does not implement add_to_docket()"
+ )
+
+
+def coerce_task_arguments(
+ component: FastMCPComponent,
+ arguments: dict[str, Any],
+ *,
+ strict: bool = False,
+) -> dict[str, Any]:
+ """Validate and coerce task arguments before any task state is created.
+
+ Called by ``submit_to_docket`` up front, so invalid inputs raise before the
+ task's Redis metadata and initial status notification exist — otherwise a
+ coercion failure during queueing would orphan a task the client has already
+ observed. Only ``FunctionTool`` splats arguments into a typed Python callable
+ and therefore mirrors the synchronous validation path; every other component
+ type is a no-op passthrough.
+
+ When ``strict`` is set (server-level ``strict_input_validation``), arguments
+ are validated in strict mode so the task path rejects lax coercions (e.g. the
+ string ``"1"`` into an ``int``) exactly as the synchronous call path does.
+ """
+ if not isinstance(component, FunctionTool):
+ return arguments
+
+ from fastmcp.server.dependencies import without_injected_parameters
+
+ wrapper_fn = without_injected_parameters(
+ component.fn, run_in_thread=component.run_in_thread
+ )
+ hints = _resolve_param_hints(wrapper_fn)
+
+ coerced = dict(arguments)
+ for name, value in arguments.items():
+ annotation = hints.get(name)
+ if annotation is None:
+ continue
+ adapter = get_cached_typeadapter(annotation)
+ try:
+ coerced[name] = adapter.validate_python(value, strict=strict)
+ except PydanticValidationError as e:
+ raise ValidationError(str(e), log_level=logging.WARNING) from e
+ return coerced
diff --git a/fastmcp_slim/fastmcp/server/tasks/context.py b/fastmcp_tasks/fastmcp_tasks/context.py
similarity index 98%
rename from fastmcp_slim/fastmcp/server/tasks/context.py
rename to fastmcp_tasks/fastmcp_tasks/context.py
index 8462ad5a7..c18e33309 100644
--- a/fastmcp_slim/fastmcp/server/tasks/context.py
+++ b/fastmcp_tasks/fastmcp_tasks/context.py
@@ -16,7 +16,7 @@ from contextvars import ContextVar
from dataclasses import dataclass
from typing import TYPE_CHECKING
-from fastmcp.server.tasks.keys import parse_task_key, task_redis_prefix
+from fastmcp_tasks.keys import parse_task_key, task_redis_prefix
try:
from docket import TaskKey
@@ -88,7 +88,7 @@ def get_task_context() -> TaskContextInfo | None:
Returns:
TaskContextInfo with task_id and task_scope, or None if not in a task.
"""
- from fastmcp.server.dependencies import is_docket_available
+ from fastmcp_tasks.dependencies import is_docket_available
if not is_docket_available():
return None
@@ -247,7 +247,8 @@ async def restore_task_snapshot(key: str = TaskKey()) -> None:
# Non-fastmcp key (e.g. docket scheduler internals) — nothing to do.
return
- from fastmcp.server.dependencies import _current_docket, get_server
+ from fastmcp.server.dependencies import get_server
+ from fastmcp_tasks.dependencies import _current_docket
try:
docket = get_server()._docket
diff --git a/fastmcp_tasks/fastmcp_tasks/dependencies.py b/fastmcp_tasks/fastmcp_tasks/dependencies.py
new file mode 100644
index 000000000..bb082483d
--- /dev/null
+++ b/fastmcp_tasks/fastmcp_tasks/dependencies.py
@@ -0,0 +1,184 @@
+"""Docket-specific dependency injection for FastMCP background tasks.
+
+Moved out of ``fastmcp.server.dependencies`` during the SEP-1686 -> SEP-2663
+migration. These helpers are all docket-touching: the ``require_docket``
+install-hint, the docket/worker ContextVars, and the ``CurrentDocket`` /
+``CurrentWorker`` dependencies. Everything here is wire-agnostic engine plumbing
+that Phase 3 rewires into ``TasksExtension``.
+
+The generic ``is_docket_available`` probe stays in ``fastmcp.server.dependencies``
+(core's ``Context``/``Progress`` still use it) and is re-exported here for the
+tasks package's callers.
+"""
+
+from __future__ import annotations
+
+import importlib.metadata
+from contextvars import ContextVar
+from types import TracebackType
+from typing import TYPE_CHECKING, cast
+
+from uncalled_for import Dependency
+
+from fastmcp.server.dependencies import (
+ _MIN_DOCKET_VERSION,
+ get_server,
+ is_docket_available,
+)
+
+if TYPE_CHECKING:
+ from docket import Docket
+ from docket.worker import Worker
+
+__all__ = [
+ "CurrentDocket",
+ "CurrentWorker",
+ "is_docket_available",
+ "require_docket",
+]
+
+
+_current_docket: ContextVar[Docket | None] = ContextVar("docket", default=None)
+_current_worker: ContextVar[Worker | None] = ContextVar("worker", default=None)
+
+
+def require_docket(feature: str) -> None:
+ """Raise ImportError with install instructions if docket not available.
+
+ Args:
+ feature: Description of what requires docket (e.g., "`task=True`",
+ "CurrentDocket()"). Will be included in the error message.
+ """
+ if is_docket_available():
+ return
+
+ try:
+ installed = importlib.metadata.version("pydocket")
+ except importlib.metadata.PackageNotFoundError:
+ installed = None
+
+ if installed is None:
+ detail = (
+ "FastMCP background tasks require the `tasks` extra. "
+ "Install with: pip install 'fastmcp[tasks]'."
+ )
+ else:
+ detail = (
+ f"FastMCP background tasks require pydocket>={_MIN_DOCKET_VERSION}, "
+ f"but pydocket {installed} is installed (likely pulled in by another "
+ f"package). Upgrade with: pip install -U 'pydocket>={_MIN_DOCKET_VERSION}'."
+ )
+
+ raise ImportError(f"{detail} (Triggered by {feature})")
+
+
+class _CurrentDocket(Dependency["Docket"]):
+ """Async context manager for Docket dependency."""
+
+ async def __aenter__(self) -> Docket:
+ require_docket("CurrentDocket()")
+ # Check server instance first, fall back to ContextVar for mounted children
+ # whose parent owns the Docket
+ try:
+ docket = get_server()._docket
+ except RuntimeError:
+ docket = None
+ if docket is None:
+ docket = _current_docket.get()
+ if docket is None:
+ raise RuntimeError(
+ "No Docket instance found. Docket is only initialized when there are "
+ "task-enabled components (task=True). Add task=True to a component "
+ "to enable Docket infrastructure."
+ )
+ return docket
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ pass
+
+
+def CurrentDocket() -> Docket:
+ """Get the current Docket instance managed by FastMCP.
+
+ This dependency provides access to the Docket instance that FastMCP
+ automatically creates for background task scheduling.
+
+ Returns:
+ A dependency that resolves to the active Docket instance
+
+ Raises:
+ RuntimeError: If not within a FastMCP server context
+ ImportError: If fastmcp[tasks] not installed
+
+ Example:
+ ```python
+ from fastmcp_tasks.dependencies import CurrentDocket
+
+ @mcp.tool()
+ async def schedule_task(docket: Docket = CurrentDocket()) -> str:
+ await docket.add(some_function)(arg1, arg2)
+ return "Scheduled"
+ ```
+ """
+ require_docket("CurrentDocket()")
+ return cast("Docket", _CurrentDocket())
+
+
+class _CurrentWorker(Dependency["Worker"]):
+ """Async context manager for Worker dependency."""
+
+ async def __aenter__(self) -> Worker:
+ require_docket("CurrentWorker()")
+ # Check server instance first, fall back to ContextVar for mounted children
+ try:
+ worker = get_server()._worker
+ except RuntimeError:
+ worker = None
+ if worker is None:
+ worker = _current_worker.get()
+ if worker is None:
+ raise RuntimeError(
+ "No Worker instance found. Worker is only initialized when there are "
+ "task-enabled components (task=True). Add task=True to a component "
+ "to enable Docket infrastructure."
+ )
+ return worker
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ pass
+
+
+def CurrentWorker() -> Worker:
+ """Get the current Docket Worker instance managed by FastMCP.
+
+ This dependency provides access to the Worker instance that FastMCP
+ automatically creates for background task processing.
+
+ Returns:
+ A dependency that resolves to the active Worker instance
+
+ Raises:
+ RuntimeError: If not within a FastMCP server context
+ ImportError: If fastmcp[tasks] not installed
+
+ Example:
+ ```python
+ from fastmcp_tasks.dependencies import CurrentWorker
+
+ @mcp.tool()
+ async def check_worker_status(worker: Worker = CurrentWorker()) -> str:
+ return f"Worker: {worker.name}"
+ ```
+ """
+ require_docket("CurrentWorker()")
+ return cast("Worker", _CurrentWorker())
diff --git a/fastmcp_slim/fastmcp/server/tasks/keys.py b/fastmcp_tasks/fastmcp_tasks/keys.py
similarity index 100%
rename from fastmcp_slim/fastmcp/server/tasks/keys.py
rename to fastmcp_tasks/fastmcp_tasks/keys.py
diff --git a/fastmcp_tasks/fastmcp_tasks/lifespan.py b/fastmcp_tasks/fastmcp_tasks/lifespan.py
new file mode 100644
index 000000000..45df1a7c5
--- /dev/null
+++ b/fastmcp_tasks/fastmcp_tasks/lifespan.py
@@ -0,0 +1,126 @@
+"""Docket lifecycle for FastMCP background tasks.
+
+Extracted from ``fastmcp.server.mixins.lifespan.LifespanMixin._docket_lifespan``
+during the SEP-1686 -> SEP-2663 migration. The logic — start Docket and a Worker
+at the runtime-tree root when there are task-enabled components, register those
+components' callables, and run the worker with the snapshot-restore dependency —
+is preserved verbatim so Phase 3 can drive it from ``TasksExtension.lifespan()``.
+
+Nothing in core calls this after Phase 2; it is engine code parked here for the
+Phase 3 adapter.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import weakref
+from collections.abc import AsyncIterator
+from contextlib import asynccontextmanager, suppress
+from typing import TYPE_CHECKING, Any
+
+from fastmcp.utilities.logging import get_logger
+
+if TYPE_CHECKING:
+ from fastmcp.server.server import FastMCP
+
+logger = get_logger(__name__)
+
+
+@asynccontextmanager
+async def docket_lifespan(server: FastMCP) -> AsyncIterator[None]:
+ """Manage the Docket instance and Worker for background task execution.
+
+ Docket infrastructure is only initialized if:
+ 1. pydocket is installed (fastmcp[tasks] extra)
+ 2. There are task-enabled components (task_config.mode != 'forbidden')
+
+ Sets ``server._docket`` / ``server._worker`` for the duration and registers
+ each task-enabled component's callable with the Docket, then runs the worker
+ until the context exits.
+ """
+ from docket import Depends, Docket, Worker
+
+ import fastmcp
+ from fastmcp.server.dependencies import _current_server
+ from fastmcp_tasks.components import register_component_with_docket
+ from fastmcp_tasks.context import restore_task_snapshot
+ from fastmcp_tasks.dependencies import (
+ _current_docket,
+ _current_worker,
+ is_docket_available,
+ )
+ from fastmcp_tasks.settings import DocketSettings
+
+ docket_settings = DocketSettings()
+
+ # Set FastMCP server in ContextVar so CurrentFastMCP can access it
+ # (use weakref to avoid reference cycles)
+ server_token = _current_server.set(weakref.ref(server))
+
+ try:
+ if not is_docket_available():
+ yield
+ return
+
+ # Collect task-enabled components at startup with all transforms applied.
+ # Components must be available now to be registered with Docket workers;
+ # dynamically added components after startup won't be registered.
+ try:
+ task_components = list(await server.get_tasks())
+ except Exception as e:
+ logger.warning(f"Failed to get tasks: {e}")
+ if fastmcp.settings.mounted_components_raise_on_load_error:
+ raise
+ task_components = []
+
+ if not task_components:
+ yield
+ return
+
+ async with Docket(
+ name=docket_settings.name,
+ url=docket_settings.url,
+ ) as docket:
+ server._docket = docket
+
+ for component in task_components:
+ register_component_with_docket(component, docket)
+
+ docket_token = _current_docket.set(docket)
+ try:
+ worker_kwargs: dict[str, Any] = {
+ "concurrency": docket_settings.concurrency,
+ "redelivery_timeout": docket_settings.redelivery_timeout,
+ "reconnection_delay": docket_settings.reconnection_delay,
+ "minimum_check_interval": docket_settings.minimum_check_interval,
+ }
+ if docket_settings.worker_name:
+ worker_kwargs["name"] = docket_settings.worker_name
+
+ # Create and start Worker. The restore_task_snapshot worker-level
+ # dependency runs before every task so the per-task snapshot
+ # ContextVar is populated before user code or task-scoped
+ # dependencies observe it.
+ async with Worker(
+ docket,
+ dependencies=[Depends(restore_task_snapshot)],
+ **worker_kwargs,
+ ) as worker:
+ server._worker = worker
+ worker_token = _current_worker.set(worker)
+ try:
+ worker_task = asyncio.create_task(worker.run_forever())
+ try:
+ yield
+ finally:
+ worker_task.cancel()
+ with suppress(asyncio.CancelledError):
+ await worker_task
+ finally:
+ _current_worker.reset(worker_token)
+ server._worker = None
+ finally:
+ _current_docket.reset(docket_token)
+ server._docket = None
+ finally:
+ _current_server.reset(server_token)
diff --git a/fastmcp_tasks/fastmcp_tasks/settings.py b/fastmcp_tasks/fastmcp_tasks/settings.py
new file mode 100644
index 000000000..57b6970a4
--- /dev/null
+++ b/fastmcp_tasks/fastmcp_tasks/settings.py
@@ -0,0 +1,122 @@
+"""Docket worker settings for FastMCP background tasks.
+
+Moved out of ``fastmcp.settings`` during the SEP-1686 -> SEP-2663 migration.
+The ``FASTMCP_DOCKET_*`` environment prefix is unchanged so existing
+deployments keep working. Phase 3 wires this configuration into
+``TasksExtension``.
+"""
+
+from __future__ import annotations
+
+import inspect
+from datetime import timedelta
+from typing import Annotated
+
+from pydantic import Field
+from pydantic_settings import BaseSettings, SettingsConfigDict
+
+
+class DocketSettings(BaseSettings):
+ """Docket worker configuration."""
+
+ model_config = SettingsConfigDict(
+ env_prefix="FASTMCP_DOCKET_",
+ extra="ignore",
+ )
+
+ name: Annotated[
+ str,
+ Field(
+ description=inspect.cleandoc(
+ """
+ Name for the Docket queue. All servers/workers sharing the same name
+ and backend URL will share a task queue.
+ """
+ ),
+ ),
+ ] = "fastmcp"
+
+ url: Annotated[
+ str,
+ Field(
+ description=inspect.cleandoc(
+ """
+ URL for the Docket backend. Supports:
+ - memory:// - In-memory backend (single process only)
+ - redis://host:port/db - Redis/Valkey backend (distributed, multi-process)
+
+ Example: redis://localhost:6379/0
+
+ Default is memory:// for single-process scenarios. Use Redis or Valkey
+ when coordinating tasks across multiple processes (e.g., additional
+ workers via the fastmcp tasks CLI).
+ """
+ ),
+ ),
+ ] = "memory://"
+
+ worker_name: Annotated[
+ str | None,
+ Field(
+ description=inspect.cleandoc(
+ """
+ Name for the Docket worker. If None, Docket will auto-generate
+ a unique worker name.
+ """
+ ),
+ ),
+ ] = None
+
+ concurrency: Annotated[
+ int,
+ Field(
+ description=inspect.cleandoc(
+ """
+ Maximum number of tasks the worker can process concurrently.
+ """
+ ),
+ ),
+ ] = 10
+
+ redelivery_timeout: Annotated[
+ timedelta,
+ Field(
+ description=inspect.cleandoc(
+ """
+ Task redelivery timeout. If a worker doesn't complete
+ a task within this time, the task will be redelivered to another
+ worker.
+ """
+ ),
+ ),
+ ] = timedelta(seconds=300)
+
+ reconnection_delay: Annotated[
+ timedelta,
+ Field(
+ description=inspect.cleandoc(
+ """
+ Delay between reconnection attempts when the worker
+ loses connection to the Docket backend.
+ """
+ ),
+ ),
+ ] = timedelta(seconds=5)
+
+ minimum_check_interval: Annotated[
+ timedelta,
+ Field(
+ description=inspect.cleandoc(
+ """
+ How frequently the worker polls for new tasks. Lower
+ values reduce latency for task pickup at the cost of
+ more CPU usage. The default of 50ms is a good balance;
+ increase for high-volume production deployments where
+ tasks are long-running.
+ """
+ ),
+ ),
+ ] = timedelta(milliseconds=50)
+
+
+docket_settings = DocketSettings()
diff --git a/fastmcp_slim/fastmcp/cli/tasks.py b/fastmcp_tasks/fastmcp_tasks/worker_cli.py
similarity index 91%
rename from fastmcp_slim/fastmcp/cli/tasks.py
rename to fastmcp_tasks/fastmcp_tasks/worker_cli.py
index 23ddc6e58..d39ddfce8 100644
--- a/fastmcp_slim/fastmcp/cli/tasks.py
+++ b/fastmcp_tasks/fastmcp_tasks/worker_cli.py
@@ -9,6 +9,7 @@ from rich.console import Console
from fastmcp.utilities.cli import load_and_merge_config
from fastmcp.utilities.logging import get_logger
+from fastmcp_tasks.settings import docket_settings
logger = get_logger("cli.tasks")
console = Console()
@@ -28,9 +29,7 @@ def check_distributed_backend() -> None:
Raises:
SystemExit: If using memory:// URL
"""
- import fastmcp
-
- docket_url = fastmcp.settings.docket.url
+ docket_url = docket_settings.url
# Check for memory:// URL and provide helpful error
if docket_url.startswith("memory://"):
@@ -76,8 +75,6 @@ def worker(
fastmcp tasks worker server.py
fastmcp tasks worker examples/tasks/server.py
"""
- import fastmcp
-
check_distributed_backend()
# Load server to get task functions
@@ -95,9 +92,9 @@ def worker(
console.print(
f"[bold green]✓[/bold green] Starting worker for [cyan]{server.name}[/cyan]"
)
- console.print(f" Docket: {fastmcp.settings.docket.name}")
- console.print(f" Backend: {fastmcp.settings.docket.url}")
- console.print(f" Concurrency: {fastmcp.settings.docket.concurrency}")
+ console.print(f" Docket: {docket_settings.name}")
+ console.print(f" Backend: {docket_settings.url}")
+ console.print(f" Concurrency: {docket_settings.concurrency}")
# Server's lifespan has started its worker - just camp here forever
while True:
diff --git a/pyproject.toml b/pyproject.toml
index 71bad7539..46174c506 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -155,6 +155,11 @@ exclude = [
"examples/providers/sqlite", # needs aiosqlite
"examples/memory.py", # needs asyncpg, numpy, pydantic_ai, pgvector
"examples/get_file.py", # needs aiohttp
+ # Dormant SEP-1686 task tests: skipped at runtime pending the Phase 3
+ # TasksExtension (SEP-2663). They reference task APIs that are removed from
+ # core and return in the fastmcp-tasks extension, so they don't type-check
+ # against core until then. Drop this exclusion when Phase 3 lands.
+ "tests/tasks",
]
[tool.ty.environment]
diff --git a/tests/cli/test_tasks.py b/tests/cli/test_tasks.py
index 8da5f80c9..a2ea42ede 100644
--- a/tests/cli/test_tasks.py
+++ b/tests/cli/test_tasks.py
@@ -1,10 +1,14 @@
"""Tests for the fastmcp tasks CLI."""
import pytest
+from fastmcp_tasks.worker_cli import check_distributed_backend, tasks_app
-from fastmcp.cli.tasks import check_distributed_backend, tasks_app
from fastmcp.utilities.tests import temporary_settings
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
class TestCheckDistributedBackend:
"""Test the distributed backend checker function."""
diff --git a/tests/client/client/test_client.py b/tests/client/client/test_client.py
index 3cd031c5a..bfe74f68c 100644
--- a/tests/client/client/test_client.py
+++ b/tests/client/client/test_client.py
@@ -7,13 +7,13 @@ from typing import Any, cast
import anyio
import pytest
+from fastmcp_tasks.client import TaskNotificationHandler
from mcp import ClientSession, MCPError
from mcp_types import TextContent
from pydantic import AnyUrl
import fastmcp
from fastmcp.client import Client
-from fastmcp.client.tasks import TaskNotificationHandler
from fastmcp.client.transports import (
ClientTransport,
FastMCPTransport,
@@ -886,22 +886,24 @@ async def test_client_list_dict_return_type():
assert result.data == [{"city": "NYC", "temp": 72}, {"city": "LA", "temp": 85}]
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
def test_client_new_resets_mutable_task_state(fastmcp_server):
"""Client.new() should not share mutable task tracking structures."""
client = Client(transport=FastMCPTransport(fastmcp_server))
- client._task_registry["task-1"] = lambda: None # type: ignore[assignment] # ty:ignore[invalid-assignment]
- client._submitted_task_ids.add("task-1")
+ client._task_registry["task-1"] = lambda: None # type: ignore[assignment] # ty: ignore
+ client._submitted_task_ids.add("task-1") # ty: ignore
clone = client.new()
assert clone is not client
- assert clone._task_registry == {}
- assert clone._submitted_task_ids == set()
- assert clone._task_registry is not client._task_registry
- assert clone._submitted_task_ids is not client._submitted_task_ids
+ assert clone._task_registry == {} # ty: ignore
+ assert clone._submitted_task_ids == set() # ty: ignore
+ assert clone._task_registry is not client._task_registry # ty: ignore
+ assert clone._submitted_task_ids is not client._submitted_task_ids # ty: ignore
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
def test_client_new_rebinds_default_task_notification_handler(fastmcp_server):
"""Client.new() should bind the default task handler to the cloned client."""
client = Client(transport=FastMCPTransport(fastmcp_server))
diff --git a/tests/client/client/test_response_cache.py b/tests/client/client/test_response_cache.py
index 40f583106..1d769ccdc 100644
--- a/tests/client/client/test_response_cache.py
+++ b/tests/client/client/test_response_cache.py
@@ -42,14 +42,11 @@ class TestCacheConstruction:
def test_cache_none_is_disabled_by_default(self):
"""Caching is opt-in: the default `cache=None` builds no cache, so a legacy
connection is byte-identical to pre-v4 behavior (no handler wrapping)."""
- from fastmcp.client.tasks import TaskNotificationHandler
-
client = Client(FastMCP("x"))
assert client._response_cache is None
- # The message handler is the bare default, not a cache-evicting wrapper.
- assert isinstance(
- client._session_kwargs["message_handler"], TaskNotificationHandler
- )
+ # No cache means no cache-evicting wrapper: the message handler is the
+ # bare default (None), not a wrapper.
+ assert client._session_kwargs.get("message_handler") is None
def test_cache_true_builds_default(self):
client = Client(FastMCP("x"), cache=True)
diff --git a/tests/client/tasks/conftest.py b/tests/client/tasks/conftest.py
deleted file mode 100644
index 29d0c9a10..000000000
--- a/tests/client/tasks/conftest.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Configuration for client task tests."""
diff --git a/tests/client/tasks/test_client_prompt_tasks.py b/tests/client/tasks/test_client_prompt_tasks.py
deleted file mode 100644
index 069c44c3b..000000000
--- a/tests/client/tasks/test_client_prompt_tasks.py
+++ /dev/null
@@ -1,108 +0,0 @@
-"""
-Tests for client-side prompt task methods.
-
-Tests the client's get_prompt_as_task method.
-"""
-
-import pytest
-
-from fastmcp import FastMCP
-from fastmcp.client import Client
-from fastmcp.client.tasks import PromptTask
-
-
-@pytest.fixture
-async def prompt_server():
- """Create a test server with background-enabled prompts."""
- mcp = FastMCP("prompt-client-test")
-
- @mcp.prompt(task=True)
- async def analysis_prompt(topic: str, style: str = "formal") -> str:
- """Generate an analysis prompt."""
- return f"Analyze {topic} in a {style} style"
-
- @mcp.prompt(task=True)
- async def creative_prompt(theme: str) -> str:
- """Generate a creative writing prompt."""
- return f"Write a story about {theme}"
-
- return mcp
-
-
-async def test_get_prompt_as_task_returns_prompt_task(prompt_server):
- """get_prompt with task=True returns a PromptTask object."""
- async with Client(prompt_server, mode="legacy") as client:
- task = await client.get_prompt("analysis_prompt", {"topic": "AI"}, task=True)
-
- assert isinstance(task, PromptTask)
- assert isinstance(task.task_id, str)
-
-
-async def test_prompt_task_server_generated_id(prompt_server):
- """get_prompt with task=True gets server-generated task ID."""
- async with Client(prompt_server, mode="legacy") as client:
- task = await client.get_prompt(
- "creative_prompt",
- {"theme": "future"},
- task=True,
- )
-
- # Server should generate a UUID task ID
- assert task.task_id is not None
- assert isinstance(task.task_id, str)
- # UUIDs have hyphens
- assert "-" in task.task_id
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on GetPromptRequestParams / "
- "ReadResourceRequestParams; prompt/resource task submission is not "
- "wire-expressible and always graceful-degrades (sdk-feedback #3).",
- strict=True,
-)
-async def test_prompt_task_result_returns_get_prompt_result(prompt_server):
- """PromptTask.result() returns GetPromptResult."""
- async with Client(prompt_server, mode="legacy") as client:
- task = await client.get_prompt(
- "analysis_prompt", {"topic": "Robotics", "style": "casual"}, task=True
- )
-
- # Verify background execution
- assert not task.returned_immediately
-
- # Get result
- result = await task.result()
-
- # Result should be GetPromptResult
- assert hasattr(result, "description")
- assert hasattr(result, "messages")
- # Check the rendered message content, not the description
- assert len(result.messages) > 0
- assert "Analyze Robotics" in result.messages[0].content.text
-
-
-async def test_prompt_task_await_syntax(prompt_server):
- """PromptTask can be awaited directly."""
- async with Client(prompt_server, mode="legacy") as client:
- task = await client.get_prompt("creative_prompt", {"theme": "ocean"}, task=True)
-
- # Can await task directly
- result = await task
- assert "Write a story about ocean" in result.messages[0].content.text
-
-
-async def test_prompt_task_status_and_wait(prompt_server):
- """PromptTask supports status() and wait() methods."""
- async with Client(prompt_server, mode="legacy") as client:
- task = await client.get_prompt("analysis_prompt", {"topic": "Space"}, task=True)
-
- # Check status
- status = await task.status()
- assert status.status in ["working", "completed"]
-
- # Wait for completion
- await task.wait(timeout=2.0)
-
- # Get result
- result = await task.result()
- assert "Analyze Space" in result.messages[0].content.text
diff --git a/tests/client/tasks/test_client_resource_tasks.py b/tests/client/tasks/test_client_resource_tasks.py
deleted file mode 100644
index 44ab5f826..000000000
--- a/tests/client/tasks/test_client_resource_tasks.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-Tests for client-side resource task methods.
-
-Tests the client's read_resource_as_task method.
-"""
-
-import pytest
-
-from fastmcp import FastMCP
-from fastmcp.client import Client
-from fastmcp.client.tasks import ResourceTask
-
-
-@pytest.fixture
-async def resource_server():
- """Create a test server with background-enabled resources."""
- mcp = FastMCP("resource-client-test")
-
- @mcp.resource("file://document.txt", task=True)
- async def document() -> str:
- """A document resource."""
- return "Document content here"
-
- @mcp.resource("file://data/{id}.json", task=True)
- async def data_file(id: str) -> str:
- """A parameterized data resource."""
- return f'{{"id": "{id}", "value": 42}}'
-
- return mcp
-
-
-async def test_read_resource_as_task_returns_resource_task(resource_server):
- """read_resource with task=True returns a ResourceTask object."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://document.txt", task=True)
-
- assert isinstance(task, ResourceTask)
- assert isinstance(task.task_id, str)
-
-
-async def test_resource_task_server_generated_id(resource_server):
- """read_resource with task=True gets server-generated task ID."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://document.txt", task=True)
-
- # Server should generate a UUID task ID
- assert task.task_id is not None
- assert isinstance(task.task_id, str)
- # UUIDs have hyphens
- assert "-" in task.task_id
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on ReadResourceRequestParams, so "
- "resource reads cannot be submitted as background tasks over the wire and "
- "always graceful-degrade to immediate execution (sdk-feedback #3).",
- strict=True,
-)
-async def test_resource_task_result_returns_read_resource_result(resource_server):
- """ResourceTask.result() returns list of ReadResourceContents."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://document.txt", task=True)
-
- # Verify background execution
- assert not task.returned_immediately
-
- # Get result
- result = await task.result()
-
- # Result should be list of ReadResourceContents
- assert isinstance(result, list)
- assert len(result) > 0
- assert result[0].text == "Document content here"
-
-
-async def test_resource_task_await_syntax(resource_server):
- """ResourceTask can be awaited directly."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://document.txt", task=True)
-
- # Can await task directly
- result = await task
- assert result[0].text == "Document content here"
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on ReadResourceRequestParams, so "
- "resource reads cannot be submitted as background tasks over the wire and "
- "always graceful-degrade to immediate execution (sdk-feedback #3).",
- strict=True,
-)
-async def test_resource_template_task(resource_server):
- """Resource templates work with task support."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://data/999.json", task=True)
-
- # Verify background execution
- assert not task.returned_immediately
-
- # Get result
- result = await task.result()
- assert '"id": "999"' in result[0].text
-
-
-async def test_resource_task_status_and_wait(resource_server):
- """ResourceTask supports status() and wait() methods."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://document.txt", task=True)
-
- # Check status
- status = await task.status()
- assert status.status in ["working", "completed"]
-
- # Wait for completion
- await task.wait(timeout=2.0)
-
- # Get result
- result = await task.result()
- assert "Document content" in result[0].text
diff --git a/tests/client/telemetry/test_client_task_tracing.py b/tests/client/telemetry/test_client_task_tracing.py
index 6af938d40..ad5081e7d 100644
--- a/tests/client/telemetry/test_client_task_tracing.py
+++ b/tests/client/telemetry/test_client_task_tracing.py
@@ -2,6 +2,7 @@
import asyncio
+import pytest
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
@@ -9,6 +10,10 @@ from opentelemetry.trace import SpanKind
from fastmcp import Client, FastMCP
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
def assert_propagating_client_span(
trace_exporter: InMemorySpanExporter,
diff --git a/tests/client/test_client_extensions.py b/tests/client/test_client_extensions.py
index 6ad8b26f3..8681fc817 100644
--- a/tests/client/test_client_extensions.py
+++ b/tests/client/test_client_extensions.py
@@ -142,6 +142,7 @@ def test_extension_populates_claim_by_model_index():
assert client._claim_by_model[ClaimedResult].result_type == CLAIMED_TYPE
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
def test_binding_composes_with_internal_task_binding():
"""User binding is appended to (not replacing) the task-status binding."""
client = Client(FastMCP("srv"), extensions=[_DemoExtension()])
@@ -153,6 +154,7 @@ def test_binding_composes_with_internal_task_binding():
assert methods[0] == TASK_STATUS_METHOD
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
def test_no_extensions_leaves_only_task_binding():
"""Without extensions, only the internal task-status binding is registered."""
client = Client(FastMCP("srv"))
@@ -163,6 +165,7 @@ def test_no_extensions_leaves_only_task_binding():
assert client._claim_by_model == {}
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
def test_new_preserves_extension_composition():
"""new() rebuilds the clone with both the task binding and user bindings."""
client = Client(FastMCP("srv"), extensions=[_DemoExtension()])
@@ -204,6 +207,7 @@ def test_result_claims_merge_with_extension_claims():
assert set(client._claim_by_model) == {ClaimedResult, ExtraClaimed}
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_user_binding_clobbering_task_method_is_rejected():
"""A user extension binding the task-status method cannot silently replace it.
@@ -233,6 +237,7 @@ async def test_user_binding_clobbering_task_method_is_rejected():
pass
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_both_bindings_fire_against_live_server():
"""The internal task binding and a user extension binding both fire.
@@ -263,8 +268,8 @@ async def test_both_bindings_fire_against_live_server():
# The user extension binding fires on the custom notification.
await client.call_tool("emit", {"value": 21})
# The internal task binding fires on the task-status notification.
- task = await client.call_tool("background", {"value": 5}, task=True)
- status = await task.wait(timeout=2.0)
+ task = await client.call_tool("background", {"value": 5}, task=True) # ty: ignore
+ status = await task.wait(timeout=2.0) # ty: ignore
# Give the custom-notification queue a moment to drain.
await asyncio.sleep(0.1)
diff --git a/tests/client/transports/test_memory_transport.py b/tests/client/transports/test_memory_transport.py
index a67784c89..5bbe3563e 100644
--- a/tests/client/transports/test_memory_transport.py
+++ b/tests/client/transports/test_memory_transport.py
@@ -18,6 +18,7 @@ def test_transport_repr_includes_server_name():
assert repr(transport) == ""
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
@pytest.mark.timeout(10)
async def test_task_teardown_does_not_hang():
"""In-memory transport must tear down in under 2 seconds after a task call.
diff --git a/tests/conftest.py b/tests/conftest.py
index 98f1c6aa9..84487e445 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -4,7 +4,6 @@ import secrets
import socket
import sys
from collections.abc import Callable, Generator
-from datetime import timedelta
from pathlib import Path
from typing import Any
@@ -115,17 +114,14 @@ def isolate_settings_home(_settings_home_root: Path):
per-test overhead (numbering, test-id sanitization, retention-policy
bookkeeping) for the ~99% of tests that never touch this directory.
- Also sets a fast Docket polling interval for tests — the default 50ms
- is fine for production but still adds ~25ms average pickup latency per
- task. 10ms makes task tests near-instant.
+ Docket settings moved to the fastmcp-tasks package, so they are no longer
+ overridden here.
"""
test_home = _settings_home_root / secrets.token_hex(8)
test_home.mkdir()
with temporary_settings(
home=test_home,
- docket__minimum_check_interval=timedelta(milliseconds=10),
- docket__url=f"memory://{secrets.token_hex(4)}",
client_disconnect_timeout=1,
):
yield
diff --git a/tests/server/http/test_http_dependencies.py b/tests/server/http/test_http_dependencies.py
index d03b3df90..531b8d1f6 100644
--- a/tests/server/http/test_http_dependencies.py
+++ b/tests/server/http/test_http_dependencies.py
@@ -146,6 +146,7 @@ async def test_get_http_headers_excludes_content_type(sse_server: ASGIServer):
assert headers["x-custom-header"] == "should-be-included"
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_background_task_can_read_snapshotted_request_headers():
"""Background tools can still access request headers via get_http_request()."""
server = FastMCP()
@@ -164,6 +165,7 @@ async def test_background_task_can_read_snapshotted_request_headers():
assert result.data == "tenant-123"
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_background_task_current_http_dependencies_restore_headers():
"""CurrentHeaders/CurrentRequest work in task workers without explicit Context."""
server = FastMCP()
diff --git a/tests/server/middleware/test_caching.py b/tests/server/middleware/test_caching.py
index 45201c640..b45961bcf 100644
--- a/tests/server/middleware/test_caching.py
+++ b/tests/server/middleware/test_caching.py
@@ -355,9 +355,18 @@ class TestResponseCachingMiddlewareIntegration:
async def test_list_operations_preserve_component_metadata(self):
"""Base component fields should survive conversion through the cache."""
+ from fastmcp.server.extensions import ServerExtension
+ from fastmcp.utilities.tasks import TASKS_EXTENSION_ID
+
+ class _StubTasksExtension(ServerExtension):
+ identifier = TASKS_EXTENSION_ID
+
icon = mcp_types.Icon(src="https://example.com/component.png")
mcp = FastMCP("MetadataServer")
mcp.add_middleware(ResponseCachingMiddleware())
+ # A task-enabled tool requires the tasks extension to serve; register a
+ # stub so the metadata (execution.task_support) can be verified end-to-end.
+ mcp.add_extension(_StubTasksExtension())
@mcp.tool(icons=[icon], task=TaskConfig(mode="optional"))
async def greet() -> str:
diff --git a/tests/server/mount/test_advanced.py b/tests/server/mount/test_advanced.py
index 5daeb65b8..89f590e63 100644
--- a/tests/server/mount/test_advanced.py
+++ b/tests/server/mount/test_advanced.py
@@ -598,6 +598,7 @@ class TestMountedServerDocketBehavior:
includes Docket creation.
"""
+ @pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_mounted_server_does_not_have_docket(self):
"""Test that a mounted server doesn't create its own Docket.
diff --git a/tests/server/providers/test_base_provider.py b/tests/server/providers/test_base_provider.py
index 38db55e08..035dec6d7 100644
--- a/tests/server/providers/test_base_provider.py
+++ b/tests/server/providers/test_base_provider.py
@@ -6,9 +6,9 @@ import pytest
from fastmcp.server.providers.aggregate import AggregateProvider
from fastmcp.server.providers.base import Provider
-from fastmcp.server.tasks.config import TaskConfig
from fastmcp.server.transforms import Namespace
from fastmcp.tools.base import Tool, ToolResult
+from fastmcp.utilities.tasks import TaskConfig
class CustomTool(Tool):
diff --git a/tests/server/providers/test_local_provider.py b/tests/server/providers/test_local_provider.py
index ebc74f90c..6096e4509 100644
--- a/tests/server/providers/test_local_provider.py
+++ b/tests/server/providers/test_local_provider.py
@@ -17,8 +17,8 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.prompts.base import Prompt
from fastmcp.server.providers.local_provider import LocalProvider
-from fastmcp.server.tasks import TaskConfig
from fastmcp.tools.base import Tool, ToolResult
+from fastmcp.utilities.tasks import TaskConfig
class TestLocalProviderStorage:
diff --git a/tests/server/tasks/conftest.py b/tests/server/tasks/conftest.py
deleted file mode 100644
index 496053bfa..000000000
--- a/tests/server/tasks/conftest.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Configuration for server task tests."""
diff --git a/tests/server/tasks/test_resource_task_meta_parameter.py b/tests/server/tasks/test_resource_task_meta_parameter.py
deleted file mode 100644
index 1a5caeffd..000000000
--- a/tests/server/tasks/test_resource_task_meta_parameter.py
+++ /dev/null
@@ -1,287 +0,0 @@
-"""
-Tests for the explicit task_meta parameter on FastMCP.read_resource().
-
-These tests verify that the task_meta parameter provides explicit control
-over sync vs task execution for resources and resource templates.
-"""
-
-import pytest
-from mcp.shared.exceptions import MCPError
-
-from fastmcp import FastMCP
-from fastmcp.client import Client
-from fastmcp.resources.base import Resource
-from fastmcp.resources.template import ResourceTemplate
-from fastmcp.server.tasks.config import TaskMeta
-
-
-class TestResourceTaskMetaParameter:
- """Tests for task_meta parameter on FastMCP.read_resource()."""
-
- async def test_task_meta_none_returns_resource_result(self):
- """With task_meta=None (default), read_resource returns ResourceResult."""
- server = FastMCP("test")
-
- @server.resource("data://test")
- async def simple_resource() -> str:
- return "hello world"
-
- result = await server.read_resource("data://test")
-
- assert result.contents[0].content == "hello world"
-
- async def test_task_meta_none_on_task_enabled_resource_still_returns_result(self):
- """Even for task=True resources, task_meta=None returns ResourceResult."""
- server = FastMCP("test")
-
- @server.resource("data://test", task=True)
- async def task_enabled_resource() -> str:
- return "hello world"
-
- # Without task_meta, should execute synchronously
- result = await server.read_resource("data://test")
-
- assert result.contents[0].content == "hello world"
-
- async def test_task_meta_on_forbidden_resource_raises_error(self):
- """Providing task_meta to a task=False resource raises MCPError."""
- server = FastMCP("test")
-
- @server.resource("data://test", task=False)
- async def sync_only_resource() -> str:
- return "hello"
-
- with pytest.raises(MCPError) as exc_info:
- await server.read_resource("data://test", task_meta=TaskMeta())
-
- assert "does not support task-augmented execution" in str(exc_info.value)
-
- async def test_task_meta_fn_key_enrichment_for_resource(self):
- """Verify that fn_key enrichment uses Resource.make_key()."""
- resource_uri = "data://my-resource"
- expected_key = Resource.make_key(resource_uri)
-
- assert expected_key == "resource:data://my-resource"
-
- async def test_task_meta_fn_key_enrichment_for_template(self):
- """Verify that fn_key enrichment uses ResourceTemplate.make_key()."""
- template_pattern = "data://{id}"
- expected_key = ResourceTemplate.make_key(template_pattern)
-
- assert expected_key == "template:data://{id}"
-
-
-class TestResourceTemplateTaslMeta:
- """Tests for task_meta with resource templates."""
-
- async def test_template_task_meta_none_returns_resource_result(self):
- """With task_meta=None, template read returns ResourceResult."""
- server = FastMCP("test")
-
- @server.resource("item://{id}")
- async def get_item(id: str) -> str:
- return f"Item {id}"
-
- result = await server.read_resource("item://42")
-
- assert result.contents[0].content == "Item 42"
-
- async def test_template_task_meta_on_task_enabled_template_returns_result(self):
- """Even for task=True templates, task_meta=None returns ResourceResult."""
- server = FastMCP("test")
-
- @server.resource("item://{id}", task=True)
- async def get_item(id: str) -> str:
- return f"Item {id}"
-
- # Without task_meta, should execute synchronously
- result = await server.read_resource("item://42")
-
- assert result.contents[0].content == "Item 42"
-
- async def test_template_task_meta_on_forbidden_template_raises_error(self):
- """Providing task_meta to a task=False template raises MCPError."""
- server = FastMCP("test")
-
- @server.resource("item://{id}", task=False)
- async def sync_only_template(id: str) -> str:
- return f"Item {id}"
-
- with pytest.raises(MCPError) as exc_info:
- await server.read_resource("item://42", task_meta=TaskMeta())
-
- assert "does not support task-augmented execution" in str(exc_info.value)
-
-
-class TestResourceTaskMetaClientIntegration:
- """Tests that task_meta works correctly with the Client for resources."""
-
- async def test_client_read_resource_without_task_gets_immediate_result(self):
- """Client without task=True gets immediate result."""
- server = FastMCP("test")
-
- @server.resource("data://test", task=True)
- async def immediate_resource() -> str:
- return "hello"
-
- async with Client(server, mode="legacy") as client:
- result = await client.read_resource("data://test")
-
- # Should get ReadResourceResult directly
- assert "hello" in str(result)
-
- async def test_client_read_resource_with_task_creates_task(self):
- """Client with task=True creates a background task."""
- server = FastMCP("test")
-
- @server.resource("data://test", task=True)
- async def task_resource() -> str:
- return "hello"
-
- async with Client(server, mode="legacy") as client:
- from fastmcp.client.tasks import ResourceTask
-
- task = await client.read_resource("data://test", task=True)
-
- assert isinstance(task, ResourceTask)
-
- # Wait for result
- result = await task.result()
- assert "hello" in str(result)
-
- async def test_client_read_template_with_task_creates_task(self):
- """Client with task=True on template creates a background task."""
- server = FastMCP("test")
-
- @server.resource("item://{id}", task=True)
- async def get_item(id: str) -> str:
- return f"Item {id}"
-
- async with Client(server, mode="legacy") as client:
- from fastmcp.client.tasks import ResourceTask
-
- task = await client.read_resource("item://42", task=True)
-
- assert isinstance(task, ResourceTask)
-
- # Wait for result
- result = await task.result()
- assert "Item 42" in str(result)
-
-
-class TestResourceTaskMetaDirectServerCall:
- """Tests for direct server read_resource calls with task_meta."""
-
- async def test_resource_can_read_another_resource_with_task(self):
- """A resource can read another resource as a background task."""
- server = FastMCP("test")
-
- @server.resource("data://inner", task=True)
- async def inner_resource() -> str:
- return "inner data"
-
- @server.tool
- async def outer_tool() -> str:
- # Read inner resource as background task
- result = await server.read_resource("data://inner", task_meta=TaskMeta())
- # Should get CreateTaskResult since we provided task_meta
- return f"Created task: {result.task.task_id}"
-
- async with Client(server, mode="legacy") as client:
- result = await client.call_tool("outer_tool", {})
- assert "Created task:" in str(result)
-
- async def test_resource_can_read_another_resource_synchronously(self):
- """A resource can read another resource synchronously (no task_meta)."""
- server = FastMCP("test")
-
- @server.resource("data://inner", task=True)
- async def inner_resource() -> str:
- return "inner data"
-
- @server.tool
- async def outer_tool() -> str:
- # Read inner resource synchronously (no task_meta)
- result = await server.read_resource("data://inner")
- # Should get ResourceResult directly
- return f"Got result: {result.contents[0].content}"
-
- async with Client(server, mode="legacy") as client:
- result = await client.call_tool("outer_tool", {})
- assert "Got result: inner data" in str(result)
-
- async def test_resource_can_read_template_with_task(self):
- """A tool can read a resource template as a background task."""
- server = FastMCP("test")
-
- @server.resource("item://{id}", task=True)
- async def get_item(id: str) -> str:
- return f"Item {id}"
-
- @server.tool
- async def outer_tool() -> str:
- result = await server.read_resource("item://99", task_meta=TaskMeta())
- return f"Created task: {result.task.task_id}"
-
- async with Client(server, mode="legacy") as client:
- result = await client.call_tool("outer_tool", {})
- assert "Created task:" in str(result)
-
- async def test_resource_can_read_with_custom_ttl(self):
- """A tool can read a resource as a background task with custom TTL."""
- server = FastMCP("test")
-
- @server.resource("data://inner", task=True)
- async def inner_resource() -> str:
- return "inner data"
-
- @server.tool
- async def outer_tool() -> str:
- custom_ttl = 45000 # 45 seconds
- result = await server.read_resource(
- "data://inner", task_meta=TaskMeta(ttl=custom_ttl)
- )
- return f"Task TTL: {result.task.ttl}"
-
- async with Client(server, mode="legacy") as client:
- result = await client.call_tool("outer_tool", {})
- assert "Task TTL: 45000" in str(result)
-
-
-class TestResourceTaskMetaTypeNarrowing:
- """Tests for type narrowing based on task_meta parameter."""
-
- async def test_read_resource_without_task_meta_type_is_resource_result(self):
- """Calling read_resource without task_meta returns ResourceResult type."""
- server = FastMCP("test")
-
- @server.resource("data://test")
- async def simple_resource() -> str:
- return "hello"
-
- # This should type-check as ResourceResult, not the union type
- result = await server.read_resource("data://test")
-
- # No isinstance check needed - type is narrowed by overload
- content = result.contents[0].content
- assert content == "hello"
-
- async def test_read_resource_with_task_meta_type_is_create_task_result(self):
- """Calling read_resource with task_meta returns CreateTaskResult type."""
- server = FastMCP("test")
-
- @server.resource("data://test", task=True)
- async def task_resource() -> str:
- return "hello"
-
- async with Client(server, mode="legacy") as client:
- # Need to use client to get full task infrastructure
- from fastmcp.client.tasks import ResourceTask
-
- task = await client.read_resource("data://test", task=True)
- assert isinstance(task, ResourceTask)
-
- # For direct server call, we need the Client context for Docket
- # This test verifies the overload works via client integration
- result = await task.result()
- assert "hello" in str(result)
diff --git a/tests/server/tasks/test_task_prompts.py b/tests/server/tasks/test_task_prompts.py
deleted file mode 100644
index 1054d3cee..000000000
--- a/tests/server/tasks/test_task_prompts.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""
-Tests for SEP-1686 background task support for prompts.
-
-Tests that prompts with task=True can execute in background.
-"""
-
-import pytest
-
-from fastmcp import FastMCP
-from fastmcp.client import Client
-from fastmcp.client.tasks import PromptTask
-
-
-@pytest.fixture
-async def prompt_server():
- """Create a FastMCP server with task-enabled prompts."""
- mcp = FastMCP("prompt-test-server")
-
- @mcp.prompt()
- async def simple_prompt(topic: str) -> str:
- """A simple prompt template."""
- return f"Write about: {topic}"
-
- @mcp.prompt(task=True)
- async def background_prompt(topic: str, depth: str = "detailed") -> str:
- """A prompt that can execute in background."""
- return f"Write a {depth} analysis of: {topic}"
-
- return mcp
-
-
-async def test_synchronous_prompt_unchanged(prompt_server):
- """Prompts without task metadata execute synchronously as before."""
- async with Client(prompt_server, mode="legacy") as client:
- # Regular call without task metadata
- result = await client.get_prompt("simple_prompt", {"topic": "AI"})
-
- # Should execute immediately and return result
- assert "Write about: AI" in str(result)
-
-
-async def test_prompt_with_task_metadata_returns_immediately(prompt_server):
- """Prompts with task metadata return immediately with PromptTask object."""
- async with Client(prompt_server, mode="legacy") as client:
- # Call with task metadata
- task = await client.get_prompt("background_prompt", {"topic": "AI"}, task=True)
-
- # Should return a PromptTask object immediately
- assert isinstance(task, PromptTask)
- assert isinstance(task.task_id, str)
- assert len(task.task_id) > 0
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on GetPromptRequestParams / "
- "ReadResourceRequestParams; prompt/resource task submission is not "
- "wire-expressible and always graceful-degrades (sdk-feedback #3).",
- strict=True,
-)
-async def test_prompt_task_executes_in_background(prompt_server):
- """Prompt task executes via Docket in background."""
- async with Client(prompt_server, mode="legacy") as client:
- task = await client.get_prompt(
- "background_prompt",
- {"topic": "Machine Learning", "depth": "comprehensive"},
- task=True,
- )
-
- # Verify background execution
- assert not task.returned_immediately
-
- # Get the result
- result = await task.result()
- assert "comprehensive" in result.messages[0].content.text.lower()
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on GetPromptRequestParams / "
- "ReadResourceRequestParams; prompt/resource task submission is not "
- "wire-expressible and always graceful-degrades (sdk-feedback #3).",
- strict=True,
-)
-async def test_forbidden_mode_prompt_rejects_task_calls(prompt_server):
- """Prompts with task=False (mode=forbidden) reject task-augmented calls."""
- from mcp.shared.exceptions import MCPError
- from mcp_types import METHOD_NOT_FOUND
-
- @prompt_server.prompt(task=False) # Explicitly disable task support
- async def sync_only_prompt(topic: str) -> str:
- return f"Sync prompt: {topic}"
-
- async with Client(prompt_server, mode="legacy") as client:
- # Calling with task=True when task=False should raise MCPError
- import pytest
-
- with pytest.raises(MCPError) as exc_info:
- await client.get_prompt("sync_only_prompt", {"topic": "test"}, task=True)
-
- # New behavior: mode="forbidden" returns METHOD_NOT_FOUND error
- assert exc_info.value.error.code == METHOD_NOT_FOUND
- assert (
- "does not support task-augmented execution" in exc_info.value.error.message
- )
diff --git a/tests/server/tasks/test_task_resources.py b/tests/server/tasks/test_task_resources.py
deleted file mode 100644
index acb136281..000000000
--- a/tests/server/tasks/test_task_resources.py
+++ /dev/null
@@ -1,125 +0,0 @@
-"""
-Tests for SEP-1686 background task support for resources.
-
-Tests that resources with task=True can execute in background.
-"""
-
-import pytest
-
-from fastmcp import FastMCP
-from fastmcp.client import Client
-from fastmcp.client.tasks import ResourceTask
-
-
-@pytest.fixture
-async def resource_server():
- """Create a FastMCP server with task-enabled resources."""
- mcp = FastMCP("resource-test-server")
-
- @mcp.resource("file://data.txt")
- async def simple_resource() -> str:
- """A simple resource."""
- return "Simple content"
-
- @mcp.resource("file://large.txt", task=True)
- async def background_resource() -> str:
- """A resource that can execute in background."""
- return "Large file content that takes time to load"
-
- @mcp.resource("file://user/{user_id}/data.json", task=True)
- async def template_resource(user_id: str) -> str:
- """A resource template that can execute in background."""
- return f'{{"userId": "{user_id}", "data": "value"}}'
-
- return mcp
-
-
-async def test_synchronous_resource_unchanged(resource_server):
- """Resources without task metadata execute synchronously as before."""
- async with Client(resource_server, mode="legacy") as client:
- # Regular call without task metadata
- result = await client.read_resource("file://data.txt")
-
- # Should execute immediately and return result
- assert "Simple content" in str(result)
-
-
-async def test_resource_with_task_metadata_returns_immediately(resource_server):
- """Resources with task metadata return immediately with ResourceTask object."""
- async with Client(resource_server, mode="legacy") as client:
- # Call with task metadata
- task = await client.read_resource("file://large.txt", task=True)
-
- # Should return a ResourceTask object immediately
- assert isinstance(task, ResourceTask)
- assert isinstance(task.task_id, str)
- assert len(task.task_id) > 0
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on GetPromptRequestParams / "
- "ReadResourceRequestParams; prompt/resource task submission is not "
- "wire-expressible and always graceful-degrades (sdk-feedback #3).",
- strict=True,
-)
-async def test_resource_task_executes_in_background(resource_server):
- """Resource task executes via Docket in background."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://large.txt", task=True)
-
- # Verify background execution
- assert not task.returned_immediately
-
- # Get the result
- result = await task.result()
- assert len(result) > 0
- assert result[0].text == "Large file content that takes time to load"
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on GetPromptRequestParams / "
- "ReadResourceRequestParams; prompt/resource task submission is not "
- "wire-expressible and always graceful-degrades (sdk-feedback #3).",
- strict=True,
-)
-async def test_resource_template_with_task(resource_server):
- """Resource templates with task=True execute in background."""
- async with Client(resource_server, mode="legacy") as client:
- task = await client.read_resource("file://user/123/data.json", task=True)
-
- # Verify background execution
- assert not task.returned_immediately
-
- # Get the result
- result = await task.result()
- assert '"userId": "123"' in result[0].text
-
-
-@pytest.mark.xfail(
- reason="SDK v2 has no `task` field on GetPromptRequestParams / "
- "ReadResourceRequestParams; prompt/resource task submission is not "
- "wire-expressible and always graceful-degrades (sdk-feedback #3).",
- strict=True,
-)
-async def test_forbidden_mode_resource_rejects_task_calls(resource_server):
- """Resources with task=False (mode=forbidden) reject task-augmented calls."""
- import pytest
- from mcp.shared.exceptions import MCPError
- from mcp_types import METHOD_NOT_FOUND
-
- @resource_server.resource(
- "file://sync.txt/", task=False
- ) # Explicitly disable task support
- async def sync_only_resource() -> str:
- return "Sync content"
-
- async with Client(resource_server, mode="legacy") as client:
- # Calling with task=True when task=False should raise MCPError
- with pytest.raises(MCPError) as exc_info:
- await client.read_resource("file://sync.txt", task=True)
-
- # New behavior: mode="forbidden" returns METHOD_NOT_FOUND error
- assert exc_info.value.error.code == METHOD_NOT_FOUND
- assert (
- "does not support task-augmented execution" in exc_info.value.error.message
- )
diff --git a/tests/server/test_dependencies.py b/tests/server/test_dependencies.py
index 818cbaca4..1916c3428 100644
--- a/tests/server/test_dependencies.py
+++ b/tests/server/test_dependencies.py
@@ -9,7 +9,6 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.dependencies import CurrentContext, Depends, Shared
from fastmcp.server.context import Context
-from fastmcp.server.dependencies import is_docket_available
from tests.conftest import make_server_request_context
HUZZAH = "huzzah!"
@@ -786,9 +785,6 @@ class TestDependencyInjection:
monkeypatch.setattr(importlib.metadata, "version", fake_version)
assert dependencies.is_docket_available() is False
- # The wrapper that actually failed in #3803 must now return None
- # instead of raising ImportError on the inner import.
- assert dependencies.get_task_context() is None
def test_is_docket_available_false_when_pydocket_not_installed(self, monkeypatch):
"""``is_docket_available()`` returns False when pydocket is absent."""
@@ -835,7 +831,7 @@ class TestDependencyInjection:
def test_require_docket_passes_when_installed(self):
"""Test require_docket doesn't raise when docket is installed."""
- from fastmcp.server.dependencies import require_docket
+ from fastmcp_tasks.dependencies import require_docket
require_docket("test feature")
@@ -849,6 +845,8 @@ class TestDependencyInjection:
"""
import importlib.metadata
+ from fastmcp_tasks.dependencies import require_docket
+
from fastmcp.server import dependencies
original_version = importlib.metadata.version
@@ -862,7 +860,7 @@ class TestDependencyInjection:
monkeypatch.setattr(importlib.metadata, "version", fake_version)
with pytest.raises(ImportError, match="pydocket 0.16.6 is installed"):
- dependencies.require_docket("CurrentDocket()")
+ require_docket("CurrentDocket()")
def test_dependency_class_exists(self):
"""Test Dependency and Depends are importable from fastmcp."""
@@ -1195,10 +1193,7 @@ class TestSharedDependencies:
)
assert call_count == 1
- @pytest.mark.skipif(
- not is_docket_available(),
- reason="requires pydocket for the Docket/Worker lifespan path",
- )
+ @pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_shared_resolves_on_task_capable_server(self):
"""Shared() dependencies resolve on a normal request even when the server
has task-enabled components.
diff --git a/tests/server/test_mrtr_guards.py b/tests/server/test_mrtr_guards.py
index d6938d7e3..4855e2c5a 100644
--- a/tests/server/test_mrtr_guards.py
+++ b/tests/server/test_mrtr_guards.py
@@ -1158,6 +1158,7 @@ class TestTaskExecution:
background task has no such request, so returning a guard result from a task
is rejected with a clear error rather than silently yielding empty content."""
+ @pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_guard_result_from_task_is_rejected(self):
mcp = FastMCP("guard-task")
diff --git a/tests/server/test_protocol_eras.py b/tests/server/test_protocol_eras.py
index 1da476c4c..972ba1733 100644
--- a/tests/server/test_protocol_eras.py
+++ b/tests/server/test_protocol_eras.py
@@ -27,11 +27,6 @@ from mcp.client import Client as SDKClient
from mcp.client.session import ClientRequestContext
from mcp.server import Server as LowLevelServer
from mcp.shared.exceptions import MCPError
-from mcp_types import methods
-from mcp_types.version import (
- HANDSHAKE_PROTOCOL_VERSIONS,
- MODERN_PROTOCOL_VERSIONS,
-)
from pydantic import FileUrl
from fastmcp import Client as FastMCPClient
@@ -543,117 +538,6 @@ async def test_logging_notification_still_flows_on_modern(push_server, mode):
assert _texts(result.content) == ["logged"]
-# ---------------------------------------------------------------------------
-# 4. Tasks: submission + tasks/get across the eras the _sdk_patches shim covers
-# ---------------------------------------------------------------------------
-
-
-@pytest.fixture
-def task_server() -> FastMCP:
- mcp = FastMCP("tasks")
-
- @mcp.tool(task=True)
- async def slow_add(a: int, b: int) -> int:
- return a + b
-
- return mcp
-
-
-async def test_task_submission_and_get_on_legacy_latest(task_server):
- """Legacy-latest (2025-11-25): a task-augmented tools/call returns a
- CreateTaskResult and tasks/get resolves it. This exercises the
- _sdk_patches registry-widening shim at the 2025-11-25 tools/call surface.
-
- Driven with the FastMCP client because the v2 SDK client's call_tool has no
- `task=` parameter (verified: mcp.client.session.ClientSession.call_tool
- exposes no task metadata arg) — see item below.
- """
- async with FastMCPClient(task_server, mode="legacy") as client:
- assert client.initialize_result is not None
- assert client.initialize_result.protocol_version == "2025-11-25"
-
- task = await client.call_tool("slow_add", {"a": 2, "b": 3}, task=True)
- assert task.task_id
- assert not task.returned_immediately
-
- await task.wait(timeout=3.0)
- result = await task.result()
- assert result.data == 5
-
-
-@pytest.mark.xfail(
- strict=True,
- reason=(
- "The v2 SDK high-level client (mcp.client.Client) and ClientSession "
- "expose no `task=` parameter on call_tool, so a task-augmented "
- "tools/call cannot be submitted through it at any era; a hand-built "
- "raw CallToolRequest does not drive FastMCP's task path either. On "
- "2026-07-28 tasks moved to the io.modelcontextprotocol/tasks extension "
- "and CreateTaskResult is not part of the tools/call union, so the "
- "_sdk_patches shim intentionally does not widen the modern row "
- "(sdk-feedback.md #1). Remove once the SDK client supports task "
- "submission."
- ),
-)
-async def test_task_submission_on_modern(task_server):
- async with SDKClient(_server(task_server), mode="2026-07-28") as client:
- params = types.CallToolRequestParams(
- name="slow_add",
- arguments={"a": 1, "b": 2},
- task=types.TaskMetadata(ttl=60000),
- )
- result = await client.session.send_request(
- types.CallToolRequest(params=params), types.CreateTaskResult
- )
- assert isinstance(result, types.CreateTaskResult)
-
-
-# ---------------------------------------------------------------------------
-# 4b. _sdk_patches registry gating: the SEP-1686 task shim widens ONLY the
-# handshake-era rows and leaves the 2026-07-28 (extension-era) rows untouched.
-# ---------------------------------------------------------------------------
-
-
-def test_task_shim_widens_handshake_tools_call_rows():
- """Every handshake-era tools/call row gains a CreateTaskResult arm."""
- from fastmcp._sdk_patches import get_union_arms
-
- for version in HANDSHAKE_PROTOCOL_VERSIONS:
- row = methods.SERVER_RESULTS[("tools/call", version)]
- assert types.CreateTaskResult in get_union_arms(row), version
-
-
-def test_task_shim_does_not_touch_modern_tools_call_row():
- """The 2026-07-28 tools/call row stays the unpatched MRTR union: tasks are
- the io.modelcontextprotocol/tasks extension there, so CreateTaskResult must
- not be injected."""
- from fastmcp._sdk_patches import get_union_arms
-
- row = methods.SERVER_RESULTS[("tools/call", "2026-07-28")]
- arms = get_union_arms(row)
- assert types.CreateTaskResult not in arms
- # Unchanged from the SDK default: the 2026 mutually-recursive tool result
- # (CallToolResult | InputRequiredResult), keyed by the version-specific types.
- arm_names = {arm.__name__ for arm in arms}
- assert arm_names == {"CallToolResult", "InputRequiredResult"}
-
-
-@pytest.mark.parametrize(
- "task_method",
- ["tasks/get", "tasks/result", "tasks/list", "tasks/cancel"],
-)
-def test_task_shim_registers_tasks_rows_only_for_handshake_eras(task_method):
- """tasks/* result rows exist for handshake-era versions and are absent for
- the modern (extension) era."""
- for version in HANDSHAKE_PROTOCOL_VERSIONS:
- assert (task_method, version) in methods.SERVER_RESULTS, (task_method, version)
- for version in MODERN_PROTOCOL_VERSIONS:
- assert (task_method, version) not in methods.SERVER_RESULTS, (
- task_method,
- version,
- )
-
-
# ---------------------------------------------------------------------------
# 5. Sessionless safety: session-id-keyed paths must not crash on 2026 in-memory
# ---------------------------------------------------------------------------
diff --git a/tests/server/test_server_docket.py b/tests/server/test_server_docket.py
index 7d0b7f5c0..ee11a5b60 100644
--- a/tests/server/test_server_docket.py
+++ b/tests/server/test_server_docket.py
@@ -3,14 +3,19 @@
import asyncio
from contextlib import asynccontextmanager
+import pytest
from docket import Docket
from docket.worker import Worker
+from fastmcp_tasks.dependencies import CurrentDocket, CurrentWorker
from fastmcp import FastMCP
from fastmcp.client import Client
-from fastmcp.dependencies import CurrentDocket, CurrentWorker
from fastmcp.server.dependencies import get_context
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
HUZZAH = "huzzah!"
diff --git a/tests/server/test_tool_annotations.py b/tests/server/test_tool_annotations.py
index e839d5689..3c66040bd 100644
--- a/tests/server/test_tool_annotations.py
+++ b/tests/server/test_tool_annotations.py
@@ -1,5 +1,6 @@
from typing import Any
+import pytest
from mcp_types import Tool as MCPTool
from mcp_types import ToolAnnotations, ToolExecution
@@ -220,6 +221,7 @@ async def test_tool_functionality_with_annotations():
assert result.data == {"name": "test_item", "value": 42}
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
async def test_task_execution_auto_populated_for_task_enabled_tool():
"""Test that execution.task_support is automatically set when tool has task=True."""
mcp = FastMCP("Test Server")
diff --git a/tests/tasks/__init__.py b/tests/tasks/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/tasks/client/__init__.py b/tests/tasks/client/__init__.py
new file mode 100644
index 000000000..24bcd2c25
--- /dev/null
+++ b/tests/tasks/client/__init__.py
@@ -0,0 +1 @@
+"""Tests for MCP SEP-1686 background task client."""
diff --git a/tests/tasks/client/conftest.py b/tests/tasks/client/conftest.py
new file mode 100644
index 000000000..226fada94
--- /dev/null
+++ b/tests/tasks/client/conftest.py
@@ -0,0 +1,25 @@
+"""Configuration for client task tests."""
+
+import secrets
+from pathlib import Path
+
+import pytest
+
+from fastmcp.utilities.tests import temporary_settings
+
+
+@pytest.fixture(autouse=True)
+def isolate_settings_home(_settings_home_root: Path):
+ """Task-local override of the repo-wide ``isolate_settings_home`` fixture.
+
+ Docket configuration moved out of core ``Settings`` into
+ ``fastmcp_tasks.settings.DocketSettings``, so the repo-wide fixture's
+ ``docket__*`` kwargs no longer resolve against core settings. This
+ override keeps the per-test settings-home isolation while dropping the
+ removed docket kwargs.
+ """
+ test_home = _settings_home_root / secrets.token_hex(8)
+ test_home.mkdir()
+
+ with temporary_settings(home=test_home, client_disconnect_timeout=1):
+ yield
diff --git a/tests/client/tasks/test_client_task_notifications.py b/tests/tasks/client/test_client_task_notifications.py
similarity index 99%
rename from tests/client/tasks/test_client_task_notifications.py
rename to tests/tasks/client/test_client_task_notifications.py
index 3e05a2055..f93365caa 100644
--- a/tests/client/tasks/test_client_task_notifications.py
+++ b/tests/tasks/client/test_client_task_notifications.py
@@ -16,6 +16,10 @@ from mcp_types import GetTaskResult
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
async def _wait_until(condition: Callable[[], bool], timeout: float = 5.0) -> None:
"""Poll until condition() is true or timeout elapses.
diff --git a/tests/client/tasks/test_client_task_protocol.py b/tests/tasks/client/test_client_task_protocol.py
similarity index 95%
rename from tests/client/tasks/test_client_task_protocol.py
rename to tests/tasks/client/test_client_task_protocol.py
index 343e69bf4..4d5d77bda 100644
--- a/tests/client/tasks/test_client_task_protocol.py
+++ b/tests/tasks/client/test_client_task_protocol.py
@@ -6,9 +6,15 @@ Generic protocol tests that use tools as test fixtures.
import asyncio
+import pytest
+
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
async def test_end_to_end_task_flow():
"""Complete end-to-end flow: submit, poll, retrieve."""
diff --git a/tests/client/tasks/test_client_tool_tasks.py b/tests/tasks/client/test_client_tool_tasks.py
similarity index 97%
rename from tests/client/tasks/test_client_tool_tasks.py
rename to tests/tasks/client/test_client_tool_tasks.py
index 2bccedccb..4a3d9d379 100644
--- a/tests/client/tasks/test_client_tool_tasks.py
+++ b/tests/tasks/client/test_client_tool_tasks.py
@@ -6,12 +6,16 @@ test_client_prompt_tasks.py and test_client_resource_tasks.py.
"""
import pytest
+from fastmcp_tasks.client import ToolTask
from fastmcp import FastMCP
from fastmcp.client import Client
-from fastmcp.client.tasks import ToolTask
from fastmcp.exceptions import ToolError
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def tool_task_server():
diff --git a/tests/client/tasks/test_poll_interval.py b/tests/tasks/client/test_poll_interval.py
similarity index 94%
rename from tests/client/tasks/test_poll_interval.py
rename to tests/tasks/client/test_poll_interval.py
index afc08f7c9..0c33cca4d 100644
--- a/tests/client/tasks/test_poll_interval.py
+++ b/tests/tasks/client/test_poll_interval.py
@@ -5,14 +5,18 @@ unadvertised one falls back to an exponential ramp up to the client setting.
"""
import pytest
+from fastmcp_tasks.client import MIN_POLL_INTERVAL, ToolTask
from mcp_types import GetTaskResult
from pydantic import ValidationError
from fastmcp import Client, FastMCP
-from fastmcp.client.tasks import MIN_POLL_INTERVAL, ToolTask
from fastmcp.settings import Settings
from fastmcp.utilities.tests import temporary_settings
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.mark.parametrize("value", [0, -0.5, -1])
def test_non_positive_poll_interval_setting_is_rejected(value: float):
diff --git a/tests/client/tasks/test_task_context_validation.py b/tests/tasks/client/test_task_context_validation.py
similarity index 98%
rename from tests/client/tasks/test_task_context_validation.py
rename to tests/tasks/client/test_task_context_validation.py
index 2b6a76832..4eda41739 100644
--- a/tests/client/tasks/test_task_context_validation.py
+++ b/tests/tasks/client/test_task_context_validation.py
@@ -10,6 +10,10 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def task_server():
diff --git a/tests/client/tasks/test_task_result_caching.py b/tests/tasks/client/test_task_result_caching.py
similarity index 99%
rename from tests/client/tasks/test_task_result_caching.py
rename to tests/tasks/client/test_task_result_caching.py
index 183014c8e..e0cf7b880 100644
--- a/tests/client/tasks/test_task_result_caching.py
+++ b/tests/tasks/client/test_task_result_caching.py
@@ -10,6 +10,10 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
async def test_tool_task_result_cached_on_first_call():
"""First call caches result, subsequent calls return cached value."""
diff --git a/tests/server/tasks/__init__.py b/tests/tasks/server/__init__.py
similarity index 100%
rename from tests/server/tasks/__init__.py
rename to tests/tasks/server/__init__.py
diff --git a/tests/tasks/server/conftest.py b/tests/tasks/server/conftest.py
new file mode 100644
index 000000000..70ea6754b
--- /dev/null
+++ b/tests/tasks/server/conftest.py
@@ -0,0 +1,25 @@
+"""Configuration for server task tests."""
+
+import secrets
+from pathlib import Path
+
+import pytest
+
+from fastmcp.utilities.tests import temporary_settings
+
+
+@pytest.fixture(autouse=True)
+def isolate_settings_home(_settings_home_root: Path):
+ """Task-local override of the repo-wide ``isolate_settings_home`` fixture.
+
+ Docket configuration moved out of core ``Settings`` into
+ ``fastmcp_tasks.settings.DocketSettings``, so the repo-wide fixture's
+ ``docket__*`` kwargs no longer resolve against core settings. This
+ override keeps the per-test settings-home isolation while dropping the
+ removed docket kwargs.
+ """
+ test_home = _settings_home_root / secrets.token_hex(8)
+ test_home.mkdir()
+
+ with temporary_settings(home=test_home, client_disconnect_timeout=1):
+ yield
diff --git a/tests/server/tasks/test_concurrent_dependencies.py b/tests/tasks/server/test_concurrent_dependencies.py
similarity index 98%
rename from tests/server/tasks/test_concurrent_dependencies.py
rename to tests/tasks/server/test_concurrent_dependencies.py
index 19f977a5c..10db2860a 100644
--- a/tests/server/tasks/test_concurrent_dependencies.py
+++ b/tests/tasks/server/test_concurrent_dependencies.py
@@ -8,15 +8,21 @@ Regression tests for:
import asyncio
+import pytest
+
from fastmcp import FastMCP
from fastmcp.client import Client
-from fastmcp.dependencies import Progress
from fastmcp.server.context import Context
from fastmcp.server.dependencies import (
+ Progress,
get_access_token,
get_http_headers,
)
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
async def test_concurrent_foreground_tools_with_context():
"""Multiple concurrent tool calls sharing the same CurrentContext() default
diff --git a/tests/server/tasks/test_context_background_task.py b/tests/tasks/server/test_context_background_task.py
similarity index 98%
rename from tests/server/tasks/test_context_background_task.py
rename to tests/tasks/server/test_context_background_task.py
index 42f92fa30..7f4bece2a 100644
--- a/tests/server/tasks/test_context_background_task.py
+++ b/tests/tasks/server/test_context_background_task.py
@@ -14,6 +14,20 @@ from typing import Any, cast
from unittest.mock import AsyncMock, patch
import pytest
+from fastmcp_tasks._legacy_wire.elicitation import handle_task_input
+from fastmcp_tasks.context import (
+ TaskContextInfo,
+ TaskContextSnapshot,
+ _remember_snapshot,
+ _task_sessions,
+ get_task_scope,
+ get_task_session,
+ register_task_session,
+)
+from fastmcp_tasks.dependencies import CurrentDocket
+from fastmcp_tasks.keys import (
+ task_redis_prefix,
+)
from mcp import ServerSession
from mcp.server.auth.middleware.auth_context import auth_context_var
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
@@ -29,7 +43,6 @@ from pydantic import BaseModel
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.elicitation import ElicitResult
-from fastmcp.dependencies import CurrentDocket
from fastmcp.server.auth import AccessToken
from fastmcp.server.context import Context
from fastmcp.server.dependencies import get_access_token
@@ -38,23 +51,13 @@ from fastmcp.server.elicitation import (
CancelledElicitation,
DeclinedElicitation,
)
-from fastmcp.server.tasks.context import (
- TaskContextInfo,
- TaskContextSnapshot,
- _remember_snapshot,
- _task_sessions,
- get_task_scope,
- get_task_session,
- register_task_session,
-)
-from fastmcp.server.tasks.elicitation import handle_task_input
-from fastmcp.server.tasks.keys import (
- task_redis_prefix,
-)
# =============================================================================
# Unit tests: Context API surface (no Redis/Docket needed)
# =============================================================================
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
class TestContextBackgroundTaskSupport:
diff --git a/tests/server/tasks/test_custom_subclass_tasks.py b/tests/tasks/server/test_custom_subclass_tasks.py
similarity index 97%
rename from tests/server/tasks/test_custom_subclass_tasks.py
rename to tests/tasks/server/test_custom_subclass_tasks.py
index 80233e8fc..cd6e87a39 100644
--- a/tests/server/tasks/test_custom_subclass_tasks.py
+++ b/tests/tasks/server/test_custom_subclass_tasks.py
@@ -12,9 +12,13 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
-from fastmcp.server.tasks import TaskConfig
from fastmcp.tools.base import Tool, ToolResult
from fastmcp.utilities.components import FastMCPComponent
+from fastmcp.utilities.tasks import TaskConfig
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
class CustomTool(Tool):
diff --git a/tests/server/tasks/test_notifications.py b/tests/tasks/server/test_notifications.py
similarity index 96%
rename from tests/server/tasks/test_notifications.py
rename to tests/tasks/server/test_notifications.py
index 4961c0822..32c6c90bc 100644
--- a/tests/server/tasks/test_notifications.py
+++ b/tests/tasks/server/test_notifications.py
@@ -9,14 +9,19 @@ import asyncio
import time
import mcp_types
+import pytest
+from fastmcp_tasks._legacy_wire.notifications import (
+ get_subscriber_count,
+)
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.elicitation import ElicitResult
from fastmcp.server.context import Context
from fastmcp.server.elicitation import AcceptedElicitation
-from fastmcp.server.tasks.notifications import (
- get_subscriber_count,
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
)
diff --git a/tests/server/tasks/test_progress_dependency.py b/tests/tasks/server/test_progress_dependency.py
similarity index 96%
rename from tests/server/tasks/test_progress_dependency.py
rename to tests/tasks/server/test_progress_dependency.py
index 5be2649c7..3cf751eb0 100644
--- a/tests/server/tasks/test_progress_dependency.py
+++ b/tests/tasks/server/test_progress_dependency.py
@@ -1,8 +1,14 @@
"""Tests for FastMCP Progress dependency."""
+import pytest
+
from fastmcp import FastMCP
from fastmcp.client import Client
-from fastmcp.dependencies import Progress
+from fastmcp.server.dependencies import Progress
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
async def test_progress_in_immediate_execution():
diff --git a/tests/server/tasks/test_server_tasks_parameter.py b/tests/tasks/server/test_server_tasks_parameter.py
similarity index 99%
rename from tests/server/tasks/test_server_tasks_parameter.py
rename to tests/tasks/server/test_server_tasks_parameter.py
index 45b777d6a..3f79a4820 100644
--- a/tests/server/tasks/test_server_tasks_parameter.py
+++ b/tests/tasks/server/test_server_tasks_parameter.py
@@ -11,6 +11,10 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.mark.timeout(10)
@pytest.mark.xfail(
diff --git a/tests/server/tasks/test_snapshot_restore.py b/tests/tasks/server/test_snapshot_restore.py
similarity index 96%
rename from tests/server/tasks/test_snapshot_restore.py
rename to tests/tasks/server/test_snapshot_restore.py
index 09a5241d1..9e30f0314 100644
--- a/tests/server/tasks/test_snapshot_restore.py
+++ b/tests/tasks/server/test_snapshot_restore.py
@@ -12,6 +12,13 @@ from __future__ import annotations
from unittest.mock import patch
+import pytest
+from fastmcp_tasks.context import (
+ TaskContextSnapshot,
+ _recall_snapshot,
+ get_task_context,
+ restore_task_snapshot,
+)
from mcp.server.auth.middleware.auth_context import auth_context_var
from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser
@@ -19,11 +26,9 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.auth import AccessToken
from fastmcp.server.dependencies import get_access_token
-from fastmcp.server.tasks.context import (
- TaskContextSnapshot,
- _recall_snapshot,
- get_task_context,
- restore_task_snapshot,
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
)
diff --git a/tests/server/tasks/test_sync_function_task_disabled.py b/tests/tasks/server/test_sync_function_task_disabled.py
similarity index 98%
rename from tests/server/tasks/test_sync_function_task_disabled.py
rename to tests/tasks/server/test_sync_function_task_disabled.py
index c5255b0b4..d6147f95f 100644
--- a/tests/server/tasks/test_sync_function_task_disabled.py
+++ b/tests/tasks/server/test_sync_function_task_disabled.py
@@ -12,6 +12,10 @@ from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.resources.function_resource import FunctionResource
from fastmcp.tools.function_tool import FunctionTool
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
async def test_sync_tool_with_explicit_task_true_raises():
"""Sync tool with task=True raises ValueError."""
diff --git a/tests/server/tasks/test_task_capabilities.py b/tests/tasks/server/test_task_capabilities.py
similarity index 94%
rename from tests/server/tasks/test_task_capabilities.py
rename to tests/tasks/server/test_task_capabilities.py
index e504cee53..a79bd753d 100644
--- a/tests/server/tasks/test_task_capabilities.py
+++ b/tests/tasks/server/test_task_capabilities.py
@@ -5,9 +5,15 @@ Verifies that the server correctly advertises task support.
Task protocol is now always enabled.
"""
+import pytest
+from fastmcp_tasks._legacy_wire.capabilities import get_task_capabilities
+
from fastmcp import FastMCP
from fastmcp.client import Client
-from fastmcp.server.tasks import get_task_capabilities
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
async def test_capabilities_include_tasks():
diff --git a/tests/server/tasks/test_task_config.py b/tests/tasks/server/test_task_config.py
similarity index 97%
rename from tests/server/tasks/test_task_config.py
rename to tests/tasks/server/test_task_config.py
index e10d7cff2..ba9cc8cb7 100644
--- a/tests/server/tasks/test_task_config.py
+++ b/tests/tasks/server/test_task_config.py
@@ -15,8 +15,8 @@ from mcp_types import Tool as MCPTool
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.exceptions import ToolError
-from fastmcp.server.tasks import TaskConfig
from fastmcp.tools.base import Tool
+from fastmcp.utilities.tasks import TaskConfig
class TestTaskConfigNormalization:
@@ -83,6 +83,7 @@ class TestTaskConfigNormalization:
assert tool2.task_config.mode == "optional"
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
class TestToolModeEnforcement:
"""Test mode enforcement for tools."""
@@ -159,6 +160,7 @@ class TestToolModeEnforcement:
assert result.data == "optional result"
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
class TestResourceModeEnforcement:
"""Test mode enforcement for resources."""
@@ -217,6 +219,7 @@ class TestResourceModeEnforcement:
assert "forbidden content" in str(result)
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
class TestPromptModeEnforcement:
"""Test mode enforcement for prompts."""
@@ -276,6 +279,7 @@ class TestPromptModeEnforcement:
assert "forbidden message" in str(result.messages[0].content)
+@pytest.mark.skip(reason="Phase 3: requires TasksExtension (SEP-2663 adapter)")
class TestToolExecutionMetadata:
"""Test that ToolExecution.task_support is set correctly in tool metadata."""
diff --git a/tests/server/tasks/test_task_dependencies.py b/tests/tasks/server/test_task_dependencies.py
similarity index 97%
rename from tests/server/tasks/test_task_dependencies.py
rename to tests/tasks/server/test_task_dependencies.py
index 1745f5e49..87304963d 100644
--- a/tests/server/tasks/test_task_dependencies.py
+++ b/tests/tasks/server/test_task_dependencies.py
@@ -9,11 +9,17 @@ from contextlib import asynccontextmanager
from typing import Any, cast
import pytest
+from fastmcp_tasks.dependencies import CurrentDocket
+from uncalled_for import Depends
from fastmcp import FastMCP
from fastmcp.client import Client
-from fastmcp.dependencies import CurrentDocket, CurrentFastMCP, Depends
from fastmcp.exceptions import ToolError
+from fastmcp.server.dependencies import CurrentFastMCP
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
@pytest.fixture
diff --git a/tests/server/tasks/test_task_elicitation_relay.py b/tests/tasks/server/test_task_elicitation_relay.py
similarity index 98%
rename from tests/server/tasks/test_task_elicitation_relay.py
rename to tests/tasks/server/test_task_elicitation_relay.py
index bf8d6e8b9..770b28801 100644
--- a/tests/server/tasks/test_task_elicitation_relay.py
+++ b/tests/tasks/server/test_task_elicitation_relay.py
@@ -13,6 +13,7 @@ These tests use Client(mcp, mode="legacy") with the real memory:// Docket backen
import asyncio
from dataclasses import dataclass
+import pytest
from pydantic import BaseModel
from fastmcp import FastMCP
@@ -25,6 +26,10 @@ from fastmcp.server.elicitation import (
DeclinedElicitation,
)
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
class TestElicitationRelay:
"""E2E tests for elicitation flowing through the standard MCP protocol."""
diff --git a/tests/server/tasks/test_task_keys.py b/tests/tasks/server/test_task_keys.py
similarity index 99%
rename from tests/server/tasks/test_task_keys.py
rename to tests/tasks/server/test_task_keys.py
index 06a64f8f1..7414cfdba 100644
--- a/tests/server/tasks/test_task_keys.py
+++ b/tests/tasks/server/test_task_keys.py
@@ -9,8 +9,7 @@ the Docket-key prefix and the Redis-key prefix.
"""
import pytest
-
-from fastmcp.server.tasks.keys import (
+from fastmcp_tasks.keys import (
build_task_key,
get_client_task_id_from_key,
parse_task_key,
diff --git a/tests/server/tasks/test_task_meta_parameter.py b/tests/tasks/server/test_task_meta_parameter.py
similarity index 98%
rename from tests/server/tasks/test_task_meta_parameter.py
rename to tests/tasks/server/test_task_meta_parameter.py
index bea973aba..4e81ad8b7 100644
--- a/tests/server/tasks/test_task_meta_parameter.py
+++ b/tests/tasks/server/test_task_meta_parameter.py
@@ -12,8 +12,12 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.exceptions import ToolError
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
-from fastmcp.server.tasks.config import TaskMeta
from fastmcp.tools.base import Tool, ToolResult
+from fastmcp.utilities.tasks import TaskMeta
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
class TestTaskMetaParameter:
diff --git a/tests/server/tasks/test_task_metadata.py b/tests/tasks/server/test_task_metadata.py
similarity index 95%
rename from tests/server/tasks/test_task_metadata.py
rename to tests/tasks/server/test_task_metadata.py
index 2bfdf4b13..a3cbb282c 100644
--- a/tests/server/tasks/test_task_metadata.py
+++ b/tests/tasks/server/test_task_metadata.py
@@ -10,6 +10,10 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def metadata_server():
diff --git a/tests/server/tasks/test_task_methods.py b/tests/tasks/server/test_task_methods.py
similarity index 98%
rename from tests/server/tasks/test_task_methods.py
rename to tests/tasks/server/test_task_methods.py
index 12c17f585..c99b8071c 100644
--- a/tests/server/tasks/test_task_methods.py
+++ b/tests/tasks/server/test_task_methods.py
@@ -13,6 +13,10 @@ from mcp.shared.exceptions import MCPError
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def endpoint_server():
diff --git a/tests/server/tasks/test_task_mount.py b/tests/tasks/server/test_task_mount.py
similarity index 99%
rename from tests/server/tasks/test_task_mount.py
rename to tests/tasks/server/test_task_mount.py
index e984f4969..9b4b28108 100644
--- a/tests/server/tasks/test_task_mount.py
+++ b/tests/tasks/server/test_task_mount.py
@@ -11,6 +11,7 @@ import time
import mcp_types as mt
import pytest
from docket import Docket
+from fastmcp_tasks.dependencies import CurrentDocket
from mcp_types import Tool as MCPTool
from mcp_types import ToolExecution
@@ -18,11 +19,15 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.prompts.base import PromptResult
from fastmcp.resources.base import ResourceResult
-from fastmcp.server.dependencies import CurrentDocket, CurrentFastMCP
+from fastmcp.server.dependencies import CurrentFastMCP
from fastmcp.server.middleware import CallNext, Middleware, MiddlewareContext
from fastmcp.server.providers.proxy import ProxyTool
-from fastmcp.server.tasks import TaskConfig
from fastmcp.tools.base import ToolResult
+from fastmcp.utilities.tasks import TaskConfig
+
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
@pytest.fixture(autouse=True)
diff --git a/tests/server/tasks/test_task_protocol.py b/tests/tasks/server/test_task_protocol.py
similarity index 96%
rename from tests/server/tasks/test_task_protocol.py
rename to tests/tasks/server/test_task_protocol.py
index f47648618..08461bb9a 100644
--- a/tests/server/tasks/test_task_protocol.py
+++ b/tests/tasks/server/test_task_protocol.py
@@ -10,6 +10,10 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def task_enabled_server():
diff --git a/tests/server/tasks/test_task_proxy.py b/tests/tasks/server/test_task_proxy.py
similarity index 98%
rename from tests/server/tasks/test_task_proxy.py
rename to tests/tasks/server/test_task_proxy.py
index c8abd5c5d..3d4219bac 100644
--- a/tests/server/tasks/test_task_proxy.py
+++ b/tests/tasks/server/test_task_proxy.py
@@ -19,6 +19,10 @@ from fastmcp.client import Client
from fastmcp.client.transports import FastMCPTransport
from fastmcp.server import create_proxy
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
def backend_server() -> FastMCP:
diff --git a/tests/server/tasks/test_task_return_types.py b/tests/tasks/server/test_task_return_types.py
similarity index 99%
rename from tests/server/tasks/test_task_return_types.py
rename to tests/tasks/server/test_task_return_types.py
index a8255a5a7..ddd80777c 100644
--- a/tests/server/tasks/test_task_return_types.py
+++ b/tests/tasks/server/test_task_return_types.py
@@ -20,6 +20,10 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.utilities.types import Audio, File, Image
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
class UserData(BaseModel):
"""Example structured output."""
diff --git a/tests/server/tasks/test_task_security.py b/tests/tasks/server/test_task_security.py
similarity index 98%
rename from tests/server/tasks/test_task_security.py
rename to tests/tasks/server/test_task_security.py
index 605382894..2ee18c6db 100644
--- a/tests/server/tasks/test_task_security.py
+++ b/tests/tasks/server/test_task_security.py
@@ -15,6 +15,10 @@ from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.server.auth import AccessToken
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
def task_server():
diff --git a/tests/server/tasks/test_task_status_notifications.py b/tests/tasks/server/test_task_status_notifications.py
similarity index 98%
rename from tests/server/tasks/test_task_status_notifications.py
rename to tests/tasks/server/test_task_status_notifications.py
index 1b1629d74..3487148f7 100644
--- a/tests/server/tasks/test_task_status_notifications.py
+++ b/tests/tasks/server/test_task_status_notifications.py
@@ -16,6 +16,10 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def notification_server():
diff --git a/tests/server/tasks/test_task_tools.py b/tests/tasks/server/test_task_tools.py
similarity index 98%
rename from tests/server/tasks/test_task_tools.py
rename to tests/tasks/server/test_task_tools.py
index c9d269bf7..15b4358c9 100644
--- a/tests/server/tasks/test_task_tools.py
+++ b/tests/tasks/server/test_task_tools.py
@@ -10,15 +10,19 @@ import functools
import mcp_types
import pytest
+from fastmcp_tasks.client import ToolTask
from pydantic import BaseModel
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.messages import MessageHandler
-from fastmcp.client.tasks import ToolTask
from fastmcp.exceptions import ToolError
from fastmcp.tools.function_tool import _resolve_param_hints
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def tool_server():
diff --git a/tests/server/tasks/test_task_ttl.py b/tests/tasks/server/test_task_ttl.py
similarity index 97%
rename from tests/server/tasks/test_task_ttl.py
rename to tests/tasks/server/test_task_ttl.py
index 0bb23a238..4464f8de3 100644
--- a/tests/server/tasks/test_task_ttl.py
+++ b/tests/tasks/server/test_task_ttl.py
@@ -12,6 +12,10 @@ import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
+pytestmark = pytest.mark.skip(
+ reason="Phase 3: requires TasksExtension (SEP-2663 adapter)"
+)
+
@pytest.fixture
async def keepalive_server():
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 3052a0d35..09d014a66 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -1,42 +1,6 @@
import pytest
-from fastmcp import settings
from fastmcp.settings import Settings
-from fastmcp.utilities.tests import temporary_settings
-
-
-def test_get_setting_reads_nested_values():
- test_settings = Settings()
-
- assert test_settings.get_setting("docket__name") == "fastmcp"
- assert test_settings.get_setting("docket__redelivery_timeout__seconds") == 300
-
-
-def test_set_setting_updates_nested_values():
- test_settings = Settings()
-
- test_settings.set_setting("docket__name", "worker-queue")
-
- assert test_settings.docket.name == "worker-queue"
- assert test_settings.get_setting("docket__name") == "worker-queue"
-
-
-def test_temporary_settings_restores_nested_values():
- original_name = settings.get_setting("docket__name")
-
- with temporary_settings(docket__name="temporary-queue"):
- assert settings.get_setting("docket__name") == "temporary-queue"
-
- assert settings.get_setting("docket__name") == original_name
-
-
-def test_get_setting_raises_for_missing_nested_parent():
- test_settings = Settings()
-
- with pytest.raises(AttributeError) as exc_info:
- test_settings.get_setting("docket__missing__value")
-
- assert str(exc_info.value) == "Setting missing does not exist."
def test_http_host_origin_protection_defaults_to_false():
diff --git a/tests/tools/tool/test_argument_validation.py b/tests/tools/tool/test_argument_validation.py
index 8f41cea5d..a526eae3f 100644
--- a/tests/tools/tool/test_argument_validation.py
+++ b/tests/tools/tool/test_argument_validation.py
@@ -86,23 +86,32 @@ class TestToolBodyErrors:
class TestTaskArgumentValidation:
- """The task-execution path (coerce_task_arguments) converts arg errors too."""
+ """The task-execution path (coerce_task_arguments) converts arg errors too.
+
+ The coercion logic moved to ``fastmcp_tasks.components`` during the
+ SEP-1686 -> SEP-2663 migration, keyed by component type instead of being a
+ method on the component.
+ """
def test_coerce_task_arguments_wrong_type(self):
+ from fastmcp_tasks.components import coerce_task_arguments
+
def tool_fn(n: int) -> int:
return n
tool = Tool.from_function(tool_fn)
with pytest.raises(ValidationError):
- tool.coerce_task_arguments({"n": "not-an-int"})
+ coerce_task_arguments(tool, {"n": "not-an-int"})
def test_coerce_task_arguments_constraint_violation(self):
+ from fastmcp_tasks.components import coerce_task_arguments
+
def tool_fn(n: Annotated[int, Field(le=10)]) -> int:
return n
tool = Tool.from_function(tool_fn)
with pytest.raises(ValidationError):
- tool.coerce_task_arguments({"n": 20})
+ coerce_task_arguments(tool, {"n": 20})
class TestValidCallsStillWork: