SEP-2663:Tasks 扩展
社区译文;来源: MCP 29affa6b
| 字段 | 值 |
|---|---|
| SEP | 2663 |
| 标题 | Tasks 扩展 |
| 状态 | Final |
| 类型 | 扩展跟踪 |
| 创建日期 | 2026-04-27 |
| 作者 | Luca Chang (@LucaButBoring), Caitie McCaffrey (@CaitieM20);代表 Agents 工作组 |
| 发起人 | Caitie McCaffrey (@CaitieM20) |
| PR | #2663 |
摘要
本 SEP 定义一项扩展,允许服务器以异步的_任务句柄_响应 tools/call 请求,而不是立即返回最终结果;客户端随后可以通过轮询获取最终结果。该扩展引入三个方法:tasks/get、tasks/update 和 tasks/cancel;一个多态结果判别字段(resultType: "task");以及一种携带任务状态、进行中的服务器到客户端请求、最终结果或错误的 Task 结构。任务由服务器决定创建:客户端在每个请求的能力中包含该扩展以表示支持,服务器则逐请求决定是否将操作实例化为任务。
Tasks 将成为 MCP 的基础构件,预计会在未来的协议版本中得到支持。2025-11-25 版规范中的实验性 tasks 功能,是协议扩展机制可用之前的过渡方案。如今,扩展已经正式确立,将 Tasks 移入官方扩展,可以让该功能根据更多真实实现反馈继续孵化和演进,而不受核心规范发布节奏约束。当该扩展稳定并得到广泛采用后,计划将其提升到核心协议中。
本提案将 2025-11-25 版本核心协议所规定的 Tasks _移出_核心协议,并迁移到扩展中。本提案还根据该版本发布后的实现反馈,以及 2026-06-30 版基础协议包含的若干变更,更新 Tasks 设计:
- SEP-2260:要求服务器请求与客户端请求关联
- SEP-2322:多轮往返请求
- SEP-2243:Streamable HTTP 传输的 HTTP Header 标准化
- SEP-2567:通过显式状态句柄实现无 Session MCP
- SEP-2575:使 MCP 无状态化
动机
实验性的 Tasks 功能为工具调用、信息征询和采样提供了另一种执行模式:接收方可以返回轮询句柄,而不是阻塞到最终结果就绪。实现经验暴露出若干问题:
握手很脆弱。 当前 Tasks 同时公开方法级能力(
tasks.requests.tools.call声明tools/call可以(MAY) 使用任务增强)和工具级execution.taskSupport字段,后者声明某个具体工具是否接受该增强。客户端通过在请求中传入task参数表达自身对 Tasks 的支持,但如果方法或工具不支持 Tasks,客户端 不得(MUST NOT) 包含该参数。因此,希望选择使用 Tasks 的客户端必须先调用tools/list来准备状态,才能发出任务增强请求;它也不能为了同构处理工具而盲目地给每个请求附加task参数。这套机制令人困惑、依赖隐式条件且容易出错。tasks/result是一个阻塞陷阱。 在当前流程中,客户端观察到input_required后,必须提前调用tasks/result,让服务器获得一条 SSE 流,以便通过旁路发送信息征询或采样请求。随后,tasks/result会持续阻塞,直到整个操作完成。这迫使客户端和服务器维持许多实现方并不想支持的长连接,并且与 SEP-2260 冲突;SEP-2260 完全禁止未经请求的服务器到客户端请求。在 SEP-2260 下,原本支持阻塞行为的 SSE 语义已不再适用。无法定义
tasks/list的作用域。 为避免客户端取消或获取自己无权访问的任务结果,所有任务都应绑定到某种“授权上下文”;其实现由各服务器根据自身定制的权限模型决定。然而,许多情况下无法建立这种绑定,此时任务 ID 就成为防止跨调用方混淆的唯一防线。在这种场景下,服务器支持tasks/list是不安全的。Tasks 原本也可以绑定到 session,但 SEP-2567 从协议中移除了 session。服务器无法单方面定义其他自然作用域:任务 ID 可以是服务器逐个识别且不可猜测的句柄,但如果没有额外状态,服务器就无法可靠地将两个互不相关的句柄关联到同一调用方。
除了实现问题外,Tasks 还面临另一个结构性问题:客户端托管的任务已无法表达。 SEP-1686 允许客户端托管用于信息征询和采样的任务,部分原因是避免将 Tasks 与工具调用耦合。SEP-2260 规定任何未经请求的服务器到客户端请求均无效;按定义,客户端托管任务中的每个服务器到客户端轮询请求都会属于未经请求的调用。
本提案通过重新设计该功能的若干方面,并将 Tasks 移入官方扩展来解决上述问题。把 Tasks 重新定义为官方扩展,可以让该功能有更多时间独立于核心规范孵化和演进,从而促进采用。作为重新设计的一部分,本提案将轮询生命周期整合到 tasks/get 和新的 tasks/update 中,移除阻塞式 tasks/result 方法。重新设计允许服务器在响应普通、未带 task 标志的请求时主动返回任务,以消除逐请求选择启用和预先调用 tools/list 的要求,仅依赖扩展能力作为唯一握手点。最后,为遵守 SEP-2260,本提案移除了客户端托管的信息征询与采样任务。
规范
MCP Tasks 扩展允许某些请求使用 Tasks 增强。Task 是持久状态机,携带其所增强请求的底层执行状态信息,供客户端轮询并延迟获取结果。每个 Task 都由服务器生成的唯一 Task ID 标识。
Task 适合表示高成本计算和批处理请求,也能自然映射到外部作业 API。
扩展标识符
此扩展的标识符为:io.modelcontextprotocol/tasks。
能力协商
客户端和服务器在各自的能力对象中声明对 Tasks 扩展的支持(使用 SEP-2575:使 MCP 无状态化更新后的形式):
// Client to server, in per-request capabilities
{
// Other request parameters...
"params": {
"_meta": {
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {},
},
},
},
},
}// Server to client, in response to server/discover
{
"result": {
// Other response parameters...
"capabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {},
},
},
},
}当前没有定义扩展专用设置;空对象表示支持。
已协商此扩展的服务器 可以(MAY) 自行决定逐请求用 CreateTaskResult 代替标准结果(例如 CallToolResult),以响应任何受支持的请求。服务器是唯一决策方;客户端不会在请求本身表达对 Tasks 的偏好。客户端声明扩展能力,并不表示它要求该请求必须返回 CreateTaskResult。
无论先前作过何种声明,服务器都 不得(MUST NOT) 向未在当前请求中包含扩展能力的客户端返回 CreateTaskResult。已协商此扩展的客户端 必须(MUST) 准备好处理其发出的任何受支持请求所返回的 CallToolResult 或 CreateTaskResult。如果客户端收到某种不支持任务增强的请求类型所对应的 CreateTaskResult,则 必须(MUST) 将其解释为该请求的无效响应。
如果服务器无法在不返回 CreateTaskResult 的情况下,为未声明此扩展能力的客户端处理请求,则服务器 必须(MUST) 返回错误码 -32021(Missing Required Client Capability),并在错误响应中指出所需扩展:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
// MISSING_REQUIRED_CLIENT_CAPABILITY
"code": -32021,
// Message provided for example purposes only. The content of this example message is non-normative.
"message": "Missing required client capability",
"data": {
"requiredCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}支持的方法
当前支持任务增强执行的方法如下:
tools/call
未来可以扩展本规范,使其他请求类型也支持 Tasks;实现 应该(SHOULD) 能够适应本规范后续修订新增的请求类型。
多态结果
符合任务增强条件的请求可以返回两种不同的结果结构之一:请求的标准结果,或 CreateTaskResult。判别字段是结果对象上的 resultType,该字段由 SEP-2322 引入:
// "task" is introduced by this extension.
type ResultType = "complete" | "input_required" | "task" | string;服务器返回 CreateTaskResult 时,必须(MUST) 将 resultType 设为 "task",使客户端能够将它与标准结果区分。对于 CreateTaskResult 之外的结果类型,服务器 不得(MUST NOT) 将 resultType 设为 "task"。
建议客户端实现者注意,现有返回固定结构的代码(例如返回 CallToolResult 的 tools/call 方法)不必改变其公开合同;它可以在内部透明驱动轮询流程,只向外公开最终完成的结果。新的实现接口 可以(MAY) 直接公开任务生命周期,供能够利用该能力的应用使用。
Tasks
Task 携带正在进行的工作的运行元数据。
interface Task {
/** Stable identifier for this task. */
taskId: string;
/** Current task status. */
status: "working" | "input_required" | "completed" | "cancelled" | "failed";
/**
* Optional message describing the current task state.
* This can provide context for any status, for example (non-normative):
* - Progress descriptions for "working"
* - Work blocked on "input_required"
* - Reasons for "cancelled" status
* - Summaries for "completed" status
* - Additional information for "failed" status (e.g., error details, what went wrong)
*
* This MAY be exposed to the end-user or model.
*/
statusMessage?: string;
/** ISO 8601 timestamp when the task was created. */
createdAt: string;
/** ISO 8601 timestamp when the task was last updated. */
lastUpdatedAt: string;
/**
* Time-to-live duration from creation in integer milliseconds, null for unlimited.
* The server may discard the task after the TTL elapses. This value MAY change
* over the lifetime of a task.
*/
ttlMs: number | null;
/**
* Suggested polling interval in integer milliseconds. Clients SHOULD honor
* this value to avoid overwhelming the server. This value MAY change over
* the lifetime of a task.
*/
pollIntervalMs?: number;
}Task 状态
Task 可以处于以下状态之一:
working:当前正在处理请求。input_required:服务器需要客户端提供输入,任务才能继续。tasks/get响应会在inputRequests字段中包含待处理请求。客户端 必须(MUST) 检查该字段,并 应该(SHOULD) 在后续tasks/update请求的inputResponses字段中提供响应。completed:请求成功完成,结果位于result字段中。这包括返回isError: true结果的工具调用。failed:请求因执行期间发生 JSON-RPC 错误而失败。Task 会包含error字段及 JSON-RPC 错误详情。此状态 不得(MUST NOT) 用于非 JSON-RPC 错误。cancelled:请求在完成前被取消。
Task 的派生结构内联了状态专用的 payload 字段,供 tasks/get 响应和 notifications/tasks 通知使用:
/**
* A task that is in a normal working state.
* Used by tasks/get and notifications/tasks.
*/
export interface WorkingTask extends Task {
status: "working";
}
/**
* A task that is waiting for input from the client.
* Used by tasks/get and notifications/tasks.
*/
export interface InputRequiredTask extends Task {
status: "input_required";
/**
* Server-to-client requests that need to be fulfilled during task execution.
* Keys are arbitrary identifiers for matching requests to responses.
*/
inputRequests: InputRequests;
}
/**
* A task that has completed successfully.
* Used by tasks/get and notifications/tasks.
*/
export interface CompletedTask extends Task {
status: "completed";
/**
* The final result of the task.
* The structure matches the result type of the original request.
* For example, a CallToolRequest task would return the CallToolResult structure.
*/
result: JSONObject;
}
/**
* A task that has failed due to a JSON-RPC error.
* Used by tasks/get and notifications/tasks.
*/
export interface FailedTask extends Task {
status: "failed";
/**
* The JSON-RPC error that caused the task to fail.
*/
error: JSONObject;
}
/**
* A task that has been cancelled.
* Used by tasks/get and notifications/tasks.
*/
export interface CancelledTask extends Task {
status: "cancelled";
}
/**
* A union type representing a task with optional inlined result/error/inputRequests fields.
* This type is used by tasks/get and notifications/tasks to provide complete task state
* including terminal results or pending input requests.
*/
export type DetailedTask =
WorkingTask | InputRequiredTask | CompletedTask | FailedTask | CancelledTask;Task 创建
服务器以 CreateTaskResult 代替请求的标准结果结构,表示该请求将异步处理。
// resultType: "task"
type CreateTaskResult = Result & Task;请求示例(CallToolRequest):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "New York"
}
}
}响应示例(CreateTaskResult):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "task",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "working",
"statusMessage": "The operation is now in progress.",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:40:00Z",
"ttlMs": 60000,
"pollIntervalMs": 5000
}
}嵌入的 task 是任务的初始状态,通常(但不一定)为 status: "working"。客户端使用 task.taskId 发起后续所有 tasks/get、tasks/update 和 tasks/cancel 调用。
任务完成持久化创建之前,也就是返回的 taskId 尚不能由 tasks/get 解析时,服务器 不得(MUST NOT) 返回 CreateTaskResult。在最终一致的环境中,服务器 必须(MUST) 等待状态达到一致后再响应。此要求使客户端无需为任务创建进行推测性轮询。
服务器实现将多轮往返请求与任务创建结合使用时(例如,工具在创建任务前需要通过 InputRequiredResult 完成信息征询),应该(SHOULD) 在返回 CreateTaskResult 前_同步_解决所有 MRTR 交互。
Task 轮询
客户端通过发送 tasks/get 请求轮询任务是否完成。
客户端在确定轮询频率时 应该(SHOULD) 遵循响应中提供的 pollIntervalMs。pollIntervalMs 在任务生命周期内 可以(MAY) 发生变化。服务器 可以(MAY) 对轮询频率高于所记录 pollIntervalMs 的客户端实施速率限制。
客户端 应该(SHOULD) 持续轮询,直到任务达到终态或调用 tasks/cancel。客户端 应该(SHOULD) 将任务 ID 持久保存,以便在崩溃或重启后恢复轮询。
请求
interface GetTaskRequest extends JSONRPCRequest {
method: "tasks/get";
params: {
/** Identifier of the task to query. */
taskId: string;
};
}响应
收到 tasks/get 请求后,服务器 必须(MUST) 检查任务状态并作出相应响应:
- 如果状态为
working,服务器 必须(MUST) 返回状态为working的Task对象。 - 如果状态为
input_required,服务器 必须(MUST) 返回状态为input_required的Task对象,并包含 多轮往返请求 中定义的inputRequests字段。inputRequests字段 必须(MUST) 包含服务器发给客户端、且任务继续执行前尚待满足的所有请求。 - 如果状态为
completed,服务器 必须(MUST) 返回状态为completed的Task对象,并通过result字段包含任务的最终结果。 - 如果状态为
cancelled,服务器 必须(MUST) 返回状态为cancelled的Task对象。 - 如果状态为
failed,服务器 必须(MUST) 返回状态为failed的Task对象,并包含执行期间发生的错误。
type GetTaskResult = Result & DetailedTask;响应携带与任务当前状态对应的响应变体(参见 Task 状态)。由于这是 tasks/get 请求的标准结果结构,该对象的 resultType 字段 必须(MUST) 设置为 "complete"。
如果任务的 ttlMs 非空,客户端 可以(MAY) 将 TTL 作为最终保障:若从 createdAt 加上 ttlMs 所得时刻已经过去,但任务的可观察状态仍未反映更新,客户端 可以(MAY) 认为该任务已不可再用。相应地,服务器在 TTL 到期后的任何时刻 可以(MAY) 将任务标记为 failed,并可在之后随时删除该任务。ttlMs 的值在任务生命周期内 可以(MAY) 发生变化。
Task 更新请求
当任务需要客户端提供输入时(由 input_required 状态表示),服务器会在 tasks/get 响应的 inputRequests 字段中包含尚待处理的请求(参见 多轮往返请求)。客户端通过后续一个或多个 tasks/update 请求中的 inputResponses 字段提供响应。
当客户端观察到状态为 "input_required" 的 tasks/get 响应(或 notifications/tasks 通知)时,客户端 应该(SHOULD) 发送一个或多个包含对应 inputResponses 的 tasks/update 请求,以满足 inputRequests 中尚待处理的请求。发送 tasks/update 后,客户端 应该(SHOULD) 继续通过轮询(tasks/get)或通知(notifications/tasks)观察任务状态,直到任务达到终态。
客户端 必须(MUST) 像处理等价的独立服务器到客户端请求一样处理 inputRequests 中的每一项。例如,通过 inputRequests 呈现的信息征询请求,与直接的 elicitation/create 请求适用相同的信任模型和面向用户的行为。客户端 应该(SHOULD) 对连续轮询中重复出现的 inputRequests 键去重,避免向用户或模型多次呈现同一请求。
单个任务生命周期内,inputRequests 中的每个请求键 必须(MUST) 唯一。某个键对应的响应交付后,服务器 不得(MUST NOT) 将该键复用于后续服务器到客户端请求,也 不得(MUST NOT) 在任务生命周期内使用同一个键指代两个不同请求。这样可保证以同一标识符为键的 inputResponses 始终指向客户端预期的请求,消除客户端跨轮询去重时的歧义,并使服务器能够忽略未知请求或已满足请求对应的 inputResponses。
请求
interface UpdateTaskRequest extends JSONRPCRequest {
method: "tasks/update";
params: {
/** Identifier of the task to update. */
taskId: string;
/**
* Responses to outstanding inputRequests previously surfaced by the
* server. Shape per MRTR. Each key MUST correspond to a currently-
* outstanding inputRequest key.
*/
inputResponses: InputResponses;
};
}响应
type UpdateTaskResult = Result; // empty acknowledgement
成功时,服务器 必须(MUST) 以空结果确认该请求。此确认是_最终一致_的:服务器 可以(MAY) 在任务的可观察状态(通过 tasks/get 或 notifications/tasks 获取)反映这些响应之前,先接受响应并返回确认。如果 taskId 不对应已知任务,服务器 应该(SHOULD) 返回 JSON-RPC 错误。客户端 应该(SHOULD) 跟踪 inputRequests 键,避免多次响应同一请求。
服务器 应该(SHOULD) 忽略映射到当前并非任务待处理请求键的任何 inputResponses 响应,包括从未发出过的键、已经响应过的键,以及对应请求已被取代的键。服务器 可以(MAY) 接受部分响应集合(即当前待处理键的严格子集);
由于这是 tasks/update 请求的标准结果结构,UpdateTaskResult 的 resultType 字段 必须(MUST) 设置为 "complete"。
Task 取消
客户端发送 tasks/cancel 请求,表示其希望取消正在进行的任务。任务取消 不得(MUST NOT) 使用 notifications/cancelled 通知。
请求
interface CancelTaskRequest extends JSONRPCRequest {
method: "tasks/cancel";
params: {
taskId: string;
};
}响应
type CancelTaskResult = Result; // empty acknowledgement
服务器 必须(MUST) 以空结果确认该请求。如果 taskId 不对应已知任务,服务器 应该(SHOULD) 返回 JSON-RPC 错误。取消处理是_最终一致_的:确认返回后,任务的可观察状态 可以(MAY) 仍为 working(或其他非终态);如果工作在取消生效前已经完成,任务最终 可以(MAY) 达到 cancelled 之外的终态。
取消采用协作式语义:请求只表示意图,由服务器决定是否以及何时执行取消。服务器没有义务实际停止工作,只需确认该请求。任务不保证最终转换为 cancelled。
客户端发送取消请求后便 可以(MAY) 删除与任务关联的所有状态(例如,不再需要保留已响应的 inputRequests 键列表)。客户端无需再次轮询 tasks/get 来等待任务达到 cancelled 状态。
由于这是 tasks/cancel 请求的标准结果结构,CancelTaskResult 的 resultType 字段 必须(MUST) 设置为 "complete"。
Task 状态通知
除响应客户端轮询外,服务器还 可以(MAY) 通过 notifications/tasks 通知推送状态更新:
export type TaskStatusNotificationParams = NotificationParams & Task;
export interface TaskStatusNotification extends JSONRPCNotification {
method: "notifications/tasks";
params: TaskStatusNotificationParams;
}要开始监听任务状态通知,客户端向服务器发送 subscriptions/listen 请求,并包含其关注的任务 ID 列表(参见 SEP-2575):
export interface SubscriptionsListenRequest extends Request {
method: "subscriptions/listen";
params: {
// Other existing fields...
notifications: {
taskIds?: string[];
// Other existing fields...
};
};
}服务器在确认通知中包含其同意发送任务状态通知的任务 ID 列表(如果有):
export interface SubscriptionsAcknowledgedNotification extends Notification {
method: "notifications/subscriptions/acknowledged";
params: {
notifications: {
/**
* Subscribe to notifications/tasks for specific task IDs.
*/
taskIds?: string[];
// Other existing fields...
};
};
}如果客户端请求任务状态通知,但未声明 io.modelcontextprotocol/tasks 扩展能力,服务器 必须(MUST) 返回说明缺少能力的 JSON-RPC 错误:
{
"jsonrpc": "2.0",
"id": 12,
"error": {
// MISSING_REQUIRED_CLIENT_CAPABILITY
"code": -32021,
// Message provided for example purposes only. The content of this example message is non-normative.
"message": "Missing required client capability",
"data": {
"requiredCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
}
}
}每条通知都携带当前状态下完整的 DetailedTask,与该时刻 tasks/get 会返回的内容相同。
通知:
{
"jsonrpc": "2.0",
"method": "notifications/tasks",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "completed",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:50:00Z",
"ttlMs": 60000,
"pollIntervalMs": 5000,
"result": {
"content": [
{
"type": "text",
"text": "Operation completed successfully."
}
],
"isError": false
}
}
}通知包含完整的任务对象,使客户端无需轮询 tasks/get 方法即可访问完整任务状态和最终结果。客户端在订阅任务状态通知之外仍 可以(MAY) 继续轮询 tasks/get,但并非必须如此。
任务的 subscriptions/listen 流上 不得(MUST NOT) 发送 notifications/progress 和 notifications/message 通知,本规范通常也不支持在任务上发送这两类通知。
Streamable HTTP:路由标头
通过 Streamable HTTP 传输发送 tasks/get、tasks/update 或 tasks/cancel 时,客户端 必须(MUST) 将 SEP-2243 定义的 Mcp-Name 标头设置为 params.taskId 的值。这样,传输中间件和负载均衡器便可将同一任务的后续请求路由到持有其状态的服务器实例;这通常是保证正确性所必需的。Mcp-Method 标头按照 SEP-2243 设置为 JSON-RPC 方法名。
消息流示例
设想一次简单的工具调用 hello_world,它需要通过信息征询让用户提供姓名。该工具本身不接收参数。
客户端按如下方式发出 CallToolRequest 来调用此工具:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "hello_world",
"arguments": {},
"_meta": {
// Other metadata...
"io.modelcontextprotocol/clientCapabilities": {
"extensions": {
"io.modelcontextprotocol/tasks": {},
},
},
},
},
}服务器通过自定义逻辑决定为这项工作创建任务,并立即返回 CreateTaskResult:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "task",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "working",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:50:00Z",
"ttlMs": 3600000,
"pollIntervalMs": 5000
}
}客户端收到 CreateTaskResult 后,开始轮询 tasks/get:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tasks/get",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}任务处于 "working" 状态时,服务器会对每次请求返回常规任务响应:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resultType": "complete",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "working",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:50:00Z",
"ttlMs": 3600000,
"pollIntervalMs": 5000
}
}最终,服务器执行到需要向用户发送信息征询的位置。它将任务状态设置为 "input_required" 来发出信号。客户端下一次发送 tasks/get 请求时,服务器通过 inputRequests 字段发送信息征询载荷。请注意,虽然任务的 inputRequests 与 SEP-2322 多轮往返请求在结构上相似,但二者是不同的机制:任务的 inputRequests 通过 tasks/get 呈现并通过 tasks/update 满足,而不是通过重试原始方法来满足。如果服务器在返回 CreateTaskResult 之前 需要客户端输入(例如用于决定是否继续),则在原始请求上使用多轮往返请求流程;如果服务器在任务执行_期间_需要客户端输入,则使用此处描述的 inputRequests/inputResponses 机制。
{
"jsonrpc": "2.0",
"id": 4,
"method": "tasks/get",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}{
"id": 4,
"jsonrpc": "2.0",
"result": {
"resultType": "complete",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "input_required",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:50:00Z",
"ttlMs": 3600000,
"pollIntervalMs": 5000,
"inputRequests": {
"name": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please enter your name.",
"requestedSchema": {
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}
}
}
}
}
}为完整起见,再考虑一种情况:用户尚未满足信息征询请求时,客户端恰好再次轮询 tasks/get。由于 inputRequests 实际上是任务关联的所有待处理服务器到客户端请求在某一时刻的快照,即便客户端已经见过这些信息,服务器仍会再次包含相同请求(为改善用户体验,建议客户端对键相同的 inputRequests 去重):
{
"jsonrpc": "2.0",
"id": 5,
"method": "tasks/get",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}{
"id": 5,
"jsonrpc": "2.0",
"result": {
"resultType": "complete",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "input_required",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:50:00Z",
"ttlMs": 3600000,
"pollIntervalMs": 5000,
"inputRequests": {
"name": {
"method": "elicitation/create",
"params": {
"mode": "form",
"message": "Please enter your name.",
"requestedSchema": {
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}
}
}
}
}
}用户输入姓名后,客户端使用满足请求的信息发出 tasks/update 请求:
{
"jsonrpc": "2.0",
"id": 6,
"method": "tasks/update",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"inputResponses": {
"name": {
"action": "accept",
"content": {
"input": "Luca"
}
}
}
}
}服务器确认该请求:
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"resultType": "complete"
}
}服务器异步处理响应,并将任务重新转换为 working 状态:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tasks/get",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}{
"id": 7,
"jsonrpc": "2.0",
"result": {
"resultType": "complete",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "working",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:50:00Z",
"ttlMs": 3600000,
"pollIntervalMs": 5000
}
}最终,服务器完成请求,因此保存最终 CallToolResult,并将任务转换为 "completed" 状态。客户端下一次发送 tasks/get 请求时,服务器把最终工具结果内联到任务对象中:
{
"jsonrpc": "2.0",
"id": 8,
"method": "tasks/get",
"params": {
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840"
}
}{
"jsonrpc": "2.0",
"id": 8,
"result": {
"resultType": "complete",
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
"status": "completed",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:50:00Z",
"ttlMs": 3600000,
"pollIntervalMs": 5000,
"result": {
"content": [
{
"type": "text",
"text": "Hello, Luca!"
}
],
"isError": false
}
}
}错误处理
Task 使用两种错误报告机制:
- 协议错误:协议层问题使用标准 JSON-RPC 错误
- Task 执行错误:底层请求执行期间发生的错误通过任务状态报告
协议错误
对于以下协议错误情况,服务器 必须(MUST) 返回标准 JSON-RPC 错误:
- 无效或不存在的
taskId:-32602(Invalid params)- 对于
tasks/get,服务器 必须(MUST) 返回此错误。 - 对于
tasks/update和tasks/cancel,服务器 应该(SHOULD) 返回此错误。
- 对于
- 内部错误:
-32603(Internal error) - 缺少必需的客户端能力:
-32021(Missing Required Client Capability)- 对于未声明能力却在
subscriptions/listen上请求任务通知的客户端,服务器 必须(MUST) 返回此错误。 - 对于未声明能力却发出
tasks/get、tasks/update和tasks/cancel请求的客户端,服务器 必须(MUST) 返回此错误。
- 对于未声明能力却在
服务器 应该(SHOULD) 提供信息充分的错误消息来说明错误原因。
示例:找不到 Task
{
"jsonrpc": "2.0",
"id": 70,
"error": {
"code": -32602,
"message": "Failed to retrieve task: Task not found"
}
}示例:Task 已过期
{
"jsonrpc": "2.0",
"id": 71,
"error": {
"code": -32602,
"message": "Failed to retrieve task: Task has expired"
}
}服务器不需要无限期保留任务。如果服务器已经清除过期任务,返回说明找不到该任务的错误属于合规行为。
Task 执行错误
底层请求在执行期间遇到 JSON-RPC 协议错误时,任务转换为 failed 状态。tasks/get 响应 应该(SHOULD) 包含提供失败诊断信息的 statusMessage 字段,并且 必须(MUST) 包含携带 JSON-RPC 错误的 error 字段。
failed 状态 不得(MUST NOT) 用于表示非 JSON-RPC 错误,例如以 isError: true 完成的工具结果。协议方法结果语境中的错误 必须(MUST) 使用 completed 状态,并在 result 字段中包含错误详情。这样可在使用 failed 状态的协议层故障与其他故障之间保持清晰分隔。
示例:发生 JSON-RPC 执行错误的 Task
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"resultType": "task",
"taskId": "786512e2-9e0d-44bd-8f29-789f820fe840",
"status": "failed",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:40:00Z",
"ttlMs": 3600000,
"statusMessage": "Tool execution failed: API rate limit exceeded",
"error": {
"code": -32603,
"message": "API rate limit exceeded"
}
}
}示例:工具调用以工具错误完成(isError: true)
对于在协议层成功完成,但返回工具层错误(由工具结果中的 isError: true 表示)的工具调用,任务达到 completed 状态,并在 result 字段中包含工具结果:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"resultType": "task",
"taskId": "786512e2-9e0d-44bd-8f29-789f820fe840",
"status": "completed",
"createdAt": "2025-11-25T10:30:00Z",
"lastUpdatedAt": "2025-11-25T10:40:00Z",
"ttlMs": 3600000,
"result": {
"content": [
{
"type": "text",
"text": "Failed to process request: invalid input"
}
],
"isError": true
}
}
}tasks/get 端点准确返回底层请求原本会返回的内容:
- 如果底层请求产生 JSON-RPC 错误,任务使用
failed状态,error字段 必须(MUST) 包含该 JSON-RPC 错误。 - 如果请求以结果完成(即使工具结果的
isError: true),任务使用completed状态,result字段 必须(MUST) 包含该结果。
保留项
tasks/方法前缀和notifications/tasks/通知前缀为此扩展保留。resultType的结果判别值"task"为此扩展保留。- 标签
io.modelcontextprotocol/tasks为此扩展保留。
理由
非应邀 Task 与即时结果
一项替代提案原本会单独处理即时结果情况,并采用略有不同的前提条件:_如果_支持 Task,_并且_客户端支持即时 Task 结果,_那么_服务器可以针对增强了 Task 的请求返回常规结果。这个版本的即时结果当时看起来更合适,因为它意味着不会在初版 Task 规范之上引入破坏性变更。
不过,考虑到我们正逐步摆脱有状态的协议交互,而且 Task 整体仍处于实验阶段,现在似乎值得提出更彻底的变更,以降低整体规范的复杂度,使 Task 此时成为 MCP 更“原生”的组成部分。具体而言,在允许即时结果之外再允许非应邀 Task,意味着将 Task 提升为面向所有持久操作的一等概念,而不是并行且有一定专用性的概念。
这恰好与提议中的 SEP-2322 一致,但二者并不耦合。
分离读取(tasks/get)与写入(tasks/update)
这次重新设计的早期草案允许 tasks/get 携带 inputResponses,以便一次往返同时提交响应并观察结果状态。这种混合会带来代价:它使读取路径不再幂等(重试 tasks/get 可能再次提交响应),迫使读取路径采用与写入相同的最终一致性模型,并让希望缓存读取或为读取去重的中间件更加复杂。拆分方法后,tasks/get 仍是任何层都能安全缓存或重放的纯幂等读取;写入语义及其最终一致性窗口则仅限于 tasks/update。
tasks/update 仅返回确认的响应结构源自同样的分离原则:服务器无需返回任何客户端无法通过后续 tasks/get 获得的读取数据,强制在响应中嵌入 Task 会重新引入我们试图避免的非幂等性。代价是每轮输入多一次往返,但只有任务确实需要客户端请求时才会付出这项代价。
Task 创建一致性
本提案引入以下新要求:
服务器在任务完成持久化创建之前,也就是返回的
taskId尚不能由tasks/get解析时,不得(MUST NOT) 返回CreateTaskResult。在最终一致的环境中,服务器 必须(MUST) 等待状态达到一致后再响应。此要求使客户端无需为任务创建进行推测性轮询。
与 tasks/update 和 tasks/cancel 不同,任务创建采用强一致性。必须如此,才能避免请求方发出推测性的 tasks/get 请求,否则请求方无法判断任务是被悄然丢弃,还是尚未创建。相对而言,tasks/update 和 tasks/cancel 可以采用最终一致性,因为客户端的行为并不取决于这些操作的结果(无论如何,客户端都可以继续轮询)。尽管一致的任务创建确实会增加此前未采用这种行为的分布式系统的延迟成本,但明确引入此要求可以简化客户端实现,并消除一种未定义行为来源。
这也与一般的长时间运行操作 API 一致:此类 API 通常要求操作一旦得到确认,就必须能通过轮询端点找到。
仅确认的取消
在 2025-11-25 的 Task 设计中,tasks/cancel 返回描述取消尝试后即时状态的任务。这种返回结构隐含同步读取要求,因为服务器必须查询任务状态来填充它;但取消在许多应用中本质上是异步的(由单独的工作进程决定是否以及何时执行),所以返回的任务对象在很多情况下只会重复下一次 tasks/get 所显示的内容。将 tasks/cancel 简化为确认,更符合该操作的实际语义:请求是一种信号,而不是状态查询。希望了解取消后状态的客户端,通过 tasks/get 获取状态,与观察所有其他状态时使用同一条代码路径。
确认的最终一致性与 tasks/update 的分离方式相同:服务器可以记录取消请求,并在工作进程实际转换任务状态之前响应,而不让客户端把该确认理解为强一致结果。
尽管 tasks/update 和 tasks/cancel 出于上述原因使用仅确认的响应结构,服务器对于明显无效的请求(例如未知 taskId)仍 应该(SHOULD) 返回错误。仅确认设计旨在避免成功路径上同步读取任务状态,并非抑制服务器能够在请求时检测到的错误。针对无效输入返回错误能让客户端更快得知出现问题,而不必通过后续 tasks/get 轮询间接发现。
与多轮往返请求组合
本提案引入以下新要求:
服务器实现将多轮往返请求与任务创建结合使用时(例如,工具在创建任务前需要通过
InputRequiredResult完成信息征询),应该(SHOULD) 在返回CreateTaskResult前_同步_解决所有 MRTR 交互。
同时支持 MRTR(SEP-2322)和此扩展的 tools/call 可以按顺序使用二者:先发送一个或多个 InputRequiredResult 交互以同步收集输入,再通过 CreateTaskResult 移交异步执行。这种组合是 resultType 判别字段的自然结果:每个响应都独立确定类型,客户端根据收到的值切换行为,且_无需_在两种模式之间保留任何状态。禁止这种组合需要施加一种协议层无法强制执行的人为约束,因为客户端无法提前知道服务器将创建任务。
两条流程即使共享字段名,也分别维护独立状态。MRTR 阶段在服务器返回任何 resultType 不为 "input_required" 的响应时结束,此时该阶段的 inputRequests 键已被消费。Task 阶段从 CreateTaskResult 开始,独立维护_自己的_ inputRequests 键。Task inputRequests 的键唯一性范围是该任务的生命周期,并不延伸至之前 MRTR 阶段的键。客户端无需跨两条流程去重。
向后兼容性
2025-11-25 版本中的实验性 Task 功能与此扩展不具备线协议兼容性。需要与两个接口互操作的实现可以在 SDK 层并行实现实验性流程和扩展流程,并根据协商出的协议版本以及对端声明的客户端能力进行分派。下表概括每种组合的预期行为:
| 协议版本 | tasks.*(旧版) | io.modelcontextprotocol/tasks |
|---|---|---|
2025-11-25 | 按 2025-11-25 规范使用旧版实验性 Task。客户端通过 CallToolRequest 的 task 参数逐请求选择启用 Task 增强;服务器按该规范使用 tasks/result、tasks/get、tasks/cancel 和(支持时)tasks/list。此扩展不适用。 | 2025-11-25 协议版本未定义此扩展。在该协议版本下,服务器 不得(MUST NOT) 将此能力视为启用 Task;请求应按客户端完全未声明 Task 能力的情况处理。 |
2026-06-30 | 旧版能力不属于此扩展。对于只声明旧版能力的客户端,服务器 必须(MUST) 将其视为未声明此扩展。如果服务器在支持此扩展的同时也支持 2025-11-25 Task 规范,则对于在该流程下创建的任务,服务器 应该(SHOULD) 继续允许此类客户端调用 tasks/get 和 tasks/cancel。 | 规范场景。完整 Task 生命周期按本文档规定执行,并与 2025-11-25 实验性功能存在以下线协议层差异:
|
返回标准 CallToolResult 结构(即从不选择创建任务)的服务器,仍完全符合此扩展。已协商此扩展的客户端对于任何增强请求都 必须(MUST) 处理两种结果结构。
安全影响
- Task ID 不可猜测性。 服务器 可以(MAY) 将 Task ID 用作访问其存储状态的持有者令牌。服务器 必须(MUST) 以足够的熵生成 Task ID,使第三方无法枚举或猜测它们。
- 认证绑定。 服务器 必须(MUST) 对每个 Task 相关请求执行身份认证和授权检查,确保客户端有权访问相应任务。
- 跨调用方关联。 由于不存在
tasks/list,服务器不会意外向某个调用方泄露另一个调用方的任务。这比2025-11-25Task 规范有所改进;在旧版规范中,范围划分不当的列表可能暴露无关的 Task ID。 - 输入请求信任模型。
inputRequests将来自服务器的信息征询和采样载荷经客户端传递给用户或模型。Host 必须(MUST) 对这些载荷应用与标准信息征询/采样请求相同的信任模型。Task 并非更高信任级别的通道。