Remove enable_docket setting; Docket is now always on

Docket provides background task execution and is now always available
for all FastMCP servers. Only `enable_tasks` remains to control the
SEP-1686 task protocol support.

Changes:
- Remove `enable_docket` setting and related validation
- Docket/Worker lifecycle is always active in server lifespan
- CurrentDocket and CurrentWorker dependencies work without config
- Add server readiness signaling via `_started` event
- Fix test timing issues with proper port probing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Guidry 2025-12-05 11:39:25 -05:00
commit 47d3044b06
21 changed files with 150 additions and 303 deletions

View file

@ -55,11 +55,8 @@ Background tasks require explicit opt-in:
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `FASTMCP_ENABLE_TASKS` | `false` | Enable the MCP task protocol |
| `FASTMCP_ENABLE_DOCKET` | `false` | Enable the Docket task system |
| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |
Both `ENABLE_TASKS` and `ENABLE_DOCKET` must be `true` for background tasks to work.
You can also set a server-wide default in the constructor:
```python

View file

@ -2,9 +2,6 @@
# This file is loaded by direnv (https://direnv.net/) when you cd into this directory
# Run `direnv allow` to enable automatic environment loading
# Enable Docket support for background task execution
export FASTMCP_ENABLE_DOCKET=true
# Enable MCP SEP-1686 task protocol support
export FASTMCP_ENABLE_TASKS=true

View file

@ -51,7 +51,6 @@ fastmcp tasks worker server.py
| Variable | Default | Description |
|----------|---------|-------------|
| `FASTMCP_ENABLE_DOCKET` | `false` | Enable Docket task system |
| `FASTMCP_ENABLE_TASKS` | `false` | Enable MCP task protocol (SEP-1686) |
| `FASTMCP_DOCKET_URL` | `memory://` | Docket backend URL |

View file

@ -19,25 +19,17 @@ tasks_app = cyclopts.App(
)
def check_docket_enabled() -> None:
"""Check if Docket is enabled with a distributed backend.
def check_distributed_backend() -> None:
"""Check if Docket is configured with a distributed backend.
The CLI worker runs as a separate process, so it needs Redis/Valkey
to coordinate with the main server process.
Raises:
SystemExit: If Docket isn't enabled or using memory:// URL
SystemExit: If using memory:// URL
"""
import fastmcp
# Check if Docket is enabled
if not fastmcp.settings.enable_docket:
console.print(
"[bold red]✗ Docket not enabled[/bold red]\n\n"
"Docket task support is not enabled.\n\n"
"To enable Docket, set the environment variable:\n"
" [cyan]export FASTMCP_ENABLE_DOCKET=true[/cyan]\n\n"
"Then try again."
)
sys.exit(1)
docket_url = fastmcp.settings.docket.url
# Check for memory:// URL and provide helpful error
@ -86,7 +78,7 @@ def worker(
"""
import fastmcp
check_docket_enabled()
check_distributed_backend()
# Load server to get task functions
try:

View file

@ -859,7 +859,7 @@ class FastMCPTransport(ClientTransport):
experimental_capabilities = {}
if fastmcp.settings.enable_tasks:
# Declare SEP-1686 task support (enable_tasks requires enable_docket via validator)
# Declare SEP-1686 task support
experimental_capabilities["tasks"] = {
"tools": True,
"prompts": True,

View file

@ -297,21 +297,12 @@ class _CurrentDocket(Dependency):
"""Internal dependency class for CurrentDocket."""
async def __aenter__(self) -> Docket:
import fastmcp
# Check if flag is enabled
if not fastmcp.settings.enable_docket:
raise RuntimeError(
"Docket support is not enabled. "
"Set FASTMCP_ENABLE_DOCKET=true to enable Docket support."
)
# Get Docket from ContextVar (set by _docket_lifespan)
docket = _current_docket.get()
if docket is None:
raise RuntimeError(
"No Docket instance found. This should not happen when "
"FASTMCP_ENABLE_DOCKET is enabled."
"No Docket instance found. Docket is only available within "
"a running FastMCP server context."
)
return docket
@ -321,16 +312,13 @@ def CurrentDocket() -> Docket:
"""Get the current Docket instance managed by FastMCP.
This dependency provides access to the Docket instance that FastMCP
automatically creates when Docket support is enabled.
Requires:
- FASTMCP_ENABLE_DOCKET=true
automatically creates for background task scheduling.
Returns:
A dependency that resolves to the active Docket instance
Raises:
RuntimeError: If flag not enabled (during resolution)
RuntimeError: If not within a FastMCP server context
Example:
```python
@ -349,19 +337,11 @@ class _CurrentWorker(Dependency):
"""Internal dependency class for CurrentWorker."""
async def __aenter__(self) -> Worker:
import fastmcp
if not fastmcp.settings.enable_docket:
raise RuntimeError(
"Docket support is not enabled. "
"Set FASTMCP_ENABLE_DOCKET=true to enable Docket support."
)
worker = _current_worker.get()
if worker is None:
raise RuntimeError(
"No Worker instance found. This should not happen when "
"FASTMCP_ENABLE_DOCKET is enabled."
"No Worker instance found. Worker is only available within "
"a running FastMCP server context."
)
return worker
@ -371,16 +351,13 @@ def CurrentWorker() -> Worker:
"""Get the current Docket Worker instance managed by FastMCP.
This dependency provides access to the Worker instance that FastMCP
automatically creates when Docket support is enabled.
Requires:
- FASTMCP_ENABLE_DOCKET=true
automatically creates for background task processing.
Returns:
A dependency that resolves to the active Worker instance
Raises:
RuntimeError: If flag not enabled (during resolution)
RuntimeError: If not within a FastMCP server context
Example:
```python
@ -463,8 +440,7 @@ class Progress(DocketProgress):
docket = _current_docket.get()
if docket is None:
raise RuntimeError(
"Progress dependency requires Docket to be enabled. "
"Set FASTMCP_ENABLE_DOCKET=true"
"Progress dependency requires a FastMCP server context."
) from None
# Return in-memory progress for immediate execution

View file

@ -19,6 +19,7 @@ from contextlib import (
AbstractAsyncContextManager,
AsyncExitStack,
asynccontextmanager,
suppress,
)
from dataclasses import dataclass
from functools import partial
@ -213,6 +214,7 @@ class FastMCP(Generic[LifespanResultT]):
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
self._lifespan_result: LifespanResultT | None = None
self._lifespan_result_set: bool = False
self._started: asyncio.Event = asyncio.Event()
# Generate random ID if no name provided
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
@ -374,34 +376,16 @@ class FastMCP(Generic[LifespanResultT]):
return self._docket
@asynccontextmanager
async def _docket_lifespan(
self, user_lifespan_result: LifespanResultT
) -> AsyncIterator[LifespanResultT]:
"""Manage Docket instance and Worker when experimental support is enabled.
Args:
user_lifespan_result: The result from the user's lifespan function
Yields:
User's lifespan result (Docket is managed via ContextVar, not lifespan result)
"""
async def _docket_lifespan(self) -> AsyncIterator[None]:
"""Manage Docket instance and Worker for background task execution."""
from fastmcp import settings
from fastmcp.server.dependencies import _current_docket, _current_worker
# Validate configuration
if settings.enable_tasks and not settings.enable_docket:
raise RuntimeError(
"Server requires enable_docket=True when enable_tasks=True. "
"Task protocol support needs Docket for background execution."
)
if not settings.enable_docket:
# Docket support not enabled, pass through user lifespan result
yield user_lifespan_result
return
# Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
from fastmcp.server.dependencies import _current_server
from fastmcp.server.dependencies import (
_current_docket,
_current_server,
_current_worker,
)
server_token = _current_server.set(weakref.ref(self))
@ -414,9 +398,10 @@ class FastMCP(Generic[LifespanResultT]):
# Store on server instance for cross-task access (FastMCPTransport)
self._docket = docket
# Register task-enabled tools/prompts/resources with Docket
tools = await self.get_tools()
for tool in tools.values():
# Register local task-enabled tools/prompts/resources with Docket
for tool in self._tool_manager._tools.values():
if not hasattr(tool, "fn"):
continue
supports_task = (
tool.task
if tool.task is not None
@ -425,8 +410,9 @@ class FastMCP(Generic[LifespanResultT]):
if supports_task:
docket.register(tool.fn)
prompts = await self.get_prompts()
for prompt in prompts.values():
for prompt in self._prompt_manager._prompts.values():
if not hasattr(prompt, "fn"):
continue
supports_task = (
prompt.task
if prompt.task is not None
@ -435,8 +421,9 @@ class FastMCP(Generic[LifespanResultT]):
if supports_task:
docket.register(prompt.fn)
resources = await self.get_resources()
for resource in resources.values():
for resource in self._resource_manager._resources.values():
if not hasattr(resource, "fn"):
continue
supports_task = (
resource.task
if resource.task is not None
@ -445,6 +432,17 @@ class FastMCP(Generic[LifespanResultT]):
if supports_task:
docket.register(resource.fn)
for template in self._resource_manager._templates.values():
if not hasattr(template, "fn"):
continue
supports_task = (
template.task
if template.task is not None
else self._support_tasks_by_default
)
if supports_task:
docket.register(template.fn)
# Set Docket in ContextVar so CurrentDocket can access it
docket_token = _current_docket.set(docket)
try:
@ -457,22 +455,19 @@ class FastMCP(Generic[LifespanResultT]):
if settings.docket.worker_name:
worker_kwargs["name"] = settings.docket.worker_name
# Create and start Worker, then task group for run_forever()
async with (
Worker(docket, **worker_kwargs) as worker, # type: ignore[arg-type]
anyio.create_task_group() as tg,
):
# Create and start Worker
async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
# Set Worker in ContextVar so CurrentWorker can access it
worker_token = _current_worker.set(worker)
try:
# Start worker as background task
tg.start_soon(worker.run_forever)
worker_task = asyncio.create_task(worker.run_forever())
try:
yield user_lifespan_result
yield
finally:
# Cancel task group when exiting (cancels worker)
tg.cancel_scope.cancel()
# Cancel worker task on exit
worker_task.cancel()
with suppress(asyncio.CancelledError):
await worker_task
finally:
_current_worker.reset(worker_token)
finally:
@ -492,9 +487,9 @@ class FastMCP(Generic[LifespanResultT]):
async with (
self._lifespan(self) as user_lifespan_result,
self._docket_lifespan(user_lifespan_result) as lifespan_result,
self._docket_lifespan(),
):
self._lifespan_result = lifespan_result
self._lifespan_result = user_lifespan_result
self._lifespan_result_set = True
async with AsyncExitStack[bool | None]() as stack:
@ -503,7 +498,11 @@ class FastMCP(Generic[LifespanResultT]):
cm=server.server._lifespan_manager()
)
yield
self._started.set()
try:
yield
finally:
self._started.clear()
self._lifespan_result_set = False
self._lifespan_result = None

View file

@ -61,7 +61,7 @@ async def handle_tool_as_task(
raise McpError(
ErrorData(
code=INTERNAL_ERROR,
message="Background tasks require Docket. Set FASTMCP_ENABLE_DOCKET=true",
message="Background tasks require a running FastMCP server context",
)
)
@ -169,7 +169,7 @@ async def handle_prompt_as_task(
raise McpError(
ErrorData(
code=INTERNAL_ERROR,
message="Background tasks require Docket. Set FASTMCP_ENABLE_DOCKET=true",
message="Background tasks require a running FastMCP server context",
)
)

View file

@ -209,21 +209,7 @@ class Settings(BaseSettings):
experimental: ExperimentalSettings = ExperimentalSettings()
# Docket/Tasks settings
enable_docket: Annotated[
bool,
Field(
description=inspect.cleandoc(
"""
Enable Docket support for background task execution.
When enabled, FastMCP will create a Docket instance with a Worker
available via dependency injection. This allows tools, prompts, and
resources to schedule background work using CurrentDocket().
"""
),
),
] = False
# Tasks settings
enable_tasks: Annotated[
bool,
Field(
@ -231,11 +217,11 @@ class Settings(BaseSettings):
"""
Enable MCP SEP-1686 task protocol support for background execution.
Server-side: Requires enable_docket=True (validated at server startup).
Advertises task capabilities and handles task/* protocol methods.
Server-side: Advertises task capabilities and handles task/* protocol
methods. Tools, prompts, and resources marked with task=True will
execute in the background via Docket.
Client-side: Advertises task capability to servers. No Docket needed
on client side.
Client-side: Advertises task capability to servers.
"""
),
),

View file

@ -22,6 +22,36 @@ if TYPE_CHECKING:
from fastmcp.server.server import FastMCP
async def _wait_for_port(
host: str, port: int, timeout: float = 5.0, interval: float = 0.01
) -> None:
"""Wait for a port to accept connections.
Args:
host: Host to connect to
port: Port to probe
timeout: Maximum time to wait in seconds
interval: Time between connection attempts
"""
import asyncio
start = asyncio.get_running_loop().time()
while True:
try:
_, writer = await asyncio.wait_for(
asyncio.open_connection(host, port), timeout=interval
)
writer.close()
await writer.wait_closed()
return
except (OSError, asyncio.TimeoutError):
if asyncio.get_running_loop().time() - start > timeout:
raise TimeoutError(
f"Port {port} on {host} not available after {timeout}s"
) from None
await asyncio.sleep(interval)
@contextmanager
def temporary_settings(**kwargs: Any):
"""
@ -209,16 +239,20 @@ async def run_server_async(
)
)
# Give the server a moment to start
await asyncio.sleep(0.1)
# Wait for server lifespan to be ready
await server._started.wait()
# Wait for HTTP port to be listening (lifespan is ready but uvicorn
# may not have bound yet)
await _wait_for_port(host, port)
try:
yield f"http://{host}:{port}{path}"
finally:
# Cleanup: cancel the task
# Cleanup: cancel the task with timeout to avoid hanging on Windows
server_task.cancel()
with suppress(asyncio.CancelledError):
await server_task
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
await asyncio.wait_for(server_task, timeout=2.0)
@contextmanager

View file

@ -2,38 +2,23 @@
import pytest
from fastmcp.cli.tasks import check_docket_enabled, tasks_app
from fastmcp.cli.tasks import check_distributed_backend, tasks_app
from fastmcp.utilities.tests import temporary_settings
class TestCheckDocketEnabled:
"""Test the Docket enabled checker function."""
class TestCheckDistributedBackend:
"""Test the distributed backend checker function."""
def test_succeeds_when_docket_enabled_with_redis(self):
"""Test that it succeeds when Docket is enabled with Redis."""
with temporary_settings(
enable_docket=True,
docket__url="redis://localhost:6379/0",
):
check_docket_enabled()
def test_exits_when_docket_not_enabled(self):
"""Test that it exits with helpful error when Docket not enabled."""
with temporary_settings(enable_docket=False):
with pytest.raises(SystemExit) as exc_info:
check_docket_enabled()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1
def test_succeeds_with_redis_url(self):
"""Test that it succeeds with Redis URL."""
with temporary_settings(docket__url="redis://localhost:6379/0"):
check_distributed_backend()
def test_exits_with_helpful_error_for_memory_url(self):
"""Test that it exits with helpful error for memory:// URLs."""
with temporary_settings(
enable_docket=True,
docket__url="memory://test-123",
):
with temporary_settings(docket__url="memory://test-123"):
with pytest.raises(SystemExit) as exc_info:
check_docket_enabled()
check_distributed_backend()
assert isinstance(exc_info.value, SystemExit)
assert exc_info.value.code == 1

View file

@ -6,10 +6,7 @@ from fastmcp.utilities.tests import temporary_settings
@pytest.fixture(autouse=True)
async def enable_docket_and_tasks():
"""Enable Docket and task protocol support for all client task tests."""
with temporary_settings(
enable_docket=True,
enable_tasks=True,
):
async def enable_tasks():
"""Enable task protocol support for all client task tests."""
with temporary_settings(enable_tasks=True):
yield

View file

@ -58,8 +58,7 @@ async def stateless_server(stateful_proxy_server: FastMCP):
host="127.0.0.1", port=port, stateless_http=True
)
)
async with Client(transport=url) as client:
assert await client.ping()
await stateful_proxy_server._started.wait()
yield url
task.cancel()
try:

View file

@ -6,16 +6,12 @@ from fastmcp.utilities.tests import temporary_settings
@pytest.fixture(autouse=True)
async def enable_docket_and_tasks():
"""Enable Docket and task protocol support for all task tests."""
with temporary_settings(
enable_docket=True,
enable_tasks=True,
):
# Verify both are enabled
async def enable_tasks():
"""Enable task protocol support for all task tests."""
with temporary_settings(enable_tasks=True):
# Verify enabled
import fastmcp
assert fastmcp.settings.enable_docket, "Docket should be enabled after fixture"
assert fastmcp.settings.enable_tasks, "Tasks should be enabled after fixture"
yield

View file

@ -1,11 +1,8 @@
"""Tests for FastMCP Progress dependency."""
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.dependencies import Progress
from fastmcp.utilities.tests import temporary_settings
async def test_progress_in_immediate_execution():
@ -111,24 +108,6 @@ async def test_progress_status_message_in_background_task():
assert result.content[0].text == "done"
async def test_progress_fails_without_docket():
"""Test Progress dependency fails when Docket is not enabled."""
with temporary_settings(enable_docket=False, enable_tasks=False):
mcp = FastMCP("test")
@mcp.tool()
async def test_tool(progress: Progress = Progress()) -> str:
return "done"
async with Client(mcp) as client:
with pytest.raises(Exception) as exc_info:
await client.call_tool("test_tool", {})
error_str = str(exc_info.value)
assert "Failed to resolve dependency" in error_str
assert "progress" in error_str
async def test_inmemory_progress_state():
"""Test that in-memory progress stores and returns state correctly."""
mcp = FastMCP("test")

View file

@ -81,10 +81,7 @@ async def test_server_tasks_false_defaults_all_components():
async def test_server_tasks_none_uses_settings():
"""Server with tasks=None (or omitted) uses global settings."""
# Test with enable_tasks=True in settings
with temporary_settings(
enable_docket=True,
enable_tasks=True,
):
with temporary_settings(enable_tasks=True):
mcp = FastMCP("test") # tasks=None, should use settings
@mcp.tool()
@ -97,10 +94,7 @@ async def test_server_tasks_none_uses_settings():
assert not tool_task.returned_immediately
# Test with enable_tasks=False in settings
with temporary_settings(
enable_docket=True,
enable_tasks=False,
):
with temporary_settings(enable_tasks=False):
mcp2 = FastMCP("test2") # tasks=None, should use settings
@mcp2.tool()
@ -249,10 +243,7 @@ async def test_mixed_explicit_and_inherited():
async def test_server_tasks_parameter_sets_component_defaults():
"""Server tasks parameter sets component defaults but global settings gate protocol."""
# Server tasks=True sets component defaults, but enable_tasks must be True
with temporary_settings(
enable_docket=True,
enable_tasks=True,
):
with temporary_settings(enable_tasks=True):
mcp = FastMCP("test", tasks=True)
@mcp.tool()
@ -265,10 +256,7 @@ async def test_server_tasks_parameter_sets_component_defaults():
assert not tool_task.returned_immediately
# Server tasks=False sets component defaults
with temporary_settings(
enable_docket=True,
enable_tasks=True,
):
with temporary_settings(enable_tasks=True):
mcp2 = FastMCP("test2", tasks=False)
@mcp2.tool()

View file

@ -4,8 +4,6 @@ Tests for SEP-1686 task capabilities declaration.
Verifies that the server correctly advertises task support based on settings.
"""
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.utilities.tests import temporary_settings
@ -13,10 +11,7 @@ from fastmcp.utilities.tests import temporary_settings
async def test_capabilities_include_tasks_when_enabled():
"""Server capabilities include tasks when enable_tasks=True."""
with temporary_settings(
enable_docket=True,
enable_tasks=True,
):
with temporary_settings(enable_tasks=True):
mcp = FastMCP("capability-test")
@mcp.tool()
@ -40,10 +35,7 @@ async def test_capabilities_include_tasks_when_enabled():
async def test_capabilities_exclude_tasks_when_disabled():
"""Server capabilities do NOT include tasks when enable_tasks=False."""
with temporary_settings(
enable_docket=True,
enable_tasks=False,
):
with temporary_settings(enable_tasks=False):
mcp = FastMCP("capability-test")
@mcp.tool()
@ -59,51 +51,9 @@ async def test_capabilities_exclude_tasks_when_disabled():
assert "tasks" not in init_result.capabilities.experimental
async def test_capabilities_exclude_tasks_when_docket_disabled():
"""Server capabilities do NOT include tasks when enable_docket=False."""
with temporary_settings(
enable_docket=False,
enable_tasks=False,
):
mcp = FastMCP("capability-test")
@mcp.tool()
def test_tool() -> str:
return "test"
async with Client(mcp) as client:
# Get server initialization result
init_result = client.initialize_result
# Verify tasks capability is NOT present
if init_result.capabilities.experimental:
assert "tasks" not in init_result.capabilities.experimental
async def test_enable_tasks_requires_enable_docket():
"""Setting enable_tasks=True without enable_docket=True raises error at server startup."""
with temporary_settings(
enable_docket=False,
enable_tasks=True,
):
mcp = FastMCP("config-test")
@mcp.tool()
async def test_tool() -> str:
return "test"
# Should fail when trying to start server (during lifespan)
with pytest.raises(RuntimeError, match="requires.*enable_docket.*enable_tasks"):
async with Client(mcp):
pass # Should never reach here
async def test_client_advertises_task_capability_when_enabled():
"""Client advertises experimental.tasks capability when enable_tasks=True."""
with temporary_settings(
enable_docket=True,
enable_tasks=True,
):
with temporary_settings(enable_tasks=True):
mcp = FastMCP("client-cap-test")
@mcp.tool()
@ -117,10 +67,7 @@ async def test_client_advertises_task_capability_when_enabled():
async def test_client_does_not_advertise_tasks_when_disabled():
"""Client does NOT use custom session when enable_tasks=False."""
with temporary_settings(
enable_docket=True,
enable_tasks=False,
):
with temporary_settings(enable_tasks=False):
mcp = FastMCP("no-tasks-client-test")
@mcp.tool()

View file

@ -36,7 +36,7 @@ async def test_uvicorn_logging_default_level(
server_task = asyncio.create_task(
mcp_server.run_http_async(log_level=test_log_level, port=8003)
)
await asyncio.sleep(0.01)
await mcp_server._started.wait()
mock_uvicorn_config_constructor.assert_called_once()
_, kwargs_config = mock_uvicorn_config_constructor.call_args
@ -96,7 +96,7 @@ async def test_uvicorn_logging_with_custom_log_config(
uvicorn_config={"log_config": sample_log_config}, port=8004
)
)
await asyncio.sleep(0.01)
await mcp_server._started.wait()
mock_uvicorn_config_constructor.assert_called_once()
_, kwargs_config = mock_uvicorn_config_constructor.call_args
@ -159,7 +159,7 @@ async def test_uvicorn_logging_custom_log_config_overrides_log_level_param(
port=8005,
)
)
await asyncio.sleep(0.01)
await mcp_server._started.wait()
mock_uvicorn_config_constructor.assert_called_once()
_, kwargs_config = mock_uvicorn_config_constructor.call_args

View file

@ -950,10 +950,8 @@ class TestAsProxyKwarg:
async with Client(mcp) as client:
await client.call_tool("hello", {})
assert len(lifespan_check) > 0
# in the present implementation the sub server will be invoked 3 times
# to call its tool
assert lifespan_check.count("start") >= 2
# Lifespan is entered exactly once and kept alive by Docket worker
assert lifespan_check == ["start"]
class TestResourceNamePrefixing:

View file

@ -3,43 +3,19 @@
import asyncio
from contextlib import asynccontextmanager
import pytest
from docket import Docket
from docket.worker import Worker
from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.dependencies import CurrentDocket, CurrentWorker
from fastmcp.exceptions import ToolError
from fastmcp.server.dependencies import get_context
from fastmcp.utilities.tests import temporary_settings
HUZZAH = "huzzah!"
@pytest.fixture(autouse=True)
def enable_docket():
"""Enable Docket support for all tests in this suite."""
with temporary_settings(enable_docket=True):
yield
async def test_docket_disabled():
"""Verify that Docket errors when flag is disabled."""
with temporary_settings(enable_docket=False):
mcp = FastMCP("test-server")
@mcp.tool()
def needs_docket(docket: Docket = CurrentDocket()) -> str:
return f"Got docket: {type(docket).__name__}"
async with Client(mcp) as client:
with pytest.raises(ToolError, match="Failed to resolve dependency"):
await client.call_tool("needs_docket", {})
async def test_current_docket_with_flag_enabled():
"""CurrentDocket dependency works when experimental flag is enabled."""
async def test_current_docket():
"""CurrentDocket dependency provides access to Docket instance."""
mcp = FastMCP("test-server")
@mcp.tool()
@ -52,8 +28,8 @@ async def test_current_docket_with_flag_enabled():
assert HUZZAH in str(result)
async def test_current_worker_with_flag_enabled():
"""CurrentWorker dependency works when experimental flag is enabled."""
async def test_current_worker():
"""CurrentWorker dependency provides access to Worker instance."""
mcp = FastMCP("test-server")
@mcp.tool()
@ -100,7 +76,7 @@ async def test_worker_executes_background_tasks():
async def test_current_docket_in_resource():
"""CurrentDocket works in resources when flag is enabled."""
"""CurrentDocket works in resources."""
mcp = FastMCP("test-server")
@mcp.resource("docket://info")
@ -114,7 +90,7 @@ async def test_current_docket_in_resource():
async def test_current_docket_in_prompt():
"""CurrentDocket works in prompts when flag is enabled."""
"""CurrentDocket works in prompts."""
mcp = FastMCP("test-server")
@mcp.prompt()
@ -128,7 +104,7 @@ async def test_current_docket_in_prompt():
async def test_current_docket_in_resource_template():
"""CurrentDocket works in resource templates when flag is enabled."""
"""CurrentDocket works in resource templates."""
mcp = FastMCP("test-server")
@mcp.resource("docket://tasks/{task_id}")

View file

@ -17,9 +17,11 @@ class TestServerLifespan:
@asynccontextmanager
async def server_lifespan(mcp: FastMCP) -> AsyncIterator[dict[str, Any]]:
_ = lifespan_events.append("enter")
yield {"initialized": True}
_ = lifespan_events.append("exit")
lifespan_events.append("enter")
try:
yield {"initialized": True}
finally:
lifespan_events.append("exit")
mcp = FastMCP("TestServer", lifespan=server_lifespan)