Skip to content

Commit 8771f29

Browse files
authored
Rewrite background tasks documentation (#2567)
- Lead with concepts instead of code - Explain MCP background tasks vs general Python concurrency - Document Docket's Prefect origins and battle-tested infrastructure - Add sections on graceful degradation and embedded workers - Fix version badge to 2.14.0 - Link to SEP-1686 spec
1 parent 6147c27 commit 8771f29

File tree

2 files changed

+137
-90
lines changed

2 files changed

+137
-90
lines changed

docs/clients/tasks.mdx

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ tag: "NEW"
88

99
import { VersionBadge } from "/snippets/version-badge.mdx"
1010

11-
<VersionBadge version="2.14" />
11+
<VersionBadge version="2.14.0" />
1212

1313
The [MCP task protocol](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) lets you request operations to run asynchronously. This returns a Task object immediately, letting you track progress, cancel operations, or await results.
1414

1515
See [Server Background Tasks](/servers/tasks) for how to enable this on the server side.
1616

1717
## Requesting Background Execution
1818

19-
Pass `task=True` to run an operation as a background task:
19+
Pass `task=True` to run an operation as a background task. The call returns immediately with a Task object while the work executes on the server.
2020

2121
```python
2222
from fastmcp import Client
@@ -33,71 +33,54 @@ async with Client(server) as client:
3333
result = await task.result()
3434
```
3535

36-
This works with all three operation types:
36+
This works with tools, resources, and prompts:
3737

3838
```python
39-
# Tools
4039
tool_task = await client.call_tool("my_tool", args, task=True)
41-
42-
# Resources
4340
resource_task = await client.read_resource("file://large.txt", task=True)
44-
45-
# Prompts
4641
prompt_task = await client.get_prompt("my_prompt", args, task=True)
4742
```
4843

49-
## Task Objects
44+
## Working with Task Objects
5045

51-
All task types share a common interface:
46+
All task types share a common interface for retrieving results, checking status, and receiving updates.
5247

53-
### Getting Results
48+
To get the result, call `await task.result()` or simply `await task`. This blocks until the task completes and returns the result. You can also check status without blocking using `await task.status()`, which returns the current state (`"working"`, `"completed"`, `"failed"`, or `"cancelled"`) along with any progress message from the server.
5449

5550
```python
5651
task = await client.call_tool("analyze", {"text": "hello"}, task=True)
5752

58-
# Wait for and get the result
59-
result = await task.result()
60-
61-
# Or use await directly (shorthand for .result())
62-
result = await task
63-
```
64-
65-
### Checking Status
66-
67-
```python
53+
# Check current status (non-blocking)
6854
status = await task.status()
55+
print(f"{status.status}: {status.statusMessage}")
6956

70-
print(f"Status: {status.status}") # "working", "completed", "failed", "cancelled"
71-
print(f"Message: {status.statusMessage}") # Progress message from server
57+
# Wait for result (blocking)
58+
result = await task.result()
7259
```
7360

74-
### Waiting for Completion
61+
For more control over waiting, use `task.wait()` with an optional timeout or target state:
7562

7663
```python
77-
# Wait for task to complete (with timeout)
64+
# Wait up to 30 seconds for completion
7865
status = await task.wait(timeout=30.0)
7966

8067
# Wait for a specific state
8168
status = await task.wait(state="completed", timeout=30.0)
8269
```
8370

84-
### Cancelling Tasks
85-
86-
```python
87-
await task.cancel()
88-
```
71+
To cancel a running task, call `await task.cancel()`.
8972

90-
### Status Notifications
73+
### Real-Time Status Updates
9174

92-
Register callbacks to receive real-time status updates:
75+
Register callbacks to receive status updates as the server reports progress. Both sync and async callbacks are supported.
9376

9477
```python
9578
def on_status_change(status):
9679
print(f"Task {status.taskId}: {status.status} - {status.statusMessage}")
9780

9881
task.on_status_change(on_status_change)
9982

100-
# Async callbacks also supported
83+
# Async callbacks work too
10184
async def on_status_async(status):
10285
await log_status(status)
10386

@@ -106,7 +89,7 @@ task.on_status_change(on_status_async)
10689

10790
## Graceful Degradation
10891

109-
You can always pass `task=True` regardless of whether the server supports background tasks. Per the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks), servers that don't support tasks will execute the operation immediately and return the result inline. Your code works either way:
92+
You can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline. The Task API provides a consistent interface either way.
11093

11194
```python
11295
task = await client.call_tool("my_tool", args, task=True)
@@ -120,7 +103,7 @@ else:
120103
result = await task.result()
121104
```
122105

123-
This means you can write task-aware client code without worrying about server capabilities—the Task API provides a consistent interface whether the operation runs in the background or completes immediately.
106+
This means you can write task-aware client code without worrying about server capabilities.
124107

125108
## Complete Example
126109

@@ -143,13 +126,13 @@ async def main():
143126

144127
task.on_status_change(on_update)
145128

146-
# Do other work
147-
print("Doing other work while task runs...")
129+
# Do other work while task runs
130+
print("Doing other work...")
148131
await asyncio.sleep(2)
149132

150133
# Wait for completion and get result
151134
result = await task.result()
152-
print(f"Result: {result.data}")
135+
print(f"Result: {result.content}")
153136

154137
asyncio.run(main())
155138
```

docs/servers/tasks.mdx

Lines changed: 116 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,141 @@ tag: "NEW"
88

99
import { VersionBadge } from "/snippets/version-badge.mdx"
1010

11-
<VersionBadge version="2.14" />
11+
<VersionBadge version="2.14.0" />
1212

13-
Background tasks allow tools, resources, and prompts to execute asynchronously, returning immediately while work continues in the background. Clients can track progress, cancel operations, and retrieve results when ready.
13+
FastMCP implements the MCP background task protocol ([SEP-1686](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)), giving your servers a production-ready distributed task scheduler with a single decorator change.
1414

15-
This implements the [MCP task protocol](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) from the MCP specification, powered by [Docket](https://github.com/chrisguidry/docket) for task queue management.
15+
<Tip>
16+
**What is Docket?** FastMCP's task system is powered by [Docket](https://github.com/chrisguidry/docket), originally built by [Prefect](https://prefect.io) to power [Prefect Cloud](https://www.prefect.io/prefect/cloud)'s managed task scheduling and execution service. Docket is the beating heart of Prefect's enterprise task infrastructure, processing millions of tasks daily across their multi-tenant SaaS platform. It's now open-sourced for the community.
17+
</Tip>
18+
19+
<Note>
20+
Background tasks are disabled by default in v2.14.0. Enable them with `FASTMCP_ENABLE_TASKS=true` or by passing `tasks=True` to the FastMCP constructor. This default will change in a future release.
21+
</Note>
22+
23+
## What Are MCP Background Tasks?
24+
25+
In MCP, all component interactions are blocking by default. When a client calls a tool, reads a resource, or fetches a prompt, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience.
26+
27+
The MCP background task protocol solves this by letting clients:
28+
1. **Start** an operation and receive a task ID immediately
29+
2. **Track** progress as the operation runs
30+
3. **Retrieve** the result when ready
31+
32+
FastMCP handles all of this for you. Add `task=True` to your decorator, and your function gains full background execution with progress reporting, distributed processing, and horizontal scaling.
1633

17-
## Requirements
34+
### MCP Background Tasks vs Python Concurrency
1835

19-
For **single-process** deployments, everything works out of the box using an in-memory backend.
36+
You can always use Python's concurrency primitives (asyncio, threads, multiprocessing) or external task queues in your FastMCP servers. FastMCP is just Python—run code however you like.
2037

21-
For **multi-process** deployments (multiple workers, distributed systems), you'll need Redis or Valkey. See the [Docket documentation](https://chrisguidry.github.io/docket/) for backend configuration details.
38+
MCP background tasks are different: they're **protocol-native**. This means MCP clients that support the task protocol can start operations, receive progress updates, and retrieve results through the standard MCP interface. The coordination happens at the protocol level, not inside your application code.
2239

2340
## Enabling Background Tasks
2441

25-
Add `task=True` to any tool, resource, or prompt decorator:
42+
Add `task=True` to any tool, resource, resource template, or prompt decorator. This marks the component as capable of background execution.
2643

27-
```python
44+
```python {6}
2845
import asyncio
2946
from fastmcp import FastMCP
30-
from fastmcp.dependencies import Progress
3147

3248
mcp = FastMCP("MyServer")
3349

3450
@mcp.tool(task=True)
35-
async def slow_computation(duration: int, progress: Progress = Progress()) -> str:
36-
"""A long-running operation with progress tracking."""
37-
await progress.set_total(duration)
38-
51+
async def slow_computation(duration: int) -> str:
52+
"""A long-running operation."""
3953
for i in range(duration):
4054
await asyncio.sleep(1)
41-
await progress.increment()
42-
await progress.set_message(f"Step {i + 1} of {duration}")
43-
4455
return f"Completed in {duration} seconds"
4556
```
4657

47-
<Note>
48-
Background tasks require async functions. Sync functions will log a warning and execute immediately instead.
49-
</Note>
58+
When a client requests background execution, the call returns immediately with a task ID. The work executes in a background worker, and the client can poll for status or wait for the result.
5059

51-
## Configuration
60+
<Warning>
61+
Background tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time.
62+
</Warning>
5263

53-
Background tasks require explicit opt-in:
64+
### Server-Wide Default
65+
66+
To enable background task support for all components by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`.
67+
68+
```python
69+
mcp = FastMCP("MyServer", tasks=True)
70+
```
71+
72+
<Warning>
73+
If your server defines any synchronous tools, resources, or prompts, you will need to explicitly set `task=False` on their decorators to avoid an error.
74+
</Warning>
75+
76+
### Graceful Degradation
77+
78+
When a client requests background execution (`task=True` in the request) but the component doesn't support it (`task=False` on the decorator), FastMCP executes synchronously and returns the result inline. This follows the SEP-1686 specification for graceful degradation—clients can always request background execution without worrying about server capabilities.
79+
80+
### Configuration
81+
82+
Background tasks require explicit opt-in via environment variable:
5483

5584
| Environment Variable | Default | Description |
5685
|---------------------|---------|-------------|
5786
| `FASTMCP_ENABLE_TASKS` | `false` | Enable the MCP task protocol |
5887
| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |
5988

60-
You can also set a server-wide default in the constructor:
89+
## Backends
6190

62-
```python
63-
mcp = FastMCP("MyServer", tasks=True)
91+
FastMCP supports two backends for task execution, each with different tradeoffs.
92+
93+
### In-Memory Backend (Default)
94+
95+
The in-memory backend (`memory://`) requires zero configuration and works out of the box.
96+
97+
**Advantages:**
98+
- No external dependencies
99+
- Simple single-process deployment
100+
101+
**Disadvantages:**
102+
- **Ephemeral**: If the server restarts, all pending tasks are lost
103+
- **Higher latency**: ~250ms task pickup time vs single-digit milliseconds with Redis
104+
- **No horizontal scaling**: Single process only—you cannot add additional workers
105+
106+
### Redis Backend
107+
108+
For production deployments, use Redis (or Valkey) as your backend by setting `FASTMCP_DOCKET_URL=redis://localhost:6379`.
109+
110+
**Advantages:**
111+
- **Persistent**: Tasks survive server restarts
112+
- **Fast**: Single-digit millisecond task pickup latency
113+
- **Scalable**: Add workers to distribute load across processes or machines
114+
115+
## Workers
116+
117+
Every FastMCP server with task-enabled components automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute.
118+
119+
To scale horizontally, add more workers using the CLI:
120+
121+
```bash
122+
fastmcp tasks worker server.py
64123
```
65124

125+
Each additional worker pulls tasks from the same queue, distributing load across processes. Configure worker concurrency via environment:
126+
127+
```bash
128+
export FASTMCP_DOCKET_CONCURRENCY=20
129+
fastmcp tasks worker server.py
130+
```
131+
132+
<Note>
133+
Additional workers only work with Redis/Valkey backends. The in-memory backend is single-process only.
134+
</Note>
135+
66136
## Progress Reporting
67137

68-
The `Progress` dependency lets you report progress back to clients:
138+
The `Progress` dependency lets you report progress back to clients. Inject it as a parameter with a default value, and FastMCP will provide the active progress reporter.
69139

70140
```python
141+
from fastmcp import FastMCP
71142
from fastmcp.dependencies import Progress
72143

144+
mcp = FastMCP("MyServer")
145+
73146
@mcp.tool(task=True)
74147
async def process_files(files: list[str], progress: Progress = Progress()) -> str:
75148
await progress.set_total(len(files))
@@ -83,45 +156,36 @@ async def process_files(files: list[str], progress: Progress = Progress()) -> st
83156
```
84157

85158
The progress API:
86-
- `await progress.set_total(n)` - Set the total number of steps
87-
- `await progress.increment(amount=1)` - Increment progress
88-
- `await progress.set_message(text)` - Update the status message
159+
- `await progress.set_total(n)` Set the total number of steps
160+
- `await progress.increment(amount=1)` Increment progress
161+
- `await progress.set_message(text)` Update the status message
89162

90-
Progress works in both immediate and background execution modes.
163+
Progress works in both immediate and background execution modes—you can use the same code regardless of how the client invokes your function.
91164

92-
## Additional Dependencies
165+
## Docket Dependencies
93166

94-
FastMCP provides several Docket-style dependencies you can inject into your functions:
167+
FastMCP exposes Docket's full dependency injection system within your task-enabled functions. Beyond `Progress`, you can access the Docket instance, worker information, and use advanced features like retries and timeouts.
95168

96169
```python
170+
from docket import Docket, Worker
171+
from fastmcp import FastMCP
97172
from fastmcp.dependencies import Progress, CurrentDocket, CurrentWorker
98173

174+
mcp = FastMCP("MyServer")
175+
99176
@mcp.tool(task=True)
100177
async def my_task(
101178
progress: Progress = Progress(),
102-
# docket: Docket = CurrentDocket(), # Access the Docket instance
103-
# worker: Worker = CurrentWorker(), # Access worker info
179+
docket: Docket = CurrentDocket(),
180+
worker: Worker = CurrentWorker(),
104181
) -> str:
105-
...
106-
```
182+
# Schedule additional background work
183+
await docket.add(another_task, arg1, arg2)
107184

108-
By injecting `CurrentDocket()`, you gain access to the full Docket API. This lets you schedule additional background tasks from within your tool, chain tasks together, or use any of Docket's advanced features like task priorities and retries. See the [Docket documentation](https://chrisguidry.github.io/docket/) for the complete API.
185+
# Access worker metadata
186+
worker_name = worker.name
109187

110-
## Running Additional Workers
111-
112-
For distributed task processing, start additional workers:
113-
114-
```bash
115-
fastmcp tasks worker server.py
188+
return "Done"
116189
```
117190

118-
Configure worker concurrency via environment:
119-
120-
```bash
121-
export FASTMCP_DOCKET_CONCURRENCY=20
122-
fastmcp tasks worker server.py
123-
```
124-
125-
<Tip>
126-
Workers only work with Redis/Valkey backends. The `memory://` backend is single-process only.
127-
</Tip>
191+
With `CurrentDocket()`, you can schedule additional background tasks, chain work together, and coordinate complex workflows. See the [Docket documentation](https://chrisguidry.github.io/docket/) for the complete API, including retry policies, timeouts, and custom dependencies.

0 commit comments

Comments
 (0)