Documentation Index
Fetch the complete documentation index at: https://mcp.zhcndoc.com/llms.txt
Use this file to discover all available pages before exploring further.
已接受标准跟踪
| Field | Value |
|---|---|
| SEP | 2322 |
| Title | 多轮往返请求 |
| Status | 已接受 |
| Type | 标准跟踪 |
| Created | 2026-02-03 |
| Author(s) | Mark D. Roth (@markdroth), Caitie McCaffrey (@CaitieM20), |
| Sponsor | Caitie McCaffrey (@CaitieM20) |
| PR | #2322 |
摘要
本提案规定了一种简单的方法,用于在客户端发起的请求上下文中处理服务器发起的请求 (例如,在工具调用上下文中的澄清请求),而无需在各个服务器实例之间共享 存储层,也无需负载均衡具备状态性,这将显著降低 在常见情况下大规模运行 MCP 服务器的成本。它还减少了 HTTP 传输对 SSE 流的依赖,而 SSE 流在许多无法支持长连接的 环境中都会引发问题。 这种拟议的服务器发起请求处理方式将取代当前发送服务器发起请求的做法。这是一个破坏性变更。 本 SEP 还规定了服务器可以在哪些客户端请求的子集上发送服务器发起请求。与当前规范相比,这一范围有所缩小,这同样是一个破坏性变更。 之所以必须在此做出破坏性变更,是因为由于支持 SSE 流和服务器端状态所带来的运维复杂性,诸如 Elicitation、Sampling 和 ListRoots 之类的服务器发起请求功能在许多远程 MCP 服务器或服务器托管客户端中的采用率非常低,或者根本无法启用。动机
注意:本 SEP 旨在为处理任何客户端发起请求上下文中的任何服务器发起请求提供通用机制。为了清晰起见,本文档中我们将专门以工具调用作为任何客户端发起请求的代表,但应将其理解为同样适用于(例如)资源请求或提示请求;类似地,我们将以澄清请求作为任何服务器发起请求的代表,但应将其理解为同样适用于(例如)采样请求。 我们先从一个观察开始:MCP 工具有两种类型:- 短暂型:服务器端不会累积状态。
- 如果服务器需要更多信息来处理工具调用,它可以在收到额外信息时从头开始。
- 示例:天气应用、访问电子邮件
- 持久型:服务器端会累积状态。
- 在向客户端请求更多信息之前,服务器可能会生成大量状态;在收到客户端的信息后,它可能需要恢复这些状态以继续处理。
- 服务器可能需要在等待客户端提供更多信息的同时在后台继续处理,在这种情况下就需要服务器端状态来跟踪该持续进行的处理。
- 示例:访问代理、启动虚拟机并需要用户交互来操作该虚拟机
- 客户端发送工具调用请求。以此为例,假设负载均衡器碰巧将该请求发送到了服务器实例 A。
- 服务器 A 打开一个 SSE 流,并在该流上发送澄清请求。
- 客户端将澄清响应作为一个单独的请求发送,负载均衡器会完全独立于步骤 1 中所选实例来选择服务器实例。以此为例,假设负载均衡器碰巧将该请求发送到了服务器实例 B。
- 服务器 A 必须以某种方式发现发往服务器 B 的澄清响应。
- 然后服务器 A 在步骤 2 中打开的 SSE 流上发送工具调用结果。
- 跨服务器实例共享的持久化存储层:服务器可以
部署并管理一个持久化存储层(例如 PostgreSQL、Redis、
DynamoDB),使多个服务器实例能够将一个服务器实例上的澄清响应与另一个服务器实例上原本正在进行的
工具调用对应起来。这种方式有若干缺点:
- 持久化存储层极其昂贵,尤其是对于 可能原本并不具备此类存储层的短暂型工具(例如天气 工具)而言。
- 持久化存储层带来显著的可靠性问题: 它会成为关键依赖,因此也可能成为单点故障。为了避免这一点,它必须提供高可用、复制和备份机制。
- 持久化存储层会成为瓶颈,限制横向 扩展能力。地理分布式部署要么需要昂贵的 全局复制,要么需要粘性路由。
- 持久化存储层还带来显著的运维 复杂性。在横向扩展部署中,它需要 分布式锁或共识协议。它还需要特殊的 垃圾回收逻辑来判断何时可以清理共享内容, 这需要仔细权衡:过于激进地清理状态会降低存储成本,但会限制用户可响应的时长; 而较不激进地清理则能容纳响应缓慢的用户,但会 增加存储成本。
- 这种方式要求工具实现中具备特殊行为, 以便与持久化存储层集成。如今的 MCP SDK 并没有针对这类存储层集成的特殊钩子, 这意味着通过 SDK 编写内联代码会非常困难。
- 负载均衡中的状态性:借助 cookie,可以
让负载均衡层确保步骤 3 中的澄清请求被送达与步骤 1 中原始请求相同的服务器实例。该方式虽然
通常比持久化存储层更便宜,但也有以下
缺点:
- 它要求负载 均衡器进行特殊配置和行为,这通常难以管理。
- 它打破了正常的负载均衡模型,导致负载分布不均,从而增加运行服务的成本。
- 它要求客户端具备特殊行为,以传递用于状态性的 cookie。
- 它要求工具实现将澄清请求与正在进行的工具调用对应起来。(MCP SDK 中有一些代码来处理这一点,但在 HTTP 世界中这仍然是一个非常奇怪的模式。)
- 它没有故障容错能力。如果服务器实例宕机,所有 状态都会丢失,工具调用就需要从头开始。(这对短暂型工具不一定重要,但对持久型工具而言是个问题。)
规范
本 SEP 提出了一种在客户端请求上下文中处理服务器请求的新机制。这种新机制对于短暂工具和持久工具会有略有不同的工作流,后者将利用 Tasks。不过,这两种工作流都会使用相同的数据结构。Schema 变更
首先,我们引入InputRequests 的概念,它表示一组要发送给客户端的一个或多个服务器发起的请求;以及 InputResponses,它表示客户端对这些请求的响应。请求和响应都存储在一个以字符串为键的映射中。对于 InputRequests,映射值是服务器发起的请求(例如,elicitation 或 sampling 请求);而对于 InputResponses,映射值是这些请求的响应。下面是它在 typescript MCP schema 中的样子:
Result 中引入了一个新字段来指示 ResultType。客户端应解析该字段以确定消息中所包含 Result 的类型。如果未提供该字段,为了向后兼容,客户端应假定 ResultType 为 “complete”。Schema 变更如下:
tasks。
这些类型将用于两种不同的工作流:一种用于短暂工具,另一种用于持久工具。
客户端请求的服务器发起请求支持
许多ClientRequest 并没有明确的使用场景,需要服务器向客户端请求更多信息。本 SEP 在 SEP-2260 的基础上进一步限制了服务器何时可以向客户端发送服务器发起请求。
服务器 MAY 在以下 ClientRequest 上发送 InputRequiredResult 响应:
| ClientRequest | ServerResult | 支持 InputRequiredResult |
|---|---|---|
GetPromptRequest | GetPromptResult | Yes |
ReadResourceRequest | ReadResourceResult | Yes |
CallToolRequest | CallToolResult | Yes |
GetTaskPayloadRequest | GetTaskPayloadResult | Yes |
InputRequiredResult 响应。下表表示在撰写本 SEP 时,这排除了哪些 ClientRequest。
| ClientRequest | 支持 InputRequiredResult |
|---|---|
PingRequest | No |
InitializeRequest | No |
CompleteRequest | No |
SetLevelRequest | No |
ListPromptsRequest | No |
ListResourcesRequest | No |
ListResourceTemplatesRequest | No |
SubscribeRequest | No |
UnsubscribeRequest | No |
ListToolsRequest | No |
GetTaskRequest | No |
ListTasksRequest | No |
CancelTaskRequest | No |
TaskInputResponseRequest | No |
短暂工具工作流
对于短暂使用场景,除了输入请求之外,我们还引入了请求状态(request state)的概念。在服务器需要更多信息的情况下,请求状态会发送给客户端,客户端再将该状态回传给服务器,从而使服务器保持无状态。 我们将为短暂工具采用以下工作流:- 客户端发送工具调用请求。
- 服务器返回一个单独的响应,表明该请求尚未完成。该响应可以包含客户端必须完成的输入请求。它也可以包含一些请求状态,客户端必须将其回传给服务器。该响应会终止原始请求。通常它会作为单独的响应发送,而不是通过 SSE 流发送,不过目前(这在未来的 SEP 中可能会改变)也允许在 SSE 流中发送该响应,且可以跟随(例如)进度通知。如果这个未完成响应是通过 SSE 流发送的,那么它必须是 SSE 流中的最后一条消息,就像普通响应一样。
- 客户端发送一个新的工具调用请求,与原始请求完全独立。这个新的工具调用包含对第 2 步输入请求的响应。它还包含服务器在第 2 步中指定的请求状态。
- 服务器返回一个 CallToolResponse。
短暂工作流的真实示例
这个示例展示了requestState 如何支持由 Azure DevOps 自定义规则 驱动的多轮询问流程。该场景涉及一个 update_work_item 工具,它将一个 Bug 工作项转换为 “Resolved”。ADO 自定义规则在某些状态转换发生时要求特定字段,服务器使用迭代式询问来收集这些字段——在多轮交互中将上下文累积到 requestState 中,从而最终能够在不依赖任何服务器端存储的情况下执行最终更新。
请求状态的使用场景
“requestState” 机制提供了一种对同一个逻辑请求进行多次往返交互的方式。它主要有两个使用场景。使用场景 1:滚动升级
假设你正在对水平扩展的服务器实例进行滚动升级,以部署某个工具实现的新版本。旧版本有两个输入请求,键分别是 “github_login” 和 “google_login”。然而在新版本的工具实现中,它仍然使用 “github_login” 输入请求,但将 “google_login” 输入请求替换为新的 “microsoft_login” 输入请求。 如果第一次请求被路由到旧版本服务器,而第二次尝试(包含 input responses)被路由到新版本服务器,那么服务器会看到它需要的 “github_login” 的结果,但不会看到 “microsoft_login” 的结果。(它也会看到 “google_login” 的结果,但它已经不再需要它,所以无关紧要。)此时,服务器需要发送一个针对 “microsoft_login” 的新输入请求,但它也不想丢失已经获取到的 “github_login” 答案,因此它会使用 1685 中提出的那种状态来保留这些信息,而无需在服务器端存储状态。 这里的工作流如下:- 客户端发送工具调用请求,命中运行旧版本的服务器实例。
- 服务器返回一个未完成响应,指示 “github_login” 和 “google_login” 的输入请求。
- 客户端发送一个新的工具调用请求,其中包含对 “github_login” 和 “google_login” 输入请求的响应。这一次它命中了运行新版本的服务器实例。
- 服务器返回另一个未完成响应,指示客户端尚未提供的 “microsoft_login” 输入请求。然而,该响应还包含请求状态,其中包含已提供的 “github_login” 响应,因此客户端不需要再次提示用户输入相同的信息。
- 客户端发送第三个工具调用请求,其中包含对 “microsoft_login” 输入请求的响应,并回传服务器在第 4 步提供的请求状态。
- 服务器现在在请求状态中看到了 “github_login” 信息,在 input responses 中看到了 “microsoft_login” 状态,因此该请求现在包含了服务器执行工具调用并返回完整响应所需的一切信息。
使用场景 2:负载卸载
假设你有一个 MCP 服务器实例正在处理大量工具调用,并且它发现当前负载过重,因此想把其中一个正在进行的工具调用迁移到另一个服务器实例。然而,它已经在该工具调用上完成了大量处理,所以它不想简单地让调用失败并让客户端在另一个服务器实例上从头开始;相反,它希望保留已经累积的状态,以便恢复处理的任何服务器实例都可以从原服务器实例停止的地方继续。这可以通过发送一个包含请求状态但不包含任何 input requests 的未完成请求来实现。 这里的工作流如下:- 客户端发送原始请求,负载均衡器将其路由到服务器实例 A。
- 服务器实例 A 在做完大量计算后决定需要卸载负载。它发送一个未完成响应,在
requestState字段中包含其已累积的状态,但不包含inputRequests字段。 - 客户端使用附加了
requestState字段的请求进行重试。负载均衡器将此请求路由到服务器实例 B。 - 服务器实例 B 从
requestState字段中看到的状态开始,因此从服务器实例 A 停止的地方继续计算,并最终返回完整响应。
短暂工作流的协议要求
-
服务器行为:
- 服务器 MAY 对任何客户端发起的请求响应
InputRequiredResult。该消息 MAY 作为独立响应发送,或作为 SSE 流中的最后一条消息发送,不过实现方建议优先采用前者。如果使用 SSE 流,服务器 MUST NOT 在未完成响应消息之后再发送任何消息。 InputRequiredResultMAY 包含inputRequests字段。InputRequiredResultMAY 包含requestState字段。如果指定,该字段是仅对服务器有意义的不透明字符串。服务器可以自由地以任何格式编码状态(例如纯 JSON、base64 编码 JSON、加密 JWT、序列化二进制等)。- 如果请求包含
requestState字段,服务器 MUST 始终验证该状态,因为客户端是不受信任的中介。如果担心篡改,服务器 SHOULD 使用其选择的加密算法对requestState字段进行加密(例如,可以使用 AES-GCM 或签名 JWT),以确保机密性和完整性。请注意,还存在重放/劫持攻击的风险:经过身份验证的攻击者可能会重新发送原本发给另一个用户的状态。因此,如果请求状态包含任何特定于原始用户的数据,服务器 MUST 使用某种机制在密码学上将该数据绑定到原始用户,并且 MUST 验证客户端发送的requestState数据是否与当前已认证用户相关联。使用明文状态的服务器 MUST 将解码后的值视为不可信输入,并以与验证任何客户端提供数据相同的方式进行验证。
- 服务器 MAY 对任何客户端发起的请求响应
-
客户端行为:
- 如果客户端收到一条
InputRequiredResult消息,并且该消息包含inputRequests字段,那么客户端 MUST 在重试原始请求之前构造所请求的输入。相反,如果消息不包含inputRequests字段,那么客户端 MAY 立即重试原始请求。 - 如果客户端收到一条包含
requestState字段的InputRequiredResult消息,它在重试原始请求时 MUST 回传该字段的精确值。客户端 MUST NOT 检查、解析、修改requestState的内容,也不得对其内容作任何假设。如果InputRequiredResult不包含requestState字段,那么客户端 MUST NOT 在重试中包含该字段。
- 如果客户端收到一条
持久工具工作流
持久工具工作流将利用 Tasks。Tasks 已经提供了一种机制,用于表示完成请求还需要更多信息。input_required Task Status 允许服务器表明处理该任务还需要额外信息。
Tasks 的工作流如下:
- 服务器将 Task Status 设为
input_required。此时服务器可以暂停处理请求。 - 客户端通过调用
tasks/get获取 Task Status,并看到需要更多信息。 - 客户端调用
tasks/result - 服务器返回
InputRequests对象。 - 客户端调用包含
InputResponses对象以及 Task 元数据字段的tasks/input_response请求。 - 服务器恢复处理,并将 TaskStatus 设回
working。
Tasks 往往运行时间更长、与其关联状态更多,而且计算成本更高,因此请求更多信息并不会终止原始请求的操作(例如工具调用)。相反,一旦提供了必要信息,服务器就可以恢复处理。
为了与 MRTR 语义保持一致,服务器将对 tasks/result 请求返回一个 InputRequests 对象。这两者将具有相同的 JsonRPC id。当客户端以 InputResponses 对象进行响应时,这是一个带有新 JSONRPC id 的新客户端请求,因此需要一个新的方法名。我们建议使用 tasks/input_response。
上面的工作流以及下面的示例都没有使用任何可选的 Task Status 通知,不过本 SEP 并不排除其使用。
持久工作流的协议要求
-
服务器行为:
- 服务器 MAY 通过指示任务处于
input_required状态来响应tasks/get。 - 当任务处于
input_required状态时,服务器 MUST 在tasks/result响应中包含inputRequests字段。
- 服务器 MAY 通过指示任务处于
-
客户端行为:
- 当
tasks/get显示状态为input_required时,客户端 MUST 调用tasks/result以获取输入请求。客户端 SHOULD 构造这些请求的结果,然后调用tasks/input_response,连同 input responses 一起提供任务所需输入。 - 客户端 MAY 选择不满足这些输入请求,在这种情况下可以取消任务。
- 当
短暂与持久工作流之间的交互
如果某个工具实现需要客户端先响应一组输入请求,然后才能开始处理,但之后又需要进行持久化处理,那么它可以先使用短暂工作流,然后在那一时刻通过创建任务切换到持久工作流。这样可以避免服务器在真正获得开始处理请求所需的信息之前就必须存储状态。 该工作流如下:- 客户端发送带有 task 元数据的工具调用请求。
- 服务器返回
inputRequests响应,表明处理该请求还需要更多信息。该响应会终止原始请求。 - 客户端发送一个新的工具调用请求,与原始请求完全独立,其中包含
inputResponses对象以及 task 元数据。 - 服务器返回一个 task ID,表明它将会在后台处理该请求。后续所有交互都将通过 Tasks API 完成。
错误处理指导
本节为以下场景中的错误处理提供实现指导:客户端在inputResponses 对象中提供了意外或格式错误的数据。
与处理任何收到的请求一样,服务器 SHOULD 验证客户端提供的数据是否是有效的 inputResponses 对象,以及其中的信息是否可以被正确解析。协议错误,例如格式错误的 JSON、无效 schema,或阻止请求处理的服务器内部错误,应返回带有适当错误代码和消息的 JSONRPCErrorResponse。
如果在 inputResponses 对象中提供了额外参数,服务器 SHOULD 将其视为可选参数。因此,它 SHOULD 忽略 inputResponses 对象中任何它不认识或不需要的意外信息。
客户端也可能没有发送前一轮 inputRequests 中请求的全部信息。如果缺失的信息对于服务器处理请求是必要的,那么服务器 SHOULD 返回一个新的 InputRequiredResult。
我们曾讨论过返回一个特定的应用层错误码,但在所有场景下客户端可能都没有足够信息来恢复。因此,我们决定依赖现有的通过 InputRequiredResult 请求更多输入的机制,以确保客户端总是可以通过让服务器再次请求必要信息来恢复。
恶意客户端可能会故意在 inputResponses 对象中发送错误信息,并通过反复触发服务器请求相同信息来制造负载。不过,这并不是此工作流引入的新问题,因为恶意客户端此前就已经可以通过发送格式错误的请求来制造负载。服务器实现者可以使用速率限制和节流等标准技术来防范此类攻击。
在短暂工作流中,这将如下所示:
- 客户端重试原始工具调用,这次包含
inputResponses对象,但响应缺少服务器处理请求所需的必需信息。
- 服务器返回一个未完成响应,表明客户端需要对一个询问请求作出响应才能完成工具调用,并包含要回传的请求状态:
- 服务器返回一个未完成响应,表明客户端需要提供缺失信息以使请求成功。
JSONRPCResultResponse 确认已收到响应。然而,由于响应缺少必需信息,服务器不会继续处理该任务,并将 Task 状态保持为 input_required。客户端下次调用 tasks/result 时,服务器将返回一个新的 inputRequest,再次请求所需信息。
原因说明
我们考虑过采用双向流的方式来替代 SSE 流。 不过,这种方式会使线协议更加复杂(例如,它需要 HTTP/2 或 HTTP/3)。此外,它也不会消除那些无法支持长连接的环境中的问题,也无法解决容错方面的问题。 关于输入请求应该是一个 map 还是仅仅一个单独对象,曾有过讨论,也有人考虑利用请求内部的某个字段(例如 elicitation ID)来区分它们。我们最终决定使用 map,因为它在结构上保证了键的唯一性,这将避免 SDK 和应用程序中为了避免冲突而进行显式检查的需要。 在持久化工作流中,我们曾考虑将输入请求直接包含在tasks/get 响应中,而不是要求客户端先看到 input_required 状态,然后再调用 tasks/result 获取输入请求。我们决定将这两者分开,以尊重那些为任务状态和实际工具实现使用不同基础设施的实现方式;这样做的想法是,无论任务状态实际是什么,tasks/get 调用都应具有一致的延迟特征。我们认识到这需要额外一次到服务器的往返,但如果这成为问题,我们可以在未来进行优化。
向后兼容性
目前,许多 SDK 通过一种内联但异步的方式支持 elicitation:它会在原始 SSE 流上发送工具调用响应之前,等待 elicitation 响应。这种方式适用于单进程的 MCP Server,或者能够确保请求粘性路由的场景。安全影响
由于requestState 会经过客户端传递,恶意或被入侵的客户端可能会尝试修改它,以改变服务器行为、绕过授权检查或破坏服务器逻辑。为减轻这一风险,我们要求服务器按照上面的协议要求验证该状态。