7 月 28 日,MCP(Model Context Protocol)发了个让我盯着屏幕愣了几秒的大版本:把自己最核心的功能之一,协议级会话(session),整个删掉了。更反常识的是,我追了三天讨论,几乎所有人都在说删得好。这篇讲清无状态化(stateless)到底改了什么、为什么全网叫好、你的 server 要不要跟着改,末尾顺手检讨我 6 月旧文里一个已经失效的前提。
先交代消化过程:公告当天我把原文啃了两遍,之后几天泡在社区讨论里,又用官方 SDK 写了个最小 server 动手验证。这篇是消化完的产出,不追热点节奏,追确定性。
无状态化到底改了什么:一张对比表说清
改动集中在传输层。按官方公告和规范文本的表述:传输层走向无状态,协议级 session 和 initialize 握手被移除,会话语义上移到应用层。翻译成人话:以前 client 连上 server 要先握手、领一个会话身份,之后每次调用都挂在这个会话上;现在这个环节没有了,每次请求自带全部信息,server 处理完就忘。
改版前的会话模型长什么样,值得复述一遍,它是后面所有讨论的地基。client 连上 server,第一件事不是干活,是 initialize 握手:互报版本、协商能力、领一个会话身份;之后的每次工具调用都挂在这个身份上,server 端为它留一份对应状态。这套模型在”一个 client 对一个 server”的本地场景没什么毛病,一旦 server 要上服务器、面向很多 client,就处处别扭。
什么没变也得说清,不然恐慌会传染。Client/Server 架构没变,Tools、Resources、Prompts 三类原语没变,工具定义用 JSON Schema 描述的惯例没变。MCP 还是那个 MCP,动的只是”会话归谁管”这一层。
| 维度 | 改版前 | 改版后 |
|---|---|---|
| 会话(session) | 协议级,server 端持有,请求绑定在会话上 | 协议层移除,会话语义上移到应用层 |
| initialize 握手 | 连接建立后先协商能力,才能正式调用 | 移除,请求即用 |
| 部署语义 | 会话粘住实例,扩容要操心会话往哪搬 | 请求自包含,打到任意实例等价 |
| 失败重试 | 重试要顾及会话一致性 | 换实例重发即可,前提是调用本身幂等 |
一句话概括这张表:改版前,会话是协议的财产;改版后,会话是应用的财产。 状态搬家这件事,我画成了图:
从插件协议到 HTTP 级基础设施:无状态白送三样东西
判断一个协议是”插件”还是”基础设施”,有个土办法:想象它的 server 要服务 100 万用户,运维半夜被告警叫醒时,手里有没有现成的家伙式。写 Web 后端的人都背过这堂入门课:想让服务能水平扩容(horizontal scaling),第一步就是把服务改成无状态。MCP 改版前把这套积累全浪费了:一个长在 HTTP 上的协议,却要求 server 记住每条连接的状态。
白送的第一样是负载均衡(load balancing)。 改版前一个会话必须粘在一个实例上,术语叫粘性会话(sticky session),扩容、滚动发版、实例宕机,每一样都得额外设计,做过老系统的人都懂,会话粘住了实例,连发个版都得挑深夜;改版后请求自包含,负载均衡器随便分发,落到哪个副本都一样。
第二样是重试。 以前请求失败不能随便换个实例重发,因为会话状态只活在原来那台机器的记忆里;现在失败就换一台重发,只要调用本身幂等(idempotent),调用方什么都不用多做。
第三样是网关缓存(gateway cache)。 像工具列表这种问一百遍答案都一样的请求,从此有了被网关拦下直接回缓存的想象空间。我两周前实测过 Coding Agent 的 token 账单,工具定义是每轮常驻前缀的大头之一,这类请求将来若真能在网关层缓存,取回路径会短一截。要说明这是间接推论,规范文本没承诺这种用法,但门确实开了。
三样加起来指向同一个结论:MCP 从”给 AI 应用装的插件协议”,变成了能按普通 Web 服务的方式部署、扩容、运维的东西。 一个月前 WebKit 官方推出 Safari MCP server,当时大家的观感是”官方下场示范”;现在协议把部署门槛又降了一截,这类官方 server 可以真的按基础设施的标准去做。
误区拆解:无状态等于丢了会话能力
常见理解:session 都删了,MCP 是不是就不会话了?多轮任务里 server 每轮失忆,Agent 退回”每次重新自我介绍”的原始时代。这是这几天社区里的高频质疑,也是我刷到公告时的第一反应。
为何错:状态没有消失,只是搬家了。删掉的是”协议层替你管会话”这项默认服务,不是会话本身。类比就在 Web 里:HTTP 从出生那天起就是无状态的,购物车和登录态从来不是 HTTP 提供的,而是 Cookie、JWT 这些应用层机制几十年里长出来的分工:协议管单次请求,应用管跨请求状态。没有人因为 HTTP 无状态,就说 Web 做不了电商。JWT 那一代甚至把状态直接签进令牌里随身携带,和”每次请求自带全部信息”是同一个思路。
正确理解:无状态化是 MCP 在补这堂课:协议只负责把单次调用可靠送达,跨调用的上下文归应用层管。对 Agent 开发者,会话的落点反而更清楚了:客户端侧的对话历史、项目记忆文件(我写过 CLAUDE.md 使用指南),或者专门的外部存储。状态没有丢,只是从协议层收回到应用层,这是全文我最想让你带走的一句。
动手验证:一个最小无状态 MCP server
光读公告不算消化,我抽了一个下午用官方 SDK 写了个最小 server,验证两件事:不做 initialize 握手,工具调用能不能直接跑通;两个进程轮流接请求,结果是否一致。先看改版前大家习惯的写法,问题就藏在这几行里:
# 改版前的典型写法(示意):会话状态长在 server 进程里sessions: dict[str, dict] = {}
async def handle(session_id: str, request: dict): sess = sessions.setdefault(session_id, {"history": []}) sess["history"].append(request) # 同一个会话必须路由回同一个进程 return do_something(sess) # 业务逻辑依赖这份进程内状态这个进程内的 sessions 字典,就是无状态化要拆掉的东西。改写后的最小 server 如下。各家 SDK 对新规范的跟进还在进行中,下面是伪代码级示意,类名与导入路径以官方 SDK 文档为准,别直接抄:
# 最小无状态 MCP server(伪代码级,具体 API 以官方 SDK 文档为准)# 验证目标:server 端不挂任何会话状态,同一请求打到哪个实例结果都一致from mcp.server import Server # 导入路径以你所用 SDK 的文档为准
app = Server("word-counter")
TOOL_SCHEMA = { "name": "count_words", "description": "统计一段文本的词数", "inputSchema": { "type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"], },}
@app.list_tools() # 工具列表是纯函数:不依赖任何调用历史async def list_tools(): return [TOOL_SCHEMA]
@app.call_tool() # 每次调用自包含:状态只在参数里async def call_tool(name: str, arguments: dict): if name != "count_words": raise ValueError(f"unknown tool: {name}") # 自查:函数体里读不到任何全局可变状态 return [{"type": "text", "text": str(len(arguments["text"].split()))}]
# 验证方法:同一份代码开两个进程,请求轮流打到两边# 预期:不握手直接调用可通;两边返回完全一致两个验证都过了。不握手直接调用,通;同一份代码开两个进程,请求轮流打到两边,返回完全一致;中途杀掉一个进程,后续请求换另一个照样答。进程变成了可丢弃的,这就是无状态最直白的含义。 昨天 Simon Willison 的解读发出来,我拿自己的验证结果对着读了一遍,思路能对上,这才敢动笔。
也交代一个卡点:第一版我差点把查询缓存挂回全局变量,手比脑子快,旧习惯还在。写无状态 server 最该提防的是自己的手指头,凡是 self. 后面跟着会变的东西,都值得多看一眼。
两个口径也要交代:我验证的是行为,不是性能和并发;SDK 对新规范的支持还在变动,你动手那天以当时的官方文档为准。
无状态的代价:长连接与有状态工具要多做的功
叫好归叫好,代价必须摆上桌面。无状态化不是免费午餐,是责任转移:以前协议替你管的事,现在应用层自己做。
最直接的受影响方是长连接场景。流式输出、进度通知这类依赖”一条活着的连接”的体验,改版前有协议级会话兜底;改版后连接管理、断线后的续传,都上移到应用层自己实现,这类 server 的改造量是实打实的。Web 那边的参照很清楚:无状态是常态,长连接是特例,特例就得自己多花做功。
第二类是自带状态的工具,典型是有缓存的搜索 server。我 6 月设想过一类带查询缓存和增量索引的检索工具,那种进程内状态现在要么外置到专门存储,要么接受缓存随实例漂移、命中率下降。协议层会话本来就不该放重状态,这次改版等于把”别把状态藏进程里”从最佳实践升级成了硬约束。
| 你的 server 长什么样 | 无状态化后的影响 | 要不要额外做功 |
|---|---|---|
| 纯计算型:转格式、统计、单次查询 | 几乎纯受益,扩容随便扩 | 基本不用 |
| 自带进程内缓存的搜索型 | 缓存随实例漂移,命中率可能掉 | 要:状态外置到存储层 |
| 多轮依赖型:上一步输出喂下一步 | 协议不再帮你传递中间状态 | 要:应用层自己带上下文 |
| 长连接流式型 | 会话兜底没了 | 要:补连接管理与续传 |
诚实版结论:server 是纯函数式的,这次改版对你是纯利好;状态藏在进程里的,这次改版等于强制做一次架构体检,而这个体检反正躲不掉,早做早省心。
我的旧方案怎么办:记忆不该长在 server 端
6 月底我写过 Agent 记忆架构全景,其中”MCP 与 RAG 的协同”一节画了张时序图,隐含前提是 MCP server 端持有会话记忆:Agent 每轮来检索,server 记得上一轮进行到哪。无状态化之后这个前提没了:会话记忆要么上移到 Agent 应用层,要么外置到专门存储,server 退回一个纯检索函数。有意思的是,那篇的核心论点本来就是记忆该归应用侧结构化管理,方向没被推翻,被推翻的只是那张图:server 从记忆的宿主降级成了记忆的搬运工。写这一节,也算给当时照着图搭方案的读者递个更正。
迁移检查清单:五条判断你的 server 要不要改
- 搜代码里的会话状态。 全局字典、
self.sessions、按连接 id 存的东西,出现任何一样,你的 server 就在改造名单上;一样都没有,基本不用动。 - 查握手依赖。 有没有逻辑默认”initialize 之后才能干活”,比如把能力协商的结果缓存起来。改版后请求即用,这类前置假设要清掉。
- 盘进程内状态。 缓存、计数器、增量索引,凡是在进程里越攒越多的,规划外置方案,别指望协议再帮你兜。
- 看部署配置。 配了会话亲和(session affinity)、粘性路由的,改版后可以拆掉,负载均衡换最普通的轮询都行。
- 测长连接行为。 有流式输出或进度通知的,把”连接断了怎么办”的应用层方案补上,协议不再负责这一块。
今天只做一个动作:打开你自己的 MCP server 项目,全局搜 session,搜出来的每一行就是你的改造清单。没有 server、只是 MCP 用户的话,什么都不用做,等客户端跟进就行。
一句话结论(2026-08-01):MCP 7 月 28 日的大版本把传输层改成无状态,移除协议级 session 与 initialize 握手,会话语义上移到应用层;状态没有消失,只是从协议搬回应用,换来负载均衡、重试、网关缓存这些 HTTP 级能力。
最后坦白一件事:两周前那篇 token 账单的结尾,我说数据齐了就写暑期第三篇,主题是 OpenCode 的账单实测。跳票了,MCP 这次改版等不了人,OpenCode 的账单往后放放。接下来我盯两件事:各客户端和 SDK 对无状态化的实际跟进进度,规范落地到大家真用上还有多远;以及 Safari MCP server 这类官方出品的示范效应,部署门槛降下来之后,“每个产品都配一个 MCP server”会不会从新闻变成日常。有结论再写。
参考链接
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时





