Background Tasks
以异步的方式运行长耗时的操作,并跟踪进度。
FastMCP 实现了 MCP 后台任务协议(SEP-1686),只需修改一个装饰器,就能为你的服务器提供一个可用于生产环境的分布式任务调度器。
什么是 Docket?
FastMCP 的任务系统由Docket提供支持,它最初由Prefect开发,用于为Prefect Cloud的托管任务调度和执行服务提供动力,该服务每天处理数百万个并发任务。如今,Docket 已开源给社区。
什么是 MCP 后台任务?
在 MCP 中,所有组件交互默认都是阻塞式的。当客户端调用工具、读取资源或获取提示时,它会发送请求并等待响应。对于需要数秒或数分钟的操作,这会带来糟糕的用户体验。
MCP 后台任务协议通过允许客户端执行以下操作解决了这个问题:
- 启动一个操作并立即收到一个任务 ID
- 跟踪操作运行时的进度
- 在结果就绪时检索结果
FastMCP 会为你处理所有这些事情。在你的装饰器中添加task=True,你的函数就会获得完整的后台执行能力,包括进度报告、分布式处理和水平扩展。
MCP 后台任务与 Python 并发
简单说就是:在你的 FastMCP 服务器里,想怎么处理"并行/异步任务"都可以------要么用 Python 自带的工具(比如 asyncio 异步、线程、多进程),要么用外面的任务队列工具;
反正 FastMCP 本质就是 Python 写的,你习惯怎么写 Python 代码,就怎么用它跑任务,没有限制。 核心是强调灵活性:不强制你用 FastMCP 自己的任务功能,而是支持你用熟悉的 Python 原生方案或其他工具,怎么方便怎么来。
启用后台任务
在任何工具、资源、资源模板或提示装饰器中添加task=True。这会将该组件标记为能够进行后台执行。
python
import asyncio
from fastmcp import FastMCP
mcp = FastMCP("MyServer")
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""A long-running operation."""
for i in range(duration):
await asyncio.sleep(1)
return f"Completed in {duration} seconds"
- 客户端发请求时:只要客户端说 "我要让这个任务在后台跑",服务器会立刻返回一个 "任务 ID"(相当于给这个任务发了个唯一编号),不用等任务做完。
- 任务实际执行时:真正的工作是交给 "后台工作节点"(专门处理耗时任务的 "小助手")来跑的,不占用客户端和服务器的实时交互资源。
- 客户端查结果时:有两种方式 ------ 要么主动用之前拿到的 "任务 ID" 去问服务器 "任务进度咋样了"(也就是 "轮询状态"),要么等着服务器主动把结果推过来 / 等结果生成后再取("等待结果")。
后台任务需要异步函数。尝试将task=True与同步函数一起使用会在注册时引发ValueError。
**客户端代码**
python
import asyncio
from fastmcp import FastMCPClient
async def main():
# 1. 初始化客户端,连接到 FastMCP 服务器
client = FastMCPClient(
server_address="http://localhost:8000", # 替换成你的服务器地址
server_name="MyServer" # 要和服务端的 FastMCP("MyServer") 一致
)
try:
# 2. 调用带 task=True 的工具,触发后台任务,立即返回任务 ID
# 注意:调用时需要指定 task 相关参数(匹配服务端的 task 模式)
task_response = await client.call_tool(
tool_name="slow_computation",
arguments={"duration": 5}, # 传入耗时参数,比如 5 秒
task={"enabled": True} # 开启任务模式,获取任务 ID
)
# 3. 提取任务 ID
task_id = task_response.task_id
print(f"后台任务已启动,任务 ID: {task_id}")
# 4. 轮询任务状态,直到完成
while True:
# 查询任务状态
task_status = await client.get_task_status(task_id)
print(f"当前任务状态: {task_status.status}")
if task_status.status == "completed":
# 任务完成,获取结果
print(f"任务执行成功!结果: {task_status.result}")
break
elif task_status.status == "failed":
# 任务失败,打印错误信息
print(f"任务执行失败!错误: {task_status.error}")
break
# 每隔 1 秒查询一次
await asyncio.sleep(1)
finally:
# 关闭客户端连接
await client.close()
# 运行客户端主函数
if __name__ == "__main__":
asyncio.run(main())
Execution Modes:
为了对任务执行行为进行细粒度控制,请使用TaskConfig而非布尔值简写形式。MCP 任务协议定义了三种执行模式:
|-----------|---------------------------|-----------------------------|
| Mode | Client calls without task | Client calls with task |
| forbidden | Executes synchronously | Error: task not supported |
| optional | Executes synchronously | Executes as background task |
| required | Error: task required | Executes as background task |
这张表是用来定义 客户端调用接口时,"是否携带任务(Task)" 与 "接口执行方式" 的对应规则,核心是通过 3 种 "Mode(模式)" 明确不同场景下的行为,避免混乱:
-
"forbidden"(禁止任务模式)
- 客户端不带任务调用:正常同步执行(按传统方式,等结果返回);
- 客户端带任务调用:直接报错(因为这种模式明确不支持 "任务",带了就违规)。
-
"optional"(任务可选模式)
- 客户端不带任务调用:同步执行(兼容传统用法);
- 客户端带任务调用:异步后台执行(有任务就用后台模式,灵活适配两种需求)。
-
"required"(任务必选模式)
- 客户端不带任务调用:直接报错(必须带任务才能用,强制走任务流程);
- 客户端带任务调用:异步后台执行(符合该模式的核心要求)。
python
from fastmcp import FastMCP
from fastmcp.server.tasks import TaskConfig
mcp = FastMCP("MyServer")
# Supports both sync and background execution (default when task=True)
@mcp.tool(task=TaskConfig(mode="optional"))
async def flexible_task() -> str:
return "Works either way"
# Requires background execution - errors if client doesn't request task
@mcp.tool(task=TaskConfig(mode="required"))
async def must_be_background() -> str:
return "Only runs as a background task"
# No task support (default when task=False or omitted)
@mcp.tool(task=TaskConfig(mode="forbidden"))
async def sync_only() -> str:
return "Never runs as background task"
布尔值快捷方式对应以下模式:
task=True → TaskConfig(mode="optional")
task=False → TaskConfig(mode="forbidden")
轮询间隔
当客户端轮询任务状态时,服务器会告知它们应多久回来检查一次。默认情况下,FastMCP 建议 5 秒的间隔,但你可以按组件自定义此间隔:
python
from datetime import timedelta
from fastmcp import FastMCP
from fastmcp.server.tasks import TaskConfig
mcp = FastMCP("MyServer")
# Poll every 2 seconds for a fast-completing task
@mcp.tool(task=TaskConfig(mode="optional", poll_interval=timedelta(seconds=2)))
async def quick_task() -> str:
return "Done quickly"
# Poll every 30 seconds for a long-running task
@mcp.tool(task=TaskConfig(mode="optional", poll_interval=timedelta(seconds=30)))
async def slow_task() -> str:
return "Eventually done"
较短的间隔能为客户端提供更快的反馈,但会增加服务器负载。较长的间隔会减少负载,但会延迟状态更新。
服务器范围的默认设置
要默认启用所有组件的后台任务支持,请在构造函数中传入tasks=True。各个装饰器仍然可以通过task=False来覆盖此设置。
python
mcp = FastMCP("MyServer", tasks=True)
如果你的服务器定义了任何同步工具、资源或提示,你需要在它们的装饰器上显式设置task=False以避免错误。
优雅降级
当客户端请求后台执行但组件具有mode="forbidden"时,FastMCP 会同步执行并内联返回结果。这遵循 SEP-1686 规范中的优雅降级原则 ------ 客户端始终可以请求后台执行,而无需担心服务器的能力。
相反,当组件具有mode="required"但客户端未请求后台执行时,FastMCP 会返回一个错误,表明任务执行是必需的。
Configuration
-
- 环境变量
FASTMCP_DOCKET_URL
-
- 默认值
memory://
-
- 描述
Backend URL (memory:// or redis://host:port/db)
Backends
FastMCP 支持两种用于任务执行的后端,每种后端都有不同的权衡。
In-Memory Backend (Default)
内存后端(memory://)无需任何配置,开箱即可使用。
优势:
- 无外部依赖
- 简单的单进程部署
劣势:
- 临时性:如果服务器重启,所有未完成的任务都会丢失
- 延迟较高:约 250 毫秒的任务获取时间,而 Redis 则为个位数毫秒
- 不支持水平扩展:仅支持单进程 ------ 无法添加额外的工作进程
Redis Backend
对于生产环境部署,请通过设置FASTMCP_DOCKET_URL=redis://localhost:6379,将 Redis(或 Valkey)用作后端。
优势:
- 持久性:任务在服务器重启后仍然存在
- 快速性:任务获取延迟为个位数毫秒
- 可扩展性:可添加工作节点以在进程或机器间分配负载
Workers
每个具有任务启用组件的 FastMCP 服务器都会自动启动一个嵌入式工作器。你无需启动单独的工作器进程来执行任务。
要进行水平扩展,请使用命令行界面添加更多工作器:
python
fastmcp tasks worker server.py
每个额外的工作进程都会从同一个队列中获取任务,从而在多个进程间分配负载。通过环境配置工作进程的并发数:
python
export FASTMCP_DOCKET_CONCURRENCY=20
fastmcp tasks worker server.py
额外的工作进程仅适用于 Redis/Valkey 后端。内存后端仅支持单进程。
Progress Reporting
Progress依赖项允许你向客户端报告进度。将其作为带有默认值的参数注入,FastMCP 会提供活动的进度报告器。
python
from fastmcp import FastMCP
from fastmcp.dependencies import Progress
mcp = FastMCP("MyServer")
@mcp.tool(task=True)
async def process_files(files: list[str], progress: Progress = Progress()) -> str:
await progress.set_total(len(files))
for file in files:
await progress.set_message(f"Processing {file}")
# ... do work ...
await progress.increment()
return f"Processed {len(files)} files"
进度 API:
- await progress.set_total(n) --- 设置总步骤数
- await progress.increment(amount=1) --- 增加进度
- await progress.set_message(text) --- 更新状态消息
进度在即时执行模式和后台执行模式下均能运行 ------ 无论客户端如何调用你的函数,你都可以使用相同的代码。
Docket Dependencies
FastMCP 在您启用任务的函数中暴露了 Docket 的完整依赖注入系统。除了Progress之外,您还可以访问 Docket 实例、工作器信息,并使用诸如重试和超时等高级功能。
python
from docket import Docket, Worker
from fastmcp import FastMCP
from fastmcp.dependencies import Progress, CurrentDocket, CurrentWorker
mcp = FastMCP("MyServer")
@mcp.tool(task=True)
async def my_task(
progress: Progress = Progress(),
docket: Docket = CurrentDocket(),
worker: Worker = CurrentWorker(),
) -> str:
# Schedule additional background work
await docket.add(another_task, arg1, arg2)
# Access worker metadata
worker_name = worker.name
return "Done"
借助CurrentDocket(),您可以安排额外的后台任务、将工作串联起来以及协调复杂的工作流。有关完整的 API(包括重试策略、超时设置和自定义依赖项),请参阅Docket 文档。