FastMCP v4 后台任务重构:基于 SEP-2663 的 fastmcp-tasks 扩展设计全解
【免费下载链接】fastmcp🚀 The fast, Pythonic way to build MCP servers and clients.项目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp
本篇技术指南基于 FastMCP 仓库中已定型并落地(Shipped,#4602、#4603)的 v4 设计文档 dev-docs/v4-notes/background-tasks.md 编写,围绕"后台任务从 SEP-1686 迁移到 SEP-2663 扩展"这条主线展开。文章会说明任务扩展的协议线形状、fastmcp-tasks可选包的打包与激活模型、FastMCP 原生服务端扩展 API(mcp.add_extension)、客户端三层调用体验,以及设计中的范围边界、风险与已定案决策;同时结合仓库内fastmcp_tasks包的源码实现与fastmcp核心中的任务声明原语,给出可复现的配置与代码依据。读完你可以掌握:如何用task=True+TasksExtension在 FastMCP v4 上运行 MCP 后台任务,理解其轮询式协议与 Docket 执行引擎的边界,以及这套扩展 API 为何能同时成为未来 MCP Apps 集成的统一入口。
TL;DR:任务机制保留,协议底座换为 SEP-2663
FastMCP 的后台任务能力不会消失。MCP 规范把任务协议从核心中移出,并合并为一个Final 状态的扩展——io.modelcontextprotocol/tasks(SEP-2663),它保留了 FastMCP 已经实现的轮询模型。截至当前仓库状态,没有任何语言的 SDK 为它提供运行时实现,而 FastMCP 拥有唯一接近生产形态的执行引擎(Docket/Redis),且其协议形态与 SEP-2663 高度一致。
因此 v4 的决策是:基于 SEP-2663 重建任务支持,做成仓库内的可选包fastmcp-tasks,用task=True门控激活,与 MCP Apps 用app=True门控的模式完全对齐。迁移过程中移除 SEP-1686 的线层(wire layer),保留并重新安置执行引擎;同时引入FastMCP 原生的服务端扩展 API,让任务(以及后续的 Apps)通过一个文档化的统一机制接入,而不是继续对核心做特制改动。
净效果是:已经用@mcp.tool(task=True)的服务端代码无需任何改动即可平滑过渡,且 FastMCP 很可能成为 tasks 扩展在生态中的第一个运行时实现。
背景:任务的现状(SEP-1686 时代)
FastMCP 3 曾基于SEP-1686(短暂存在于 MCP 核心规范中的任务协议)实现后台任务。该实现横跨 server、client、CLI 和一个 SDK shim,约 4,000 行代码,可拆成两个性质完全不同的部分(见 fastmcp_tasks 包结构):
- 线层(wire layer):能力通告、
tasks/get|result|list|cancel四个处理器、在增强版tools/call上返回的CreateTaskResult,以及一个基于 Redis 的推送中继,让 worker 能触达客户端以投递通知和 elicitation(追问)请求。 - 执行引擎(execution engine):基于 Docket(队列、worker、结果存储、TTL,支持
memory://或redis://后端),外加 FastMCP 自己构建的持久化层:按 auth 作用域复合键隔离任务访问、跨 worker 进程的请求上下文快照/恢复、与同步路径一致的参数强转,以及fastmcp tasks workerCLI。
SDK v2 迁移时把 SEP-1686 从核心规范中移除,v4 设计笔记一度记录"删除任务机制,需要任务的用户停留在 FastMCP 3"。当时的判断在信息不完整的前提下是正确的——假设后继协议要么不存在、要么不可实现。而这两个假设后来都被证伪。
上游变化:SEP-2663 做了什么
任务协议是被重构而非删除。SEP-2663("Tasks Extension")已是 Final 状态,于 2026-05-15 在上游合并,取代 SEP-1686。它定义io.modelcontextprotocol/tasks扩展,是一个基于 SEP-2133 扩展机制的能力协商特性,保留了 SEP-1686 的轮询核心并加以收紧。
线形状(wire shape)
- 客户端通告任务能力(在每次请求的
_meta中)。这是"同意"——"我能处理任务结果"——而不是发起任务的请求。 - 客户端发出普通
tools/call。由服务端决定是否以任务方式运行。 - 若被任务化,服务端返回
CreateTaskResult(一个携带resultType: "task"的 claimed 结果形状),其中的taskId由服务端生成。 - 客户端轮询
tasks/get直到状态进入终态;结果内联在该响应中返回。 - 任务执行过程中的输入(elicit/sample/roots)采用轮询式:状态翻转为
input_required,未决请求出现在inputRequests映射中,客户端通过tasks/update应答。 tasks/cancel是协作式的。推送是可选能力(notifications/tasks走subscriptions/listen),服务端可以不发。
与 SEP-1686 的差异
设计文档用一张对照表总结了差异,最值得注意的是其中大部分是删除——因为规范在向 FastMCP 已经构建的方向靠拢:
| 维度 | SEP-1686(旧) | SEP-2663(新) | FastMCP 现状 |
|---|---|---|---|
| 任务 id 生成 | 客户端生成 | 服务端生成 | 已是服务端生成 |
tasks/list | 存在 | 移除(枚举风险) | 已是返回[]的桩 |
| 结果获取 | 独立的tasks/result | 内联进tasks/get | 合并两个处理器即可 |
tasks/delete | 存在 | 移除(依赖 TTL) | TTL 是 Docket 原生能力 |
| 创建竞态 | notifications/tasks/created | 必须持久化创建 | 差一个读己之写(read-your-writes)检查 |
| 任务内输入 | 推送中继 +_meta标记 | 轮询:input_required+tasks/update | 替换掉最棘手的模块 |
| 状态集合 | 7 个(含submitted、unknown) | 5 个 | 收缩映射表 |
| 可增强请求 | 任意 | 仅tools/call | 仅工具面(见范围) |
| LB 路由 | 未规定 | Mcp-Name: <taskId>头 | 共享 Redis 下无关紧要 |
关键点:目前没有任何运行时实现。ext-tasks仓库只有 schema 和文字规范;TypeScript 与 Python SDK 只携带线类型和一致性测试夹具,没有客户端/服务端实现。这个领域是开放的。
决策:构建它
两个事实推翻了之前"删除并等待"的判断:
- 规范正是 FastMCP 已实现的形态,只是去掉了一个可以丢弃的推送中继。重建主要由删除和一个薄薄的新线适配器组成,而非从零开始。
- FastMCP 位置独特。SEP-2663 的隐含前提是:持久化的服务端存储、服务端生成的高熵 id、容忍最终一致性的创建流程、多节点路由——这恰是 Docket/Redis 提供的。没有其他框架内置了这套能力。
在迁移期间继续维护 SEP-1686 机制是死重(它是_sdk_patches.pyshim、TaskNotificationHandler以及一批协议时代 xfail 的唯一原因)。基于 SEP-2663 重建既能清除这笔技术债,又能产出一个"零代码改动迁移"的旗舰 v4 能力。
架构:引擎与线的拆分
引擎/线分层
现有代码已经沿着这条线清晰分离,重建只是把边界变成包边界:
- 移除:SEP-1686 线层——能力通告、四个 CRUD 处理器,以及最大的收获:整个 Redis 推送中继(
server/tasks/elicitation.py、notifications.py)。它存在只是因为 SEP-1686 没有基于轮询的任务内输入通道;SEP-2663 的input_required/tasks/update取代了它。请求/响应存储保留,推送信封(push envelope)不再需要。 - 保留并重新安置:Docket 执行引擎、auth 作用域键编码(这是
tasks/get/update/cancel的授权层——比规范中"taskIds 可以当作 bearer token"更强)、上下文快照/恢复、参数强转、worker CLI。这些全部与线协议无关。 - 新增:一个薄的 SEP-2663 线适配器——能力、
tasks/get/update/cancel方法,以及一个决定是否任务化并执行的tools/call拦截器。
在源码中可以看到这条边界的落地:核心的 fastmcp_slim/fastmcp/utilities/tasks.py 只保留纯声明——TaskMode、TASKS_EXTENSION_ID(反向 DNS 标识io.modelcontextprotocol/tasks)、TaskConfig与TaskMeta——而 Docket 相关的依赖注入与"安装提示"全部迁入 fastmcp_tasks/fastmcp_tasks/dependencies.py(模块 docstring 明确写着"在 SEP-1686 → SEP-2663 迁移中从 fastmcp.server.dependencies 移出")。
打包与激活模型
fastmcp-tasks是仓库内uvworkspace 成员,模板沿用fastmcp_remote(独立pyproject.toml、与主版本锁步、通过fastmcp元包再导出)。与 MCP Apps 的开发者体验完全平行:
| 关注点 | MCP Apps | 后台任务 |
|---|---|---|
| 作者标记(核心) | @mcp.tool(app=True) | @mcp.tool(task=True) |
| 可选包 | prefab-ui | fastmcp-tasks |
| 额外依赖 extra | fastmcp[apps] | fastmcp[tasks] |
| 缺包行为 | 响亮的安装提示 | 服务端构建时响亮的安装提示 |
核心只保留声明:task=True/TaskConfig是组件上的元数据,不导入任何引擎。引擎和线适配器都住在fastmcp-tasks包里。现有[tasks]extra 从 SEP-1686 机制改指向fastmcp-tasks,因此pip install fastmcp[tasks]与task=True都会在现代化线协议之下继续工作。
激活保持隐式但响亮(沿用现有require_docket()模式,绝不静默降级):任何task=True都会在构建期触发对fastmcp-tasks的惰性导入,缺装立即抛错。作者标记为任务、却在线内静默执行的工具是一个正确性 bug,而不是优雅回退。require_docket的实现可见 fastmcp_tasks/fastmcp_tasks/dependencies.py#L45-L72:缺装时提示pip install 'fastmcp[tasks]',装了旧版pydocket时提示升级版本。
扩展 API:mcp.add_extension
MCP 扩展(SEP-2133)是 SDK v2 中真正的新抽象——v1 时代并不存在。MCP Apps 当时手工拼接集成并非错误选择,而是先于该工具诞生。如今 FastMCP 的服务端完全绕过 SDK 的Extension类(把ui能力手工拼接进低级服务端并直接遍历工具元数据),而客户端原生转发ClientExtension。每新增一个协议扩展都意味着对核心的特制手术。
Tasks 成为推动修复此问题的契机,设计引入单一注册点:
from fastmcp import FastMCP from fastmcp_tasks import TasksExtension mcp = FastMCP("Server") mcp.add_extension(TasksExtension(url="redis://...")) # 启用任务所必需 @mcp.tool(task=True) # 意图:这个工具可以以任务方式运行 async def crunch(dataset: str) -> str: ...add_extension对task=True生效是必需的——不会因为存在task=True标记就被自动检测。这是有意为之:
- 扩展需要配置(后端 URL、worker 并发、TTL 默认值),
add_extension(TasksExtension(...))是其天然归宿;自动检测只会把这些配置打散到 settings/env 中,并隐藏启用时刻。 - 要求显式注册能让能力通告保持诚实——服务端仅在扩展已注册时才通告
tasks能力。 - 它移除了最危险的 footgun:因为没人配置 Redis,工具在生产环境静默运行在内存后端上。
两个关注点保持清晰分离:task=True是组件级意图("这个工具可以是任务");add_extension是服务端级启用与配置("这个服务端运行任务,方式如下")。用了task=True却没有注册扩展,会在构建期报出响亮的错误。
在源码中,ServerExtension基类(fastmcp_slim/fastmcp/server/extensions.py)定义了四类可贡献物:
- 协商能力:
settings()拼接到ServerCapabilities.extensions[identifier]; - 新增请求方法:
methods()返回MethodBinding,注册时通过add_request_handler挂到低级服务端; tools/call拦截器:intercept_tool_call()是工具体运行前的最后一道门——它组合在 FastMCP 中间件链之后、组件执行之前,可以观察、短路或放行调用;- 生命周期:
lifespan()随服务端生命周期进入/退出——这是 SDK 的Extension所缺失的钩子,用于启动后端/worker。
基类遵循 SDK 的 httpx 式形状:每个贡献方法都有默认实现,子类只需覆写所需部分。MethodBinding还有防呆约束:不能绑定规范已有的请求方法(tools/call、completion/complete等),否则会静默遮蔽服务端自己的处理器,见 extensions.py#L97-L108。
扩展 vs 中间件:判别准则
为避免过度应用该抽象,设计文档给出明确判别器:扩展是客户端必须理解的协商性协议变更;中间件是客户端永远看不见的单边服务端行为。PII 检测、鉴权、限流走中间件;Tasks、Apps 走扩展。一个试金石:删掉能力通告——如果客户端的任何行为没有改变,那它就是中间件。
客户端体验
SEP-2663 移除了客户端"把这次调用做成任务"的标记——由服务端决定。这恰好映射到 FastMCP 现有的两层客户端表面:友好的call_tool与低层的call_tool_mcp,因此几乎没有新增 API:
call_tool(name, args)(友好层):通告能力;若服务端把调用任务化,则透明驱动轮询循环并返回完成后的结果。调用方完全察觉不到是否被任务化。任务内input_required会路由到客户端已有的 elicitation handler,经tasks/update应答——因此后台 elicitation 与前台 elicitation 看起来完全一样,零新增客户端 API。call_tool_mcp(...)(低层):把原始CreateTaskResultclaimed 形状交还给自行管理任务的调用方。- 友好接口上的"快速返回"标志:立即得到
Task句柄(.status()、.wait()、.cancel(),可 await),不阻塞——这是进度与取消的逃生舱。
客户端侧的实现在 fastmcp_tasks/fastmcp_tasks/client.py:TasksClientExtension自动注册到每个 FastMCPClient上(调用方无需任何 opt-in),其 claim resolver 在底层把tasks/get轮询到完成,再以CallToolResult返回真实结果;ToolTask则是显式句柄。模块 docstring 还注明:任务只存在于现代协议(modern protocol)上——在 legacy 连接上 SDK 会剥离能力通告,服务端永不任务化,该扩展处于惰性(inert)状态。
服务端侧,TaskConfig三种模式直接翻译(对应 fastmcp_slim/fastmcp/utilities/tasks.py#L42-L62 中的TaskMode):
required→ 总是任务化(对未声明的客户端返回-32021,即MISSING_REQUIRED_CLIENT_CAPABILITY);optional→ 客户端声明了才任务化;forbidden→ 永不任务化。
TasksExtension.intercept_tool_call的判定逻辑见 fastmcp_tasks/fastmcp_tasks/extension.py#L213-L271:它还会解析请求_meta中的版本规范,避免对指向旧版本的tools/call错误地任务化最高版本的工具;同时只在现代 era(2026-07-28 起)才认可客户端 opt-in。
实操:安装、配置与运行
安装
作为 FastMCP 的tasksextra 安装(见 fastmcp_tasks/README.md):
uv pip install "fastmcp[tasks]"最小服务端
from fastmcp import FastMCP from fastmcp_tasks import TasksExtension mcp = FastMCP("Analytics") mcp.add_extension(TasksExtension(url="redis://localhost:6379/0")) @mcp.tool(task=True) async def analyze(dataset: str) -> str: # 长时间运行的工作。客户端立即拿到任务句柄并轮询结果; # 这段代码在后台 worker 中执行。 ...task=True是意图声明——该工具可能以任务方式运行——而按规范,服务端在每次调用时决定是否真的任务化。需要更精细控制时使用TaskConfig:
from fastmcp.utilities.tasks import TaskConfig @mcp.tool(task=TaskConfig(mode="required")) async def must_run_async(n: int) -> int: # 总是以任务方式运行;未 opt-in 的客户端会被明确告知。 ...注意:注册TasksExtension是服务task=True工具的前提——工具声明意图,扩展提供引擎。服务端注册了task=True工具却没有任务扩展,会在启动时响亮失败,而不是静默在线内运行。
配置项与默认值
后端在扩展上配置。每个选项都有对应的FASTMCP_DOCKET_*环境变量,因此纯环境变量配置的部署可以无参数构造TasksExtension()(fastmcp_tasks/fastmcp_tasks/settings.py 中DocketSettings完整定义了这些字段):
| 选项 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
url | FASTMCP_DOCKET_URL | memory:// | 后端 URL。memory://用于单进程;redis://host:port/db用于分布式 worker。 |
name | FASTMCP_DOCKET_NAME | fastmcp | 队列名。同名同 URL 的服务端与 worker 共享同一任务队列。 |
worker_name | FASTMCP_DOCKET_WORKER_NAME | None(Docket 自动生成) | worker 名称。 |
concurrency | FASTMCP_DOCKET_CONCURRENCY | 10 | 每个 worker 的最大并发任务数。 |
redelivery_timeout | FASTMCP_DOCKET_REDELIVERY_TIMEOUT | 300s | 任务重投递超时:worker 未在期限内完成任务,任务会被重投递给其他 worker。 |
reconnection_delay | FASTMCP_DOCKET_RECONNECTION_DELAY | 5s | worker 失去与后端连接后的重连间隔。 |
minimum_check_interval | FASTMCP_DOCKET_MINIMUM_CHECK_INTERVAL | 50ms | worker 轮询新任务的频率。调低降低任务拾取延迟但增加 CPU;高吞吐生产环境建议调高。 |
此外TasksExtension构造器还接受url、name、worker_name、concurrency、redelivery_timeout、reconnection_delay、minimum_check_interval共 7 个关键字参数(extension.py#L86-L108),任何未传参数都会回落到环境变量默认值。
两组额外的设置类值得了解:
TasksSettings(前缀FASTMCP_TASKS_):encryption_key,用于对任务上下文快照静态加密。快照携带提交者的访问令牌与 HTTP 头,写入 Docket 后端并存活到任务 TTL;共享同一任务队列的服务端与 worker 必须配置相同密钥,worker 无法解密的快照会让任务失败而不是以匿名身份运行。未设置时快照以明文 JSON 存储。密钥经 PBKDF2 派生 Fernet key,任意非空字符串可用,但建议至少 32 个随机字符。TasksClientSettings(前缀FASTMCP_TASKS_CLIENT_):poll_interval(默认0.5秒),是客户端等待后台任务时回退轮询的上限;仅当服务端未通告自己的pollIntervalMs时生效——此时客户端从约20ms起步并倍增到该上限,快速任务即时解决、长任务不会锤爆服务端。服务端通告了pollIntervalMs时精确遵从该值并忽略此设置。
运行独立 worker(分布式)
基于 Redis 的分布式部署中,让专用 worker 进程与服务端并存:
python -m fastmcp_tasks.worker_cli worker server.py共享同一后端 URL 与队列名的 worker 与服务端共享任务队列,因此执行能力可以独立于请求服务的前端节点横向扩展。worker CLI 实现见 fastmcp_tasks/fastmcp_tasks/worker_cli.py。
实施顺序(Sequencing)
- 先设计与单测扩展 API:针对 tasks 的全表面(能力、方法、拦截、客户端 claims/通知)进行设计与测试——作为独立可测层,在任务逻辑落地之前先用一个琐碎的内测扩展验证隔离性。
- 构建
fastmcp-tasks:从被移除的 SEP-1686 层中抽取引擎,编写 SEP-2663 适配器,移植客户端半边。 - 把 MCP Apps 迁移到扩展 API 上:快速跟进,不在关键路径上,用 Apps 现有的绿色测试作为回归网。
Tasks 先行,因为只有它能触达扩展 API 的全表面;先用 Apps 的子集设计会把自己逼进死角。Apps 成为第二个消费者,用于确认设计具有通用性。
v1 范围(非目标)
- 仅轮询。可选的
notifications/tasks推送与subscriptions/listen集成推迟到后续fastmcp-tasks版本,让第二条 Redis 通知队列就此消亡而非移植。 - 仅
tools/call,不领先规范。SEP-2663 只增强tools/call。FastMCP 3 曾在 SEP-1686 下对 prompts 和 resources 提供task=True,先于 SDK——那是个错误:它产生了线协议无法表达的能力、一批永久 xfail 和 sdk-feedback #3 的差距。本次重建不会重蹈覆辙:task=是仅限工具的表面,通用 prompt/resource 任务脊梁被丢弃而非携带。若规范将来扩展增强类型,表面随之增长。 - 以 experimental 状态发布。
ext-tasksschema 标记为 experimental 且无发布版本;fastmcp-tasks初期同样标注 experimental,schema 演进时按自身节奏发版。
风险与缓解
| 风险 | 缓解 |
|---|---|
| 规范变动(扩展仍为 experimental) | 在无线相关的引擎上放一个薄线适配器;以 experimental 发布;SEP 本身是 Final,即使字段名变动,轮询模型也是稳定的。 |
Era 门控——SDK 在 2026 之前的协商版本剥离capabilities.extensions(sdk-feedback #2) | 能力通告实质上要求 2026-07-28 era;FastMCP 3 覆盖 legacy 任务。#2 现在门控一个旗舰能力 → 上报上游。 |
| 同时开发新抽象 + 全新功能 | 先隔离构建并单测扩展 API(步骤 1),再在其上落地任务逻辑。 |
命名混淆——[tasks]extra 在同名下改指向 | 有意的 changelog 说明;用户代码与 extra 名均不变,只有线协议现代化。 |
设计决策(已定案)
以下是曾经的开放分叉,维护者已拍板定案,记录于此以保证实施方向无歧义:
- 线适配器位置——在
fastmcp-tasks包内。引擎与 SEP-2663 线适配器都住在包中,核心只携带task=True声明。这把 experimental schema 的变动与核心隔离,代价是与 Apps 先例分叉(Apps 的ui线胶水今天仍在核心中——Apps 迁移到扩展 API 时会向该模型收敛)。 - 扩展 API 形态——FastMCP 原生
mcp.add_extension(),启用 tasks 为必需项。选它而非对 SDK 的MCPServer(extensions=...)做薄透传,是因为 FastMCP 原生 API 能把 SDKExtension不提供的Context、组件注册表、auth 作用域交给扩展。add_extension对task=True是必需的(不做自动检测)——它是后端配置的唯一归宿,也是能力通告的诚实来源。 - 客户端默认——友好接口透明完成。
call_tool驱动轮询循环并返回完成结果;call_tool_mcp暴露原始CreateTaskResult;"快速返回"标志给出Task句柄。 - experimental 标注——要。
fastmcp-tasks至少在一个小版本周期内标注 experimental,跟随 experimental 的ext-tasksschema。 - 资源/提示词脊梁——丢弃,仅限工具。本次重建不领先 SDK 的可增强请求类型,纠正 SEP-1686 时代的错误。
延伸阅读
- 设计文档:dev-docs/v4-notes/background-tasks.md
- 任务特性一览(feature program):dev-docs/v4-notes/feature-program.md
fastmcp-tasks包实现:fastmcp_tasks/(README 见 fastmcp_tasks/README.md)- 核心任务声明原语:fastmcp_slim/fastmcp/utilities/tasks.py
- 服务端扩展 API 基类:fastmcp_slim/fastmcp/server/extensions.py
- 任务扩展适配器:fastmcp_tasks/fastmcp_tasks/extension.py
- 后端/worker 配置:fastmcp_tasks/fastmcp_tasks/settings.py
- 客户端任务驱动:fastmcp_tasks/fastmcp_tasks/client.py
- 相关测试:tests/tasks/、tests/server/test_dependencies.py
【免费下载链接】fastmcp🚀 The fast, Pythonic way to build MCP servers and clients.项目地址: https://gitcode.com/GitHub_Trending/fa/fastmcp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考