跳到主内容

MCP 长任务怎么做进度与取消:progressToken、notifications/cancelled 与 Tasks 扩展

100%
MCP 长任务怎么做进度与取消:progressToken、notifications/cancelled 与 Tasks 扩展

MCP 里跑长任务的正确姿势不是”客户端一直等”,而是三件事一起做对:用 progressToken 报进度、用 notifications/cancelled 支持取消、用超时兜底。如果任务真的会超过几十秒,那就别阻塞了——改用 Tasks 扩展,返回一个可轮询的 taskId 。

进度通知:客户端先要,服务端才发

进度不是服务端想发就发的。客户端发起请求时在 _meta 里带上 progressToken,服务端从中读出来,才能往回发 notifications/progress。

// 服务端:从请求上下文取 token,逐单元上报
server.registerTool('process-files', {
  description: 'Process files with progress updates',
  inputSchema: z.object({ files: z.array(z.string()) })
}, async ({ files }, ctx) => {
  const progressToken = ctx.mcpReq._meta?.progressToken;
  for (let i = 0; i < files.length; i++) {
    await process(files[i]);
    if (progressToken) {
      await ctx.mcpReq.notify({
        method: 'notifications/progress',
        params: {
          progressToken,
          progress: i + 1,
          total: files.length,
          message: `Processed ${files[i]}`
        }
      });
    }
  }
  return { content: [{ type: 'text', text: `Processed ${files.length} files` }] };
});

三条硬规则:

  • progress 必须单调递增,total 和 message 可选。
  • 客户端没给 token 就别发。省掉这次调用,同一个请求就不会带 token,服务端的空值判断自然跳过上报。
  • progressToken 只对当前请求有效,不要跨请求复用,也不要自己编一个。

取消:三件事必须同时做对

很多”取消点了没反应”的问题,是因为只做了一半。

  1. 客户端侧:发 notifications/cancelled,带上要取消的 requestId 和可选的 reason。 SDK 层面通常就是 abort 一个 signal 。
  2. 服务端侧:把 ctx.mcpReq.signal(AbortSignal)用到实处——在每个工作单元开始前检查它,被 abort 就立刻收尾退出。只注册回调不检查,等于没实现取消。
  3. 传输层差异:这一条最容易踩。
传输取消信号要点
Streamable HTTP关闭 SSE 响应流服务端必须把客户端断连当作取消,不需要也不应期待收到 notifications/cancelled
stdio显式发 notifications/cancelled没有可关闭的流,必须引用 requestId 发通知
{ "jsonrpc": "2.0", "method": "notifications/cancelled",
  "params": { "requestId": "123", "reason": "User requested cancellation" } }

服务端的义务是”尽力”:应当停止处理、释放资源、并且不再为被取消的请求回响应;如果请求 ID 未知、已经处理完、或者本身无法取消,可以忽略这条通知。客户端同样要忽略迟到到达的响应——网络延迟下,取消通知和响应可能顺序颠倒,双方都得能优雅处理这个竞态。

还有一条容易越界的规则:服务端不得主动发 notifications/cancelled,唯一的例外是拆掉 subscriptions/listen 订阅流时。想通知客户端”我这边出事了”,走日志或错误响应,不要借用取消通道。

超时:进度可以续命,但不能无限续

规范的建议是给所有发出的请求设超时,超时后发一个取消并停止等待。实现上可以在收到进度通知时重置计时器(毕竟说明活儿还在干),但必须同时保留一个最大超时上限——否则一个不停报进度却永不结束的对端,会把连接和连接资源一直占着。

什么时候该上 Tasks 扩展

如果你的任务已经不是”几秒”级别,阻塞式调用就开始碍事了:很多客户端和中间层都有超时,长连接本身也脆弱。 Tasks 扩展的做法是服务端返回一个持久化句柄,客户端拿它轮询。

状态含义
working执行中
input_required需要客户端补充输入(如一次确认),通过 tasks/update 回应
completed完成,result 字段就是原本同步返回的内容
failed失败,error 字段是 JSON-RPC 错误
cancelled已取消(不保证被真正执行)

CreateTaskResult 里带上 taskId、初始状态、 TTL 和建议轮询间隔;客户端用 tasks/get 查状态,需要补充输入时用 tasks/update 回应。服务端如果支持,也可以通过 subscriptions/listen 推 notifications/tasks,省掉轮询。

两个提醒:取消在 Tasks 里是协作式的——服务端确认收到意图,但不保证真的停下;能力要协商,客户端在每请求能力里声明该扩展,服务端也要在自己能力里宣告。相关做法我们在MCP 能力协商里展开过。

五个常见错误

  • 服务端不看 progressToken 就无条件上报进度。
  • 注册了取消回调,但循环体里从不检查 signal 。
  • 在 stdio 传输下期望”关流即取消”,结果对方根本收不到。
  • 进度通知重置超时,却没有最大超时上限。
  • 把长任务硬塞进同步调用,客户端一断连,已经干完的活儿全部丢失。

相关阅读


服务端一定要实现取消吗?
规范里取消是可选能力,但对长任务来说基本是必需的。不实现的后果是用户点”停止”后界面卡住,服务端还在空转烧资源。最小实现成本其实很低:循环里每次检查一次 AbortSignal 。

进度通知和 Tasks 该选哪个?
看时长。几秒到几十秒、客户端能保持连接的,用 progressToken 就够了;会跑几分钟以上,或者客户端可能断开重连(移动端、不稳定网络)的,用 Tasks 。任务 ID 是持久化句柄,断线后能拿同一个 ID 继续轮询。

收到取消后还能回响应吗?
不应该回。规范要求服务端停止处理并释放资源,且不为被取消的请求发送响应;客户端也要忽略之后才到达的响应。双方都要能处理这个竞态。

tasks/cancel 发出去就一定会停吗?
不一定。 Tasks 的取消是协作式的,服务端确认收到意图但不承担必须停止的义务。对真正需要强停的场景,要在服务端自己实现中断点,不能只依赖协议。

这篇有帮助吗?
云上的幻象
云上的幻象查看主页

七彩云博客,分享 WordPress 建站实战与 AI 工具测评,覆盖服务器运维、站长工具、软件资源与电商运营干货,专注原创实用的主题插件、网站加速与安全优化教程。

942文章4评论

相关文章

评论 (0)

欢迎你,新朋友,感谢参与互动!文明发言,理性交流 · 首次评论将在审核后展示