Skip to content

Commit 960ce77

Browse files
authored
Merge pull request #2558 from jlowin/unify-enable-tasks
Remove enable_docket setting; Docket is now always on
2 parents e47abb4 + e63855c commit 960ce77

File tree

21 files changed

+121
-302
lines changed

21 files changed

+121
-302
lines changed

docs/servers/tasks.mdx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,8 @@ Background tasks require explicit opt-in:
5555
| Environment Variable | Default | Description |
5656
|---------------------|---------|-------------|
5757
| `FASTMCP_ENABLE_TASKS` | `false` | Enable the MCP task protocol |
58-
| `FASTMCP_ENABLE_DOCKET` | `false` | Enable the Docket task system |
5958
| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |
6059

61-
Both `ENABLE_TASKS` and `ENABLE_DOCKET` must be `true` for background tasks to work.
62-
6360
You can also set a server-wide default in the constructor:
6461

6562
```python

examples/tasks/.envrc

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
# This file is loaded by direnv (https://direnv.net/) when you cd into this directory
33
# Run `direnv allow` to enable automatic environment loading
44

5-
# Enable Docket support for background task execution
6-
export FASTMCP_ENABLE_DOCKET=true
7-
85
# Enable MCP SEP-1686 task protocol support
96
export FASTMCP_ENABLE_TASKS=true
107

examples/tasks/README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ fastmcp tasks worker server.py
5151

5252
| Variable | Default | Description |
5353
|----------|---------|-------------|
54-
| `FASTMCP_ENABLE_DOCKET` | `false` | Enable Docket task system |
5554
| `FASTMCP_ENABLE_TASKS` | `false` | Enable MCP task protocol (SEP-1686) |
5655
| `FASTMCP_DOCKET_URL` | `memory://` | Docket backend URL |
5756

src/fastmcp/cli/tasks.py

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,25 +19,17 @@
1919
)
2020

2121

22-
def check_docket_enabled() -> None:
23-
"""Check if Docket is enabled with a distributed backend.
22+
def check_distributed_backend() -> None:
23+
"""Check if Docket is configured with a distributed backend.
24+
25+
The CLI worker runs as a separate process, so it needs Redis/Valkey
26+
to coordinate with the main server process.
2427
2528
Raises:
26-
SystemExit: If Docket isn't enabled or using memory:// URL
29+
SystemExit: If using memory:// URL
2730
"""
2831
import fastmcp
2932

30-
# Check if Docket is enabled
31-
if not fastmcp.settings.enable_docket:
32-
console.print(
33-
"[bold red]✗ Docket not enabled[/bold red]\n\n"
34-
"Docket task support is not enabled.\n\n"
35-
"To enable Docket, set the environment variable:\n"
36-
" [cyan]export FASTMCP_ENABLE_DOCKET=true[/cyan]\n\n"
37-
"Then try again."
38-
)
39-
sys.exit(1)
40-
4133
docket_url = fastmcp.settings.docket.url
4234

4335
# Check for memory:// URL and provide helpful error
@@ -86,7 +78,7 @@ def worker(
8678
"""
8779
import fastmcp
8880

89-
check_docket_enabled()
81+
check_distributed_backend()
9082

9183
# Load server to get task functions
9284
try:

src/fastmcp/client/transports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ async def connect_session(
859859

860860
experimental_capabilities = {}
861861
if fastmcp.settings.enable_tasks:
862-
# Declare SEP-1686 task support (enable_tasks requires enable_docket via validator)
862+
# Declare SEP-1686 task support
863863
experimental_capabilities["tasks"] = {
864864
"tools": True,
865865
"prompts": True,

src/fastmcp/server/dependencies.py

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -297,21 +297,12 @@ class _CurrentDocket(Dependency):
297297
"""Internal dependency class for CurrentDocket."""
298298

299299
async def __aenter__(self) -> Docket:
300-
import fastmcp
301-
302-
# Check if flag is enabled
303-
if not fastmcp.settings.enable_docket:
304-
raise RuntimeError(
305-
"Docket support is not enabled. "
306-
"Set FASTMCP_ENABLE_DOCKET=true to enable Docket support."
307-
)
308-
309300
# Get Docket from ContextVar (set by _docket_lifespan)
310301
docket = _current_docket.get()
311302
if docket is None:
312303
raise RuntimeError(
313-
"No Docket instance found. This should not happen when "
314-
"FASTMCP_ENABLE_DOCKET is enabled."
304+
"No Docket instance found. Docket is only available within "
305+
"a running FastMCP server context."
315306
)
316307

317308
return docket
@@ -321,16 +312,13 @@ def CurrentDocket() -> Docket:
321312
"""Get the current Docket instance managed by FastMCP.
322313
323314
This dependency provides access to the Docket instance that FastMCP
324-
automatically creates when Docket support is enabled.
325-
326-
Requires:
327-
- FASTMCP_ENABLE_DOCKET=true
315+
automatically creates for background task scheduling.
328316
329317
Returns:
330318
A dependency that resolves to the active Docket instance
331319
332320
Raises:
333-
RuntimeError: If flag not enabled (during resolution)
321+
RuntimeError: If not within a FastMCP server context
334322
335323
Example:
336324
```python
@@ -349,19 +337,11 @@ class _CurrentWorker(Dependency):
349337
"""Internal dependency class for CurrentWorker."""
350338

351339
async def __aenter__(self) -> Worker:
352-
import fastmcp
353-
354-
if not fastmcp.settings.enable_docket:
355-
raise RuntimeError(
356-
"Docket support is not enabled. "
357-
"Set FASTMCP_ENABLE_DOCKET=true to enable Docket support."
358-
)
359-
360340
worker = _current_worker.get()
361341
if worker is None:
362342
raise RuntimeError(
363-
"No Worker instance found. This should not happen when "
364-
"FASTMCP_ENABLE_DOCKET is enabled."
343+
"No Worker instance found. Worker is only available within "
344+
"a running FastMCP server context."
365345
)
366346

367347
return worker
@@ -371,16 +351,13 @@ def CurrentWorker() -> Worker:
371351
"""Get the current Docket Worker instance managed by FastMCP.
372352
373353
This dependency provides access to the Worker instance that FastMCP
374-
automatically creates when Docket support is enabled.
375-
376-
Requires:
377-
- FASTMCP_ENABLE_DOCKET=true
354+
automatically creates for background task processing.
378355
379356
Returns:
380357
A dependency that resolves to the active Worker instance
381358
382359
Raises:
383-
RuntimeError: If flag not enabled (during resolution)
360+
RuntimeError: If not within a FastMCP server context
384361
385362
Example:
386363
```python
@@ -463,8 +440,7 @@ async def __aenter__(self) -> DocketProgress:
463440
docket = _current_docket.get()
464441
if docket is None:
465442
raise RuntimeError(
466-
"Progress dependency requires Docket to be enabled. "
467-
"Set FASTMCP_ENABLE_DOCKET=true"
443+
"Progress dependency requires a FastMCP server context."
468444
) from None
469445

470446
# Return in-memory progress for immediate execution

src/fastmcp/server/server.py

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import inspect
67
import re
78
import secrets
@@ -19,6 +20,7 @@
1920
AbstractAsyncContextManager,
2021
AsyncExitStack,
2122
asynccontextmanager,
23+
suppress,
2224
)
2325
from dataclasses import dataclass
2426
from functools import partial
@@ -213,6 +215,7 @@ def __init__(
213215
self._lifespan: LifespanCallable[LifespanResultT] = lifespan or default_lifespan
214216
self._lifespan_result: LifespanResultT | None = None
215217
self._lifespan_result_set: bool = False
218+
self._started: asyncio.Event = asyncio.Event()
216219

217220
# Generate random ID if no name provided
218221
self._mcp_server: LowLevelServer[LifespanResultT, Any] = LowLevelServer[
@@ -374,34 +377,16 @@ def docket(self) -> Docket | None:
374377
return self._docket
375378

376379
@asynccontextmanager
377-
async def _docket_lifespan(
378-
self, user_lifespan_result: LifespanResultT
379-
) -> AsyncIterator[LifespanResultT]:
380-
"""Manage Docket instance and Worker when experimental support is enabled.
381-
382-
Args:
383-
user_lifespan_result: The result from the user's lifespan function
384-
385-
Yields:
386-
User's lifespan result (Docket is managed via ContextVar, not lifespan result)
387-
"""
380+
async def _docket_lifespan(self) -> AsyncIterator[None]:
381+
"""Manage Docket instance and Worker for background task execution."""
388382
from fastmcp import settings
389-
from fastmcp.server.dependencies import _current_docket, _current_worker
390-
391-
# Validate configuration
392-
if settings.enable_tasks and not settings.enable_docket:
393-
raise RuntimeError(
394-
"Server requires enable_docket=True when enable_tasks=True. "
395-
"Task protocol support needs Docket for background execution."
396-
)
397-
398-
if not settings.enable_docket:
399-
# Docket support not enabled, pass through user lifespan result
400-
yield user_lifespan_result
401-
return
402383

403384
# Set FastMCP server in ContextVar so CurrentFastMCP can access it (use weakref to avoid reference cycles)
404-
from fastmcp.server.dependencies import _current_server
385+
from fastmcp.server.dependencies import (
386+
_current_docket,
387+
_current_server,
388+
_current_worker,
389+
)
405390

406391
server_token = _current_server.set(weakref.ref(self))
407392

@@ -414,9 +399,10 @@ async def _docket_lifespan(
414399
# Store on server instance for cross-task access (FastMCPTransport)
415400
self._docket = docket
416401

417-
# Register task-enabled tools/prompts/resources with Docket
418-
tools = await self.get_tools()
419-
for tool in tools.values():
402+
# Register local task-enabled tools/prompts/resources with Docket
403+
for tool in self._tool_manager._tools.values():
404+
if not hasattr(tool, "fn"):
405+
continue
420406
supports_task = (
421407
tool.task
422408
if tool.task is not None
@@ -425,8 +411,9 @@ async def _docket_lifespan(
425411
if supports_task:
426412
docket.register(tool.fn)
427413

428-
prompts = await self.get_prompts()
429-
for prompt in prompts.values():
414+
for prompt in self._prompt_manager._prompts.values():
415+
if not hasattr(prompt, "fn"):
416+
continue
430417
supports_task = (
431418
prompt.task
432419
if prompt.task is not None
@@ -435,8 +422,9 @@ async def _docket_lifespan(
435422
if supports_task:
436423
docket.register(prompt.fn)
437424

438-
resources = await self.get_resources()
439-
for resource in resources.values():
425+
for resource in self._resource_manager._resources.values():
426+
if not hasattr(resource, "fn"):
427+
continue
440428
supports_task = (
441429
resource.task
442430
if resource.task is not None
@@ -445,6 +433,17 @@ async def _docket_lifespan(
445433
if supports_task:
446434
docket.register(resource.fn)
447435

436+
for template in self._resource_manager._templates.values():
437+
if not hasattr(template, "fn"):
438+
continue
439+
supports_task = (
440+
template.task
441+
if template.task is not None
442+
else self._support_tasks_by_default
443+
)
444+
if supports_task:
445+
docket.register(template.fn)
446+
448447
# Set Docket in ContextVar so CurrentDocket can access it
449448
docket_token = _current_docket.set(docket)
450449
try:
@@ -457,22 +456,21 @@ async def _docket_lifespan(
457456
if settings.docket.worker_name:
458457
worker_kwargs["name"] = settings.docket.worker_name
459458

460-
# Create and start Worker, then task group for run_forever()
461-
async with (
462-
Worker(docket, **worker_kwargs) as worker, # type: ignore[arg-type]
463-
anyio.create_task_group() as tg,
464-
):
459+
# Create and start Worker
460+
async with Worker(docket, **worker_kwargs) as worker: # type: ignore[arg-type]
465461
# Set Worker in ContextVar so CurrentWorker can access it
466462
worker_token = _current_worker.set(worker)
467463
try:
468-
# Start worker as background task
469-
tg.start_soon(worker.run_forever)
470-
464+
worker_task = asyncio.create_task(worker.run_forever())
471465
try:
472-
yield user_lifespan_result
466+
yield
473467
finally:
474-
# Cancel task group when exiting (cancels worker)
475-
tg.cancel_scope.cancel()
468+
# Cancel worker task on exit with timeout to prevent hanging
469+
worker_task.cancel()
470+
with suppress(
471+
asyncio.CancelledError, asyncio.TimeoutError
472+
):
473+
await asyncio.wait_for(worker_task, timeout=2.0)
476474
finally:
477475
_current_worker.reset(worker_token)
478476
finally:
@@ -492,9 +490,9 @@ async def _lifespan_manager(self) -> AsyncIterator[None]:
492490

493491
async with (
494492
self._lifespan(self) as user_lifespan_result,
495-
self._docket_lifespan(user_lifespan_result) as lifespan_result,
493+
self._docket_lifespan(),
496494
):
497-
self._lifespan_result = lifespan_result
495+
self._lifespan_result = user_lifespan_result
498496
self._lifespan_result_set = True
499497

500498
async with AsyncExitStack[bool | None]() as stack:
@@ -503,7 +501,11 @@ async def _lifespan_manager(self) -> AsyncIterator[None]:
503501
cm=server.server._lifespan_manager()
504502
)
505503

506-
yield
504+
self._started.set()
505+
try:
506+
yield
507+
finally:
508+
self._started.clear()
507509

508510
self._lifespan_result_set = False
509511
self._lifespan_result = None

src/fastmcp/server/tasks/handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ async def handle_tool_as_task(
6161
raise McpError(
6262
ErrorData(
6363
code=INTERNAL_ERROR,
64-
message="Background tasks require Docket. Set FASTMCP_ENABLE_DOCKET=true",
64+
message="Background tasks require a running FastMCP server context",
6565
)
6666
)
6767

@@ -169,7 +169,7 @@ async def handle_prompt_as_task(
169169
raise McpError(
170170
ErrorData(
171171
code=INTERNAL_ERROR,
172-
message="Background tasks require Docket. Set FASTMCP_ENABLE_DOCKET=true",
172+
message="Background tasks require a running FastMCP server context",
173173
)
174174
)
175175

0 commit comments

Comments
 (0)