MCP Tasks 扩展
MCP Tasks 扩展:协议详解
Model Context Protocol 的 Tasks 扩展(
io.modelcontextprotocol/tasks,SEP-2663),让 server 用一个持久化的 task 句柄代替阻塞式的长时间等待。本文聚焦协议本身——消息流、状态机、数据结构、路由头——C# SDK 只在需要处一笔带过。
一、为什么需要 Tasks
普通 tool 调用会阻塞到结果返回。对 CI 流水线、批处理、大计算、需要人工审批的流程,阻塞不可行:
- 连接被长时间占用,而客户端/网关普遍有超时;
- 连接一断,进度全丢,不可恢复;
- 看不到进度,也无法在执行中途与用户交互。
Tasks 的核心是 "call-now, fetch-later":server 判断请求会长跑时,不返回最终结果,而返回一个 taskId;客户端凭它轮询进度、按需补输入、最终取回结果。taskId 是耐用句柄,断线重连后还能接着 poll。
- 扩展标识符:
io.modelcontextprotocol/tasks - 当前仅
tools/call支持 task 增强;tasks/get/tasks/update/tasks/cancel是配套方法 - 可选扩展,客户端与服务端双向 opt-in 才生效
二、整体消息流
关键规则:
- 能力协商:客户端在每次请求的
_meta.io.modelcontextprotocol/clientCapabilities.extensions里声明扩展;server 在server/discover里 advertise。server 是唯一决定方,按请求逐个决定是否创建 task;客户端不在请求上表达"我想要 task"。 - 未声明扩展的客户端:server MUST NOT 返回
CreateTaskResult;若非它不可,返回-32003(Missing Required Client Capability)。 - 持久化先于响应:server MUST NOT 在 task 持久化之前返回
CreateTaskResult——即返回那一刻起,对该 taskId 的tasks/get必须能命中(哪怕打到别的实例)。
三、状态机
| 状态 | 含义 | 终态 |
|---|---|---|
working |
正在处理 | 否 |
input_required |
等待客户端输入,见 inputRequests |
否 |
completed |
成功,result 含最终结果(含 isError:true 的工具结果) |
是 |
failed |
执行中发生 JSON-RPC 协议错误,error 含错误 |
是 |
cancelled |
被取消(取消是协作式,不保证到达) | 是 |
completed / failed / cancelled 一旦到达不可再变。
四、数据结构
CreateTaskResult(创建 task 时返回,取代标准结果)
resultType 必须为 "task",用来和标准结果区分:
{
"resultType": "task",
"taskId": "786512e2-...",
"status": "working",
"statusMessage": "The operation is now in progress.", // 可选
"createdAt": "2025-11-25T10:30:00Z", // ISO 8601
"lastUpdatedAt": "2025-11-25T10:40:00Z",
"ttlMs": 60000, // number 或 null(无限)
"pollIntervalMs": 5000 // 建议轮询间隔,可选
}
GetTaskResult(tasks/get 的响应)
GetTaskResult = Result & DetailedTask——一个扁平对象,task 字段直接摊在 result 上(不套 task 键),resultType 必须为 "complete"。
公共字段与上面一致,按状态追加:
- working / cancelled:无额外字段。
- completed:加
result,必须是对象——即原始请求本该同步返回的结果(CallToolResult形状):
{
"resultType": "complete",
"taskId": "786512e2-...",
"status": "completed",
"createdAt": "...", "lastUpdatedAt": "...", "ttlMs": 3600000,
"result": {
"content": [ { "type": "text", "text": "Hello, Luca!" } ],
"isError": false
}
}
- failed:加
error(JSON-RPC error 对象),如{ "code": -32603, "message": "..." }。 - input_required:加
inputRequests(map),见下。
三个配套方法
| 方法 | 请求 params | 响应 |
|---|---|---|
tasks/get |
{ taskId } |
GetTaskResult(上面) |
tasks/update |
{ taskId, inputResponses:{ key:{...} } } |
空 ack { resultType:"complete" } |
tasks/cancel |
{ taskId } |
空 ack;取消是协作式 |
无效/不存在的 taskId:tasks/get MUST 返回 -32602(Invalid params)。没有 tasks/list——一个调用方看不到另一个的 task(安全:taskId 需高熵、可当 bearer token)。
五、中途输入(MRTR)
task 执行到一半需要用户确认/补数据时,进入 input_required,tasks/get 响应里带 inputRequests——每个条目就是一个完整的 server→client 请求(elicitation 或 sampling):
{
"status": "input_required",
"inputRequests": {
"name": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please enter your name.",
"requestedSchema": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
}
}
}
}
}
客户端通过 tasks/update 回填,key 与 inputRequests 对应:
{
"method": "tasks/update",
"params": {
"taskId": "786512e2-...",
"inputResponses": { "name": { "action": "accept", "content": { "input": "Luca" } } }
}
}
规则要点:
inputRequests是某一时刻所有未决请求的快照;客户端在同一 key 上要去重,避免重复向用户提示。- 每个 key 在一个 task 生命周期内唯一,不复用。
- server 可接受部分响应,未收齐前保持
input_required。 - 客户端必须用对待独立 elicitation/sampling 请求的同等信任模型来处理
inputRequests——task 不是更高信任的通道。
注意区分:执行中途要输入 → 用这里的
inputRequests/tasks/update;在创建 task 之前就要输入 → 用原始请求的 MRTR 流程(resultType: "input_required"重试),两者是不同机制。
六、Streamable HTTP 路由头
无状态化后 server 要活在负载均衡/网关后面。规范要求 POST 带标准头,让中间层不解析 body 就能路由:
Mcp-Method:每个 POST 都带,值 = JSON-RPC 方法名。Mcp-Name:只对携带 primitive 名字的请求——tools/call/prompts/get(→ body 的name)、resources/read(→uri)。
Tasks 额外规定:tasks/get / tasks/update / tasks/cancel 时,客户端必须设 Mcp-Name = params.taskId,让中间层按 taskId 把请求路由到持有该 task 的实例。server 校验头与 body 一致,不一致直接拒。
实践坑:C# SDK 里
tasks/get默认不要求Mcp-Name;若你手动注册请求 handler 并设了RoutingNameParameter = "taskId",就会强制要求它,导致只按基础规范发头的客户端(如 MCP Inspector)报Missing required Mcp-Name header。开发/单实例场景把它设为 null 即可;需要网关按 taskId 路由时才保留,并让客户端发Mcp-Name: <taskId>。
七、错误处理:两条通道
Tasks 严格区分协议级错误和执行级错误:
- 协议错误 → 标准 JSON-RPC error(如
tasks/get拿到无效 taskId 返回-32602)。 - 执行错误:
- 底层请求发生 JSON-RPC 协议错误 → task 进
failed,error带该 JSON-RPC 错误; - 请求正常返回、但工具业务失败(
isError: true)→ task 进completed,result里带那个isError:true的 CallToolResult。
- 底层请求发生 JSON-RPC 协议错误 → task 进
一句话:tasks/get 返回的,就是底层请求本该返回的东西——协议错走 failed,其余(含工具级错误)走 completed。
八、C# SDK 落地(要点即可)
- 包:v2 独立扩展包
ModelContextProtocol.Extensions.Tasks(2.0.0)。 - 服务端开启:
.WithTasks(new InMemoryMcpTaskStore())。 - 哪些 tool 支持 task:
.WithTasks(store, o => o.ExecutionModeSelector = ctx => ...),返回Synchronous(不支持)/Optional/Required。 - 持久化:实现
IMcpTaskStore接外部存储;无状态 HTTP 下 store 必须跨请求共享(singleton 或外部存储),否则tasks/get找不到 task。 - 构造响应:优先用 SDK 的
CompletedTaskResult/FailedTaskResult/ … 或让内置 handler 生成,别手搓 JSON——completed的result必须是 CallToolResult 对象,塞裸字符串会被客户端 schema 拒(expected: record, path: ["result"])。
v1(1.3/1.4)是内建 Core 的 experimental 版,用
[McpServerTool(TaskSupport=...)]+McpServerOptions.TaskStore;v2 与 v1 wire 不兼容。
九、踩坑速查
区分两类:协议层的坑源自规范本身的规则(换任何 SDK 都存在);SDK 层的坑是 C# SDK 的实现/API 造成的。
9.1 协议层(规范规则,与实现无关)
Mcp-Name是 taskId 而非工具名:tasks/get/update/cancel的Mcp-Name头规范要求等于params.taskId(基础规范里Mcp-Name只给tools/call/prompts/get→name、resources/read→uri,tasks 是特例)。Mcp-Method则每个 POST 都要带、且与 body 一致。- v1/v2 wire 不兼容:2026-07-28 的 Tasks(SEP-2663)与 2025-11-25 的 experimental Tasks 在 API 和协议层都不兼容——v2 client 打 v1 task-server 只拿到普通结果,反之亦然。这是规范级 breaking change。
9.2 SDK 层(C# SDK 实现/API 造成)
Missing required Mcp-Name header(tasks/get):tasks/get内置不要求Mcp-Name(GetRoutingNameParameter对它返回 null)。报这个错是因为自己注册的McpServerRequestHandler把RoutingNameParameter设成了"taskId",SDK 于是强制校验该头,而基础规范客户端(MCP Inspector)不发它。改法:开发/单实例设 null;要网关路由才保留并让客户端发头。- v2 找不到
TaskSupport属性:SDK 在 v2 把它从[McpServerTool]移除了;只在 v1.3/1.4 有,且 experimental(MCPEXP001)。包 < 1.3.0 也没有。v2 改用.WithTasks(store, o => o.ExecutionModeSelector = ...)。 - "让某 tool 不支持 task"的 API 变了:v1 用 attribute 的
ToolTaskSupport.Forbidden;v2 用ExecutionModeSelector返回McpTaskExecutionMode.Synchronous。照 v1 写法在 v2 编不过。 WithTasks无泛型重载:只有WithTasks(store)/WithTasks(store, configure),都要求传IMcpTaskStore实例,没有WithTasks<TStore>()从 DI 解析。DI 带依赖的 store 需在Build()后用"延迟壳"回填 provider。- 无状态 HTTP 下 store 必须共享:SDK 每请求 new 一个 server 实例,
IMcpTaskStore不注册 singleton / 不用外部存储的话,后续tasks/get落到空 store、永远找不到 task。 - taskId 由 store 生成,tool 不能自带:v2
CreateTaskAsync()无参、SDK 自己 mint id;v1 那种 toolreturn McpTask自带 id 的写法 v2 没了。要用外部 job id,得用AsyncLocal把 id 捅进CreateTaskAsync,或维护mcpTaskId ↔ jobId映射。 - experimental 诊断码要抑制:
MCPEXP001(v1 Tasks)、MCPEXP002(McpServerRequestHandler/RoutingNameParameter)、MCPEXP003(Apps)——用到需#pragma warning disable或<NoWarn>,否则编译报错。
另注(非 bug,是设计):持久化 store ≠ 容错。store 只保状态;真正在跑的计算若在 MCP 进程内,进程挂了就没了。要跨重启续跑需独立执行层(Service Bus / Temporal / Durable Functions)。
参考
- Tasks 扩展规范:https://tasks.extensions.modelcontextprotocol.io/specification/draft/tasks.html
- 2026-07-28 规范:https://blog.modelcontextprotocol.io/posts/2026-07-28/
- SEP-2663:https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2663
- C# SDK v2 Tasks API:https://csharp.sdk.modelcontextprotocol.io/v2/api/ModelContextProtocol.Extensions.Tasks.html

浙公网安备 33010602011771号