SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-28

无状态 MCP 服务器到底改了什么?

MCP 2026-07-28 修订版移除会话与 initialize 握手。了解这对反向代理、健康检查、超时、认证及负载均衡的具体影响。

什么是无状态 MCP 服务器

无状态 MCP 服务器不会在请求之间保存每个客户端的状态。每个请求都携带协议版本、客户端能力和服务器处理该请求所需的凭据,因此任何机器上的任何进程都可以处理任何请求。MCP(Model Context Protocol,代理访问工具时使用的线路格式)在修订版 2026-07-28 中将此设为规则,并移除了 initialize 握手及其底层的 HTTP 会话。本文只讨论该线路的服务器端。如果您还不熟悉代理端,学习 AI 代理的分阶段路径介绍了在需要了解这些 HTTP 细节之前,决定调用工具的完整流程。

这就是它在运维上的全部意义。服务器不保存每个客户端的状态,因此可以放在普通负载均衡器后面,无需会话保持;部署期间可以重启而不中断客户端;还可以运行 4 个相同的进程,而不是只能运行 1 个。面向会话的服务器如果没有额外的机制,无法实现这些特性。

Model Context Protocol 是无状态协议:处理请求所需的全部信息都包含在请求本身中。服务器独立处理每个请求;即使请求来自同一连接或数据流,也不应根据之前的请求推断状态。

无状态并不意味着服务器不存储任何数据。数据库、队列和缓存仍然存在。它表示该协议不会在连接上携带状态,因此服务器不能将某个连接、进程或打开的套接字视为“正在进行对话的客户端”的替代品。对于已经管理自身数据的应用,这种区别最容易理解:openGym 的只读 MCP 服务器可以回答有关训练历史的问题;这些数据存储在应用自己的数据库中,与具体请求所使用的连接无关。

移除的内容:2026-07-28 修订版

截至 2026 年 8 月,2026-07-28 是规范的当前修订版。与 2025-11-25 相比,它移除了五项用于支持会话的内容。

  • initialize 请求和 notifications/initialized 通知。规范完全不再执行握手(SEP-2575)。
  • Mcp-Session-Id 标头,以及通过 HTTP DELETE 终止会话的方式(SEP-2567)。
  • 独立的 HTTP GET 流,服务器原本通过该流推送通知。现在改用 subscriptions/listen,即响应为长连接流的普通 POST 请求。
  • SSE(服务器发送事件)流恢复功能。Last-Event-ID 标头和每个事件的 ID 都已移除,因此流中断后,正在处理的请求会丢失,客户端必须以新的请求 ID 重新发起请求。
  • pinglogging/setLevelnotifications/roots/list_changed。日志级别现在是每个请求的字段,位于 _meta 中的 io.modelcontextprotocol/logLevel

规范新增了一个方法,所有服务器都必须实现该方法。server/discover 会在一次调用中返回服务器支持的协议版本、功能和身份信息。它是现存最接近握手的机制,但客户端可以选择不调用它。

为何会话传输难以在生产环境中运行

2025-11-25 及更早版本中,服务器可以在初始化时生成会话 ID,并在 InitializeResult 响应的 Mcp-Session-Id 标头中返回该 ID。客户端随后必须在每个后续请求中发送该标头。协商出的协议版本和客户端能力保存在服务器内存中,并以该 ID 作为键。每项设计都会带来运维成本。

  • 重启会丢弃会话表。规范要求服务器对携带失效会话 ID 的任何请求返回 404 Not Found,并要求客户端使用新的 InitializeRequest 重新开始。每次部署都会导致所有已连接客户端重新连接。
  • 第二个副本不知道第一个副本的会话。横向扩展需要在负载均衡器上启用会话保持,或者使用所有副本在每次请求时都读取的共享会话存储。
  • 会话表会占用内存,并随着空闲客户端数量增加而增长。DELETE 是可选的;客户端关闭连接时如果未发送该内容,相关条目就会一直保留。
  • 列表结果可能因连接而异,因此在服务器前置缓存是不安全的。

移除会话后,这4个问题会同时消失。在修改任何配置之前,值得先理解这一变化。

现在每个请求都携带哪些信息

对 MCP 端点的每个 POST 请求都是独立的。协议版本和客户端能力通过请求体中的 _meta 传递,选定字段还会映射到 HTTP 标头中,使中间设备无需解析 JSON 即可根据这些字段进行路由。

POST /mcp HTTP/1.1
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
Authorization: Bearer <access token>

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_weather",
    "arguments": {"location": "Seattle, WA"},
    "_meta": {
      "io.modelcontextprotocol/protocolVersion": "2026-07-28",
      "io.modelcontextprotocol/clientInfo": {"name": "ExampleClient", "version": "1.0.0"},
      "io.modelcontextprotocol/clientCapabilities": {}
    }
  }
}

每个请求都必须包含 io.modelcontextprotocol/protocolVersionio.modelcontextprotocol/clientCapabilitiesclientInfo 不是必需字段,但客户端应发送该字段。缺少必需字段的请求格式错误,因此服务器必须使用 JSON-RPC 错误 -32602 和 HTTP 400 Bad Request 拒绝该请求。

每个请求都必须包含 Mcp-Method 标头。对于 tools/callresources/readprompts/get,必须包含 Mcp-Name。标头值必须与请求体一致。处理请求体的服务器发现两者不一致时,必须返回 400 Bad Request 和错误代码 -32020HeaderMismatch。这样规定是因为,负载均衡器根据标头进行路由,而服务器根据请求体执行操作时,实际使用的是两个不同的信息源。如果根据这些标头进行路由或速率限制,请先检查 MCP-Protocol-Version:早期版本从未验证标头与请求体是否一致,因此在这些版本中,标头值不可信。

版本不一致现在属于普通的单请求错误,不再导致握手失败。不支持所请求版本的服务器会返回 400 Bad Request,并附带错误 -32022UnsupportedProtocolVersion,同时在 data.supported 中列出其支持的版本。客户端从该列表中选择一个版本,然后重试。

状态的去向:令牌、游标和订阅

状态并没有消失,而是转移到了可查看和记录的位置。

凭据会放入每个请求。 没有可绑定身份的会话,因此访问令牌会随每次 HTTP 调用发送,并在每次调用时进行验证。详细信息请参见下方的身份验证部分。

游标必须携带自身的位置。 tools/listresources/listprompts/listresources/templates/list 上的分页使用不透明游标字符串,客户端不得解析或修改该字符串。在单进程服务器中,通常会以会话为键,将偏移量保存在内存中。没有会话后,游标必须包含足够的信息,使任意副本都能继续列出结果。因此,应将位置编码到游标中并对其签名,或将位置保存在所有副本共享的存储中。无效游标应返回 -32602。必须对游标签名,因为不透明游标仍是由客户端提供的输入,而您的代码会对其进行解码并信任其中的内容。

订阅属于请求,而不是连接。 需要变更通知的客户端会发送 subscriptions/listen,并通过筛选器指定所需的类型:toolsListChangedpromptsListChangedresourcesListChangedresourceSubscriptions。服务器返回 notifications/subscriptions/acknowledged,并保持该响应流处于打开状态。如果流断开,服务器不会保留任何状态,客户端会重新发送 subscriptions/listen 以恢复订阅。

跨调用的应用状态会变成显式句柄。 当服务器确实必须在调用之间记住某些内容时,规范给出的做法是由服务器生成一个标识符,并将其作为普通工具参数传回。该标识符会出现在工具架构中,可以记录到日志,并且绝不会由连接隐式提供。具有真实用户级数据的服务器,例如 自行托管的 MCP 电子邮件服务器,会使用这种模式,而不是使用会话:邮箱或草稿标识符是工具参数,因此任意副本都可以继续处理下一次调用。许多工具根本不需要句柄:由您自己的 SearXNG 实例提供支持的搜索工具接收查询并返回结果,不需要为下一次调用保留任何状态,也无需关心由哪个副本响应。

部署:反向代理、超时和运行状况检查

MCP 端点是一个接受 POST 的路径。大多数流量都是短请求和 JSON 响应,任何代理都能处理。例外是流式响应,此时代理的默认设置会造成问题。从笔记本演示迁移到在 VPS 上运行的 MCP 服务器时,需要调整的就是这一部分。

location /mcp {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 1h;
    proxy_send_timeout 1h;
}

proxy_buffering off很重要,因为 nginx 默认会缓冲代理响应,直到缓冲区填满或响应结束后才发送 SSE 事件。规范还要求服务器在 SSE 响应中发送 X-Accel-Buffering: no,nginx 会遵循该响应头。因此,正确的服务器会自行向代理传递正确设置。但仍应配置该指令,因为这是您可以控制的部分。

proxy_read_timeout的默认值为 60 秒。静默时间超过该时长的 subscriptions/listen 流会被 nginx 关闭,而不是被服务器关闭。因此,日志显示进程正常运行,但客户端显示流已断开。只在 MCP location 中调高该值,不要修改整个服务器的设置。规范还建议服务器在静默期间发送 SSE 注释行(以冒号开头的行)作为保活信号,这样可以完全避免中间设备因超时而关闭流。

Caddy 需要的配置更少。为提高线路传输效率,它默认会进行部分缓冲;当响应包含 Content-Type: text/event-stream 时会立即刷新。因此,无需额外指令即可支持流式传输。

mcp.example.com {
	reverse_proxy 127.0.0.1:8080 {
		health_uri /healthz
		health_interval 10s
	}
}

请注意该运行状况检查指向的路径。不要使用 GET 对 MCP 端点执行主动检查,因为只实现此版本的服务器会对 GETDELETE 返回 405 Method Not Allowed,而 Caddy 的默认健康检查方法是 GET。这样,代理会将完全正常的后端标记为已停止。应为代理提供类似 /healthz 的普通路径,并通过 POST 单独检查协议。

curl -sS https://mcp.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -H 'MCP-Protocol-Version: 2026-07-28' \
  -H 'Mcp-Method: server/discover' \
  -d '{"jsonrpc":"2.0","id":"health-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'

包含 supportedVersions 列表的 200 表示进程正在运行并且能够处理该协议。带有 JSON-RPC 错误 -32601404 表示进程正在运行,但不提供 server/discover;每个 2026-07-28 服务器都必须实现该功能。带有 -32022400 表示检查程序请求了此构建不支持的版本。依赖项升级后,正需要捕获这种情况。开源版 nginx 不提供主动健康检查,因此请在 upstream 上使用被动 max_failsfail_timeout,并从监控系统执行协议检查。

现在,滚动重启只会影响正在处理的请求,不会造成其他影响。先排空连接,等待未完成的 POST 请求结束,再启动新进程;失败的请求由客户端重新发送。仍会丢失的一类连接是已打开的 subscriptions/listen 流,因为该流是连接到某个特定进程的实时连接。无状态设计移除了会话亲和性,但没有移除当前已打开流的连接亲和性。任何路由规则都无法解决这一点。客户端可以区分这两种情况:以空的 subscriptions/listen 结果结束的流表示正常关闭;结束时没有该结果的流表示连接被丢弃,客户端可以据此决定重新连接。

现在首次可以使用缓存。列表方法的结果现在包含 ttlMscacheScope,而 cacheScope: "public" 告知共享中间设备可以缓存该响应。这样做是安全的,原因是列表结果不再因连接而变化;这是移除会话后直接产生的结果。

无会话时身份验证为何会发生变化

有会话时,可以在 initialize 处完成一次身份验证,然后将会话 ID 作为后续所有操作的凭据。这样使用的会话 ID 实际上是不限制受众、不设置过期时间且无法撤销的持有者凭据,并由您自己的服务器签发。移除会话后,这种捷径不复存在,替代方案也更加严格。

受保护的 MCP 服务器充当 OAuth 2.1 资源服务器。客户端发出的每个 HTTP 请求都必须携带 Authorization: Bearer <access token>,服务器会在每个请求上验证令牌。验证内容包括受众:根据 RFC 8707(OAuth 2.0 的资源指示器),服务器必须确认令牌确实是专门为自身签发的,不得接受或转发用于其他对象的令牌。客户端通过发送服务器规范 URI 对应的 resource 参数来请求正确的受众。

发现流程由质询触发。请求到达时如果没有可用令牌,服务器会返回 401 Unauthorized

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         scope="files:read"

客户端读取 resource_metadata,获取该文档(RFC 9728,即 OAuth 2.0 受保护资源元数据;MCP 服务器必须实现此规范),找到授权服务器并执行流程。权限不足的有效令牌会收到 403 Forbidden,其中包含 error="insufficient_scope" 以及执行该操作所需的作用域。

这会影响运行方式。现在令牌验证发生在每个请求上,而不是每个会话只验证一次。因此,每次调用都访问一次令牌内省端点会增加延迟。应优先使用可根据签名、受众和过期时间在本地验证的令牌,或按令牌缓存验证结果,并将缓存时间控制在较短窗口内。由于不再有会话保存身份,每次调用都必须根据令牌计算授权。这比会话模型更准确,也符合将凭据留在代理进程之外的通行做法,详见不要将密钥保存在 AI 代理中。作用域只限制请求到达服务器后令牌可以执行的操作;在代理运行的机器上,使用可添加工具权限规则和预算上限的 harness 插件决定实际发起哪些调用。

此修订版哪些内容成立,哪些内容不成立

上文内容全部描述的是修订版 2026-07-28。它并不适用于所有 MCP,也不适用于您去年部署的服务器。

使用 2025-11-25 及更早修订版的客户端和服务器仍采用握手模型。规范将这些修订版称为旧版修订版,并将每个请求携带元数据的修订版称为现代修订版。仅支持此修订版的服务器遇到旧版客户端时,应在 MCP 端点上对 GETDELETE 返回 405 Method Not Allowed,忽略任何 Mcp-Session-Id header,不生成或回显该 header,并忽略 Last-Event-ID,因为流无法恢复。跨两个时代的服务器可以在同一个端点上同时提供两种模式:携带现代 _meta 的请求以无状态方式处理,而 initialize 请求则选择旧版会话语义。

因此,在信任上述内容前,请先检查修订版字符串。如果您的 SDK 仍发送 initialize,则会话对您的部署仍然有效,上文所述的会话相关问题仍需由您管理。客户端同样如此:您自己的主机上运行的代理进程,例如在 VPS 上运行编码代理中的配置,只有在其使用的库支持现代修订版时,在这里才属于无状态。请先读取运行时协商使用的版本,再阅读规范中对应的修订版,并将本页视为对某个指定修订版的说明,而不是对整个协议的概述。

FAQ

无状态 MCP 服务器是否意味着我无法存储任何内容?

不是。无状态描述的是协议,而不是应用程序。数据库、队列和缓存仍可按原方式使用。变化在于,跨越多次调用的状态必须通过显式标识符引用,客户端需要在每次请求中传递该标识符,例如在工具参数中传递由服务器生成的句柄。不能做的是根据连接推断上下文:规范规定,服务器不得依赖同一连接上的先前请求来确定功能、协议版本或客户端身份,因为每个请求都会在 _meta 中提供这些信息。

我仍需在负载均衡器上配置会话保持吗?

普通请求不需要。在修订版 2026-07-28 中,每个 POST 都携带自己的协议版本、功能和凭据,因此任意副本都可以处理任意请求,使用轮询即可。唯一仍保持长生命周期的是 subscriptions/listen 响应流,它是与单个进程建立的单个开放连接。该进程结束时,连接也会结束,客户端会重新发送 subscriptions/listen 以重新建立连接。这属于连接生命周期,而不是会话亲和性,不需要任何路由规则来阻止它。

Mcp-Session-Id 和 HTTP GET 流发生了什么变化?

二者已在修订版 2026-07-28 中移除,依据是 SEP-2567 和 SEP-2575。仅实现此修订版的服务器,在 MCP 端点收到 GETDELETE 时,应返回 405 Method Not Allowed,并且应忽略 Mcp-Session-Id 标头,而不是将其原样返回。服务器发起的变更通知现在通过 subscriptions/listen 请求的响应流传输,不再使用独立的 GET 流。必须继续服务旧客户端的服务器,会同时实现早期修订版的行为。

没有握手时,如何对 MCP 服务器执行健康检查?

分两层执行。将代理的主动检查指向应用提供的普通 HTTP 路径,因为对 MCP 端点执行 GET 会正确返回 405,从而将健康的后端误判为已停止。然后通过 POST 发送 server/discover 来检查协议本身;每个 2026-07-28 服务器都必须实现该方法,并确认响应的 HTTP 状态为 200,且列出的协议版本是客户端使用的版本。带有 JSON-RPC 错误 -32601404 表示进程正在运行,但未提供该方法;带有 -32022400 表示该构建不支持请求的版本。