Skip to content

Commit 66aaf42

Browse files
chrisguidryclaudejlowin
authored
[2.14] SEP-1686 tasks (#2378)
* Implement MCP background tasks (SEP-1686) using Docket Adds support for background task execution via the MCP task protocol, powered by Docket for task queue management. - Tools, resources, and prompts can be marked with `task=True` to run async - Progress dependency for tracking task progress - CurrentDocket and CurrentWorker dependencies for advanced use cases - Client API with `.call_tool(..., task=True)` returns task handles - Task status notifications via subscriptions - CLI worker command for distributed task processing Configuration via environment: - FASTMCP_ENABLE_DOCKET=true - FASTMCP_ENABLE_TASKS=true - FASTMCP_DOCKET_URL=redis://... (or memory:// for single-process) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Fix tasks example import (TaskStatusResponse → GetTaskResult) The example was using a non-existent TaskStatusResponse type. Updated to use mcp.types.GetTaskResult which is what the on_status_change callback actually receives. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Fix env var name in Docket error messages The error messages referenced FASTMCP_EXPERIMENTAL_ENABLE_DOCKET but the actual setting is FASTMCP_ENABLE_DOCKET. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Remove deprecated code re-added from pre-#2329 branch - Remove ExtendedEnvSettingsSource (FASTMCP_SERVER_ prefix support) - Remove dependencies parameter from FastMCP.__init__ * Replace fakeredis git pin with PyPI release * Remove redundant fakeredis dev dep (pulled via pydocket) --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Jeremiah Lowin <[email protected]>
1 parent b109cf0 commit 66aaf42

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+9959
-459
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ repos:
2626
hooks:
2727
- id: ty
2828
name: ty check
29-
entry: uv run ty check
29+
entry: uv run --isolated ty check
3030
language: system
3131
types: [python]
3232
files: ^src/|^tests/

docs/clients/tasks.mdx

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
---
2+
title: Background Tasks
3+
sidebarTitle: Background Tasks
4+
description: Execute operations asynchronously and track their progress
5+
icon: clock
6+
tag: "NEW"
7+
---
8+
9+
import { VersionBadge } from "/snippets/version-badge.mdx"
10+
11+
<VersionBadge version="2.14" />
12+
13+
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.
14+
15+
See [Server Background Tasks](/servers/tasks) for how to enable this on the server side.
16+
17+
## Requesting Background Execution
18+
19+
Pass `task=True` to run an operation as a background task:
20+
21+
```python
22+
from fastmcp import Client
23+
24+
async with Client(server) as client:
25+
# Start a background task
26+
task = await client.call_tool("slow_computation", {"duration": 10}, task=True)
27+
28+
print(f"Task started: {task.task_id}")
29+
30+
# Do other work while it runs...
31+
32+
# Get the result when ready
33+
result = await task.result()
34+
```
35+
36+
This works with all three operation types:
37+
38+
```python
39+
# Tools
40+
tool_task = await client.call_tool("my_tool", args, task=True)
41+
42+
# Resources
43+
resource_task = await client.read_resource("file://large.txt", task=True)
44+
45+
# Prompts
46+
prompt_task = await client.get_prompt("my_prompt", args, task=True)
47+
```
48+
49+
## Task Objects
50+
51+
All task types share a common interface:
52+
53+
### Getting Results
54+
55+
```python
56+
task = await client.call_tool("analyze", {"text": "hello"}, task=True)
57+
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
68+
status = await task.status()
69+
70+
print(f"Status: {status.status}") # "working", "completed", "failed", "cancelled"
71+
print(f"Message: {status.statusMessage}") # Progress message from server
72+
```
73+
74+
### Waiting for Completion
75+
76+
```python
77+
# Wait for task to complete (with timeout)
78+
status = await task.wait(timeout=30.0)
79+
80+
# Wait for a specific state
81+
status = await task.wait(state="completed", timeout=30.0)
82+
```
83+
84+
### Cancelling Tasks
85+
86+
```python
87+
await task.cancel()
88+
```
89+
90+
### Status Notifications
91+
92+
Register callbacks to receive real-time status updates:
93+
94+
```python
95+
def on_status_change(status):
96+
print(f"Task {status.taskId}: {status.status} - {status.statusMessage}")
97+
98+
task.on_status_change(on_status_change)
99+
100+
# Async callbacks also supported
101+
async def on_status_async(status):
102+
await log_status(status)
103+
104+
task.on_status_change(on_status_async)
105+
```
106+
107+
## Graceful Degradation
108+
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:
110+
111+
```python
112+
task = await client.call_tool("my_tool", args, task=True)
113+
114+
if task.returned_immediately:
115+
print("Server executed immediately (no background support)")
116+
else:
117+
print("Running in background")
118+
119+
# Either way, this works
120+
result = await task.result()
121+
```
122+
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.
124+
125+
## Complete Example
126+
127+
```python
128+
import asyncio
129+
from fastmcp import Client
130+
131+
async def main():
132+
async with Client(server) as client:
133+
# Start background task
134+
task = await client.call_tool(
135+
"slow_computation",
136+
{"duration": 10},
137+
task=True,
138+
)
139+
140+
# Subscribe to updates
141+
def on_update(status):
142+
print(f"Progress: {status.statusMessage}")
143+
144+
task.on_status_change(on_update)
145+
146+
# Do other work
147+
print("Doing other work while task runs...")
148+
await asyncio.sleep(2)
149+
150+
# Wait for completion and get result
151+
result = await task.result()
152+
print(f"Result: {result.data}")
153+
154+
asyncio.run(main())
155+
```

docs/docs.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,8 @@
116116
"servers/progress",
117117
"servers/proxy",
118118
"servers/sampling",
119-
"servers/storage-backends"
119+
"servers/storage-backends",
120+
"servers/tasks"
120121
]
121122
},
122123
{
@@ -171,6 +172,7 @@
171172
"clients/logging",
172173
"clients/progress",
173174
"clients/sampling",
175+
"clients/tasks",
174176
"clients/messages",
175177
"clients/roots"
176178
]

0 commit comments

Comments
 (0)