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.
已接受标准跟踪
| 字段 | 值 |
|---|---|
| SEP | 2575 |
| Title | 使 MCP 无状态 |
| Status | 已接受 |
| Type | 标准跟踪 |
| Created | 2025-06-18 |
| Author(s) | Jonathan Hefner (@jonathanhefner), Mark Roth (@markdroth), |
| Sponsor | Kurtis Van Gent (@kurtisvg) |
| PR | #2575 |
摘要
一种真正无状态的协议,其中每个请求都是自包含的,并且可以独立理解,因其内在的简洁性、可扩展性和可靠性而极具吸引力。当前的 Model Context Protocol(MCP)默认并不是无状态的。该规范要求进行一次初始化握手,用于在客户端和服务器之间建立会话状态,并在连接期间持续存在。 这种固有的有状态特性使 MCP 难以大规模运行。例如,将 MCP 服务器置于标准负载均衡器之后会很困难,因为客户端的会话会绑定到持有其状态的特定服务器实例。 本提案概述了一系列变更,旨在默认启用无状态 MCP,并采用一种协议复杂度与状态“按需付费”的模型。在该模型下,我们默认提供简单、无状态的特性,只在某些功能确实需要时才引入有状态、长连接的开销。 具体而言,本 SEP 提议移除建立状态的初始化握手,并以离散的、无状态的替代方案取而代之。这一初始步骤使每个请求都能独立处理,从而简化服务端逻辑,并为健壮、可扩展的部署铺平道路。动机
Model Context Protocol(MCP)规范目前强制要求一个有状态的初始化握手。这一设计选择给可扩展性、可靠性和实现简洁性带来了显著挑战。本 SEP 的动机是解决这些不足。有状态性的問題
核心问题在于,服务器必须保留先前请求的会话状态,才能理解后续请求。这与现代云原生系统的设计方向直接相悖;后者更偏好无状态服务,以获得更好的弹性和可扩展性。- 阻碍可扩展性: 最关键的问题是有状态 MCP 难以进行负载均衡。简单的无状态负载均衡器(例如 L4/L7 轮询)无法使用,因为它会将客户端请求路由到不同的后端服务器,而这些服务器都不会拥有正确的会话状态。运维人员不得不实现诸如粘性会话之类复杂且脆弱的方案,将客户端绑定到特定服务器。这会使基础设施复杂化,可能导致负载分配不均,并使服务的水平扩展变得不平凡。
- 弹性和故障容忍性较差: 在有状态模型中,如果处理某个客户端会话的特定服务器实例失败,该会话状态就会丢失。客户端必须检测连接失败、重新建立连接(很可能通过负载均衡器连接到新的服务器实例),并再次执行整个初始化握手。这个过程具有破坏性且效率低下,并增加了围绕“可恢复性”的复杂性。
- 实现复杂度增加: 当前模型给开发者带来了显著负担。
- 服务端: 开发者必须实现逻辑来创建、管理并最终回收每个客户端的会话状态。这是 bug 和内存泄漏的常见来源。
- 客户端: 开发者必须编写复杂代码来管理持久连接,并处理不可避免的网络故障和重连,包括断连后重新同步状态的逻辑。
设计原则
本提案建立了一个协议复杂度“按需付费”的模型,按照以下优先级原则指导:- 优先无状态: 只要可能,请求必须是自包含的,提供服务器处理它所需的全部信息,而不依赖先前请求中的状态。
- 优先状态引用: 如果完全无状态的交换不现实,应在每个请求中传递对状态的引用。
- 将有状态性视为最后手段: 只有在不存在更简单的替代方案来解决关键用例时,才应接受有状态逻辑和长生命周期流连接的复杂性。
传输一致性
关键是,这些无状态原则必须在所有传输方式中一致应用。保持stdio 和 http 实现同步,可以确保统一的开发者体验,使核心协议语义能够一次学习并在任何地方适用。这种一致性简化了传输无关库和工具的创建,并防止协议碎片化,即不同传输方式在根本上表现出不同的行为。一个单一且一致的协议模型对于健康的生态系统至关重要。
规范
概览
本规范从根本上将 MCP 交互模型重构为以无状态优先。当前,MCP 要求在任何资源交换之前必须进行一次强制性的三方初始化握手。该握手协商并建立以下关键信息:- MCP 协议版本
- 服务器能力和
serverInfo - 客户端能力和
clientInfo
注意: 会话管理(包括传输层和应用层)由 SEP-2322 和 SEP-2567 单独处理。此 SEP 仅专注于移除初始化握手,并为版本协商、发现和能力提供无状态替代方案。
协议版本
为了使请求自包含,之前在握手期间协商的元数据现在必须包含在每个请求中。HTTP
对于 HTTP 传输,协议版本 MUST 通过 HTTP 头传递。该头部值 MUST 与请求负载_meta 字段中提供的值匹配;否则服务器 MUST 返回 400 Bad Request(见 SEP-2243)。
MCP-Protocol-Version: 2025-06-18- 用途:通知服务器客户端在此特定请求中使用的是 MCP 规范的哪个版本。
- 要求:此头部是强制性的。服务器应拒绝缺少版本或版本不受支持的请求。
- 此头部 MUST 与下文请求中指定的值匹配。
每请求版本
protocol-version MUST 直接嵌入请求负载的 _meta 字段中。对于 HTTP,\_meta MUST 与相应的 HTTP 头匹配,否则服务器应返回 400 Bad Request。
以下 diff 展示了对 RequestMetaObject 所需的更改:
不受支持的协议版本
如果服务器收到一个其未实现的协议版本请求(无论该版本对服务器而言是未知的,还是已知但服务器选择不支持的,例如实验版或草案版),它 MUST 返回一个 JSON-RPC 错误响应。对于 HTTP,响应状态码 MUST 为400 Bad Request。该错误 MUST 符合以下结构:
版本协商流程
在没有初始化握手的情况下,版本协商会以内联方式进行:- 客户端发送请求时,在
MCP-Protocol-Version头和io.modelcontextprotocol/protocolVersion_meta字段中带上其首选协议版本。 - 如果服务器支持该版本,它会正常处理该请求。
- 如果服务器不支持所请求的版本,它会返回一个
UnsupportedProtocolVersionError,其中包含其supported版本列表。 - 客户端从列表中选择一个双方都支持的版本并重试。
server/discover,以便在发送其他请求之前了解服务器支持的版本。
服务器能力发现
为了让客户端适配不同的服务器实现,本规范引入了一个发现 RPC。它为服务器提供了一种标准机制,用于声明其支持的协议版本和能力。 服务器 MUST 实现server/discover。客户端 MAY 调用它,但并非必须——客户端可以在不先调用发现端点的情况下自由发起任何 RPC。如果客户端调用了不受支持的 RPC,服务器 MUST 返回 Method not found JSON-RPC 错误(-32601)。对于 HTTP,响应状态码 MUST 为 404 Not Found。
server/discover RPC
- 用途:允许客户端查询服务器支持的协议版本、能力以及其他元数据。
每请求客户端能力
为了完成与初始握手的解耦,客户端能力不再只在初始化时协商一次。相反,客户端 MUST 在每个请求中说明其能力。这确保服务器始终充分了解该特定事务中客户端能够处理哪些可选特性。空能力对象表示客户端不支持任何可选能力——服务器 MUST NOT 从先前请求中推断能力。每请求元数据 Schema
每个请求的_meta 都携带一小组之前位于初始化握手中的字段。完整的 RequestMetaObject 形状如下:
"io.modelcontextprotocol/protocolVersion":string— MCP 协议版本。必需。 协商细节见上文“协议版本”部分。"io.modelcontextprotocol/clientInfo":Implementation— 标识客户端软件。必需。ImplementationSchema 要求name和version;其他字段为可选。"io.modelcontextprotocol/clientCapabilities":ClientCapabilities— 本次请求中客户端的能力。必需。"io.modelcontextprotocol/logLevel":LoggingLevel— 本次请求期望的日志级别。可选。 如果缺失,服务器 MUST NOT 为此请求发送任何日志通知。客户端通过显式设置级别来选择接收日志消息。取代logging/setLevelRPC。
_meta 字段包含。需要客户端 roots 的服务器 MUST 通过 MRTR ListRootsRequest 机制请求它们(见 SEP-2322),这避免了在每个请求上放置可能很大的 root 列表,并遵循“按需付费”原则。
如果请求缺少任何必需字段,则该请求格式错误;服务器 MUST 使用 INVALID_PARAMS 拒绝它(对于 HTTP 还需返回 400 Bad Request)。
响应流
这些已声明的能力决定服务器可以在响应流中包含什么。SEP-2322(MRTR)定义了服务器到客户端的交互如何通过IncompleteResult 内联嵌入到响应中;本 SEP 指定这些交互由 RequestMetaObject 中声明的每请求 clientCapabilities 所约束。
对于 HTTP,任何请求的响应 MAY 以 SSE 流(Content-Type: text/event-stream)的形式返回,而不是单个 JSON 对象。只有通知(例如 notifications/progress、notifications/message)会作为独立消息在该流中传输,随后是最终结果。服务器到客户端的交互(sampling、elicitation、listRoots)不是作为独立请求发送的——它们作为输入请求嵌入在特定请求路径(例如 CallTool、GetPrompt、ListResources)返回的 IncompleteResult 中。客户端满足这些输入请求并重试原始请求。
请求取消
客户端如何取消正在进行的请求取决于传输方式:- HTTP。 关闭 SSE 响应流 MUST 被服务器视为取消该请求。由于每个请求都有自己的响应流,传输层断开是明确无歧义的。
- STDIO。 客户端 MUST 发送一个引用请求 ID 的
notifications/cancelled通知。STDIO 只有一个共享通道,因此没有可关闭的每请求流。
移除可恢复流
由于连接中断现在会隐式取消请求,因此可恢复的 SSE 流(通过Last-Event-ID 重连)被移除。它们与默认无状态范式相矛盾:恢复需要服务器在连接失败后保留每请求状态。
需要持久性或可恢复性的工作负载 MUST 改用 tasks 原语,该原语提供在连接中断后获取结果的显式机制。
缺少必需能力
服务器 MUST NOT 依赖客户端未声明的能力。如果处理某个请求需要客户端未在其clientCapabilities 中声明的能力,服务器 MUST 返回一个 JSON-RPC 错误,说明缺少的能力。对于 HTTP,响应状态码 MUST 为 400 Bad Request。
subscriptions/listen RPC
本 SEP 引入了一个新的 subscriptions/listen RPC,它取代了之前的 HTTP GET 端点,并确保 HTTP 与 STDIO 之间的一致行为。客户端使用它打开一条长生命周期通道,以便在特定请求上下文之外接收通知。
本版本协议中,Streamable HTTP 用于服务器到客户端消息的 HTTP GET 端点被移除。所有通信都使用 POST。
根据 SEP-2260,只有通知(不是请求)会在此通道上流动;服务器发起的请求使用 MRTR(见上文“响应流”),并限定于特定客户端请求的范围内。
请求 Schema
notifications 字段是必需的,并且客户端 MUST 明确选择加入其希望接收的每一种通知类型。如果 notifications 中的某个字段被省略(或设为 false),服务器 MUST NOT 发送该类型的通知。
确认通知
服务器首先发送此通知,以确认订阅已建立。该订阅是长生命周期的,没有自然的“完成结果”;它在以下情况结束:- 客户端显式取消它(HTTP 上关闭 SSE 流,或在 STDIO 上发送
notifications/cancelled); - 底层连接关闭(HTTP 超时、TCP 断开、STDIO 进程退出);或
- 服务器将其拆除(例如关机),在这种情况下它 MUST 关闭 SSE 流(HTTP)或发送引用该订阅请求 ID 的
notifications/cancelled(STDIO)。
多个并发订阅
客户端 MAY 同时拥有多个活动订阅(例如,一个监听 tools-list 变更,另一个监听资源更新)。每个订阅都由其SubscriptionsListenRequest 的 JSON-RPC 请求 ID 标识。
为了让 STDIO 客户端在单一共享通道上区分属于不同订阅的通知,作为活动订阅一部分传递的每个通知 MUST 在 _meta 中包含该订阅的请求 ID:
notifications/progress(它使用最初发起请求的 ID)。
停止订阅
- HTTP。 关闭 SSE 响应流即可停止订阅。
- STDIO。 客户端发送引用 listen 请求 ID 的
notifications/cancelled。服务器 MUST 停止向该订阅发送通知。
传输行为
HTTP。 客户端通过POST 发送 SubscriptionsListenRequest。服务器的响应是一个开放的 SSE 流(Content-Type: text/event-stream),且该流上的第一条 JSON-RPC 消息 MUST 是 SubscriptionsAcknowledgedNotification。
STDIO。 客户端可在任意时刻发送 SubscriptionsListenRequest。服务器 MUST 通过发送 SubscriptionsAcknowledgedNotification 来确认它。后续通知在双向 STDIO 通道上传输,每条都按上述描述附带订阅的请求 ID 标记。如果连接被终止(例如服务器崩溃并重启),客户端 MUST 重新发送 SubscriptionsListenRequest 以重新建立其订阅。
已弃用和已移除的 RPC
为了简化协议并与向每请求能力迁移保持一致,以下 RPC 方法和通知已被移除:initialize/notifications/initialized:初始化握手已移除。版本协商现在通过每请求的MCP-Protocol-Version头和_meta字段处理。能力发现由server/discover处理。logging/setLevel:已移除。日志级别现在通过'io.modelcontextprotocol/logLevel'_meta字段按请求指定。没有替代 RPC。roots/list:作为顶层服务器到客户端 RPC 已移除。需要客户端 roots 的服务器 MUST 通过 MRTRListRootsRequest机制请求它们(见 SEP-2322)。notifications/roots/list_changed:已移除。Roots 通过 MRTR 按需获取,因此不需要变更通知。resources/subscribe/resources/unsubscribe:这些方法已移除。资源订阅本质上是有状态的——服务器必须记住每个客户端订阅了哪些资源。取而代之,客户端在subscriptions/listen请求的notifications参数中声明其希望接收更新的资源。服务器在 listen 流上为匹配的资源发送notifications/resources/updated。ping:在双向上都已移除。服务器到客户端的 ping 被移除,因为服务器不能再独立发送请求。客户端到服务器的 ping 也被移除,因为任何正常的 RPC 调用已经证明服务器存活,而传输层机制(HTTP keep-alive、SSE 注释、STDIO 进程状态)能更合适地处理连接健康检查。
原理
默认采用无状态优先
本 SEP 的主要设计决策是移除强制性的初始化握手,使无状态交互成为该协议的默认模型。这个选择基于“按需付费”(pay as you go)原则,以及希望让 MCP 与现代云原生架构保持一致的愿望。通过将最简单的交互模型设为默认,我们降低了入门门槛,并减少了最常见用例的实现复杂度。这会立即支持直接的水平扩展,并提高系统的弹性,因为任何请求都可以由任何服务器实例处理。考虑过的替代方案:可选握手
我们考虑过的一个替代方案是保留现有的有状态握手,但将其设为可选。在这种模型下,客户端可以选择执行握手来建立持久会话,或者跳过它并发送自包含请求。为什么被否决:
支持两种并行的交互模型会显著增加协议及每个实现的复杂度。服务器和客户端都需要构建、测试并维护两套独立的逻辑路径,这会增加 bug 的暴露面。它也违背了“完成核心功能只应有一种清晰、显而易见的方式”这一设计原则。通过做出彻底的切换,我们确保整个生态系统能够向前发展,并受益于更简单、更具可扩展性且更稳健的基础。显式会话管理
本提案最初包含专门的sessions/create 和 sessions/delete RPC,用于管理逻辑会话的生命周期。
会话管理现在由 SEP-2567 单独处理,该提案建议彻底移除会话,并用显式状态句柄替代它们。这与核心维护者做出的 sessions-vs-sessionless 决策 保持一致。
职责分离
本提案的核心原则之一,是将单一的初始化握手“拆包”为一组离散的、单用途的 RPC。原始握手将协议协商和能力发现混合在一次复杂交互中。新设计明确将它们分离:- 发现:由
server/discover专门处理。 - 能力:通过
_meta字段按请求处理,或通过subscriptions/listenRPC 处理。
考虑过的替代方案:单一、整体式握手
我们本可以保留一个单一、整体式的握手 RPC,只是增加更多参数和复杂逻辑来支持无状态优先模型。为什么被否决:
一个包揽一切的 RPC 很难实现、测试和演进。它迫使所有客户端,即使是最简单的客户端,也必须了解协议中最复杂的功能。通过分离这些职责,我们让协议更容易学习并正确实现,同时也让它在未来更灵活、更具扩展性。向后兼容性
尽管本提案试图保留现有功能和使用场景,但它引入了一个根本性的、不向后兼容的变更。因此,它需要协议的新版本。支持多个版本
虽然本 SEP 移除了initialize 握手,但希望同时支持旧客户端和新客户端的服务器可以这样做。这样的服务器可以继续实现旧的 initialize RPC 以处理遗留客户端,同时也为更新后的客户端提供新的无状态 RPC(server/discover 等)。
服务器和客户端都应能够适当地处理版本变化。下面列出了两个示例场景,其中 vPrev 表示 SEP 之前的版本,vAfter 表示 SEP 之后的版本。
客户端(支持 vPrev)→ 服务器(vPrev, vPost)
- 客户端发送初始化
- 服务器支持 vPrev,因此会按规范返回初始化结果
- 客户端和服务器按
vPrev进行通信。
客户端(支持 vPrev, vPost)→ 服务器(vPrev)
对于 HTTP,客户端可以尝试任何 vPost 请求(例如,带有 MCP 协议版本头的tools/list)。服务器返回 400 Bad Request(或“不支持的协议版本”);客户端随后回退到 vPrev(并执行初始化)以用于后续请求。
对于 STDIO,客户端无法依赖逐请求错误来检测服务器版本。支持 vPost(不需要初始化)并且支持需要 initialize 的旧版本的客户端,应当先用 server/discover 探测,以确定应使用哪一个:
- 客户端发送带有 MCP 协议版本
_meta字段的server/discover,并将其设置为首选的 vPost。 - 如果服务器支持 vPost(或客户端也支持的任何 vPost 风格版本),客户端将使用发现到的版本进行后续请求。
- 如果服务器返回“不支持的协议版本”或“未找到方法”,客户端将回退到其支持的旧版本,并执行
initialize握手。
安全影响
没有会话握手时,每个请求都必须独立完成身份验证和授权。实现必须确保不会因为移除了初始化阶段而绕过认证。 除逐请求认证之外,本提案没有引入额外的安全问题。参考实现
// 待办事项常见问题
什么是协议级无状态?
维基百科 将无状态协议定义为:无状态协议是一种通信协议,在这种协议中,接收方不得保留先前请求中的会话状态。发送方以一种方式将相关会话状态传递给接收方,使得每个请求都可以独立理解,也就是说,无需参考接收方保留的先前请求中的会话状态。这并不意味着你不能在无状态协议之上构建有状态应用。HTTP 就是无状态协议的一个例子,而今天大多数 Web 都建立在它之上。不过,这确实意味着状态不能存在于 协议本身 中,而应当改为在请求中指定状态(或者在无法做到时,提供一个供服务器或客户端跟踪的状态引用)。
这会让 MCP 成为完全无状态的协议吗?
并不完全是(因此是“默认如此”)。取决于你对“请求”的解释,文中提到的 SSE 流(包括客户端发起和服务器发起)往往会在一个流的上下文中包含多个请求。不过,这些流都被限制在单个 HTTP 请求内,而且是可选使用的,这意味着复杂性既受限,也只在情况需要时才可选使用。为什么让 STDIO 也保持无状态很重要?
MCP 使用的传输方式应该只是一种实现细节。如果某个协议版本支持的功能无法无缝映射到该协议的另一个版本,那它们实际上就是两个不同的协议。 这使开发者更容易将服务从一种传输切换到另一种,而无需对其应用行为做出重大更改,也更容易在不同传输之间正确进行代理。否则,这些不同实现之间将继续存在功能缺口和分裂,导致混乱与不兼容。server/discover 与 MCP Server Card 有什么关系?
server/discover RPC 与 MCP Server Card 提案有重叠,后者定义了一个用于基于 HTTP 发现的 .well-known/mcp.json 文档。两种机制都被有意保留:Server Card 非常适合 HTTP(无需认证、可缓存、可索引),而 server/discover 提供了一个统一的 RPC 接口,可在 HTTP 和 STDIO 传输之间保持一致工作。在适用的情况下,两者的内容应当保持一致。
开放问题
_meta 中应放什么,什么应作为顶层协议字段?
本 SEP 将几个先前在握手阶段协商的值(protocolVersion、clientInfo、roots、logLevel、clientCapabilities)放入 io.modelcontextprotocol/ 命名空间下的按请求 _meta 字段中。这符合规范允许由 schema 定义保留的“特定用途元数据”。
然而,这也存在 _meta 随时间被过度使用的风险——到什么时候我们还要重新添加顶层字段?一个可能的区分方式是:必需的协议级字段(例如 protocolVersion)也许更适合放在顶层字段中,而可选的或由扩展提供的值则保留在 _meta 中。在本 SEP 定稿前,这个问题值得更广泛的讨论。
clientInfo 应该成为 ClientCapabilities 的一部分吗?
目前,clientInfo(Implementation 类型)和 clientCapabilities(ClientCapabilities 类型)是分开的字段。在按请求模型中,将所有客户端元数据放在一个字段里可以减少开销。然而,clientInfo 的用途(身份/UI)与能力(功能协商)不同。clientInfo 应该并入 ClientCapabilities,还是保留为一个独立的按请求 _meta 字段,或者完全通过另一种机制处理(例如仅通过 subscriptions/listen 发送)?