一、从 Demo 玩具协议到企业级 AI 基础设施
新旧对比:有状态 vs 无状态
先放一张总表,后面各节都是它的展开。
| 维度 | 旧版有状态(Demo 版) | 新版无状态(生产版) |
|---|---|---|
| 会话依赖 | 必须 initialize 握手,建立会话后才能请求 | 无会话,请求自描述,一次请求一个响应 |
| 部署方式 | 会话状态在实例内存,必须粘性路由 | 任意副本都能处理请求,水平扩容无约束 |
| 扩展能力 | 会话在哪个副本,请求只能去哪个副本 | 加副本即加容量,请求均衡分发 |
| 生产可用性 | 实例宕机,挂在上面的会话全部不可用 | 单点故障无感知,请求由其他副本接管 |
旧版列有三个具体缺陷:宕机即会话不可用、扩容不能真正分摊负载、重启必须等会话迁移。这些在 demo 环境都无所谓,到生产就变成事故。
核心结论:这不是功能迭代
这次重构解决的问题非常具体:无法集群、无法扩容、无法上线、无法规模化。
举个我身边的真实案例:团队用旧版 MCP 起了 3 个副本,会话仍然绑定在最初握手的那个实例上。凌晨流量上来,扩容到 6 个副本,40% 的请求还是压在老实例上,压垮之后存量会话全部中断,报 502 报了一整晚。
我当时想,这不是加个新工具、改个参数能解决的,是协议假设错了。本次重构要解决的就是这四个「无法」。
二、为什么这次更新叫「史上最大重构」
灵活轻量化的另一面
MCP 早期设计强调灵活轻量:想加一个工具,写个 handler 注册就行。这个自由度在 demo 阶段是优点,到生产就成了乱象源头。
我见过的真实乱象,至少三种:
- 版本不统一。团队里五个服务,四个 MCP SDK 版本,握手流程、session 处理细节全不一样,联调全靠人肉对齐。
- 会话泄漏。有人把会话标识存全局变量,有人存 localStorage,连接断开后会话状态还留在内存里,资源越积越多。
- 无鉴权裸奔。MCP Server 直接暴露在内网,任何人连上来就能调工具,没有授权没有审计。
我后来总结成一句话:灵活 ≠ 简单,灵活 = 更需要规范。这些问题分别对应后文 5.4(授权)、5.5(规范)、6.1(迁移点位)的方案。
四大演进思维
这次重构的四个方向,每个都指向一类生产问题:
| 演进思维 | 能力落点 | 对应章节 |
|---|---|---|
| 规范化 | 协议流程统一、错误码标准化、参数校验、废弃机制 | 5.5 |
| 去中心化 | 抛弃会话粘性,适配云原生弹性扩缩容 | 5.1、6.4 |
| 能力补齐 | MCP Apps 交互、Tasks 长任务、权限分级、传输升级 | 5.2、5.3、5.4 |
| 可运维可落地 | 集群部署、灰度迁移、生产监控、标准错误排查 | 六、七章 |
四个思维加起来覆盖了错误码、参数校验、云原生、扩缩容、Apps、长任务、权限、传输、集群、灰度、监控这些能力点。我对照过协议文本,每一条都能在协议层面找到对应机制。
旧版原生痛点清单
旧版有状态架构的痛点,后文每一节都会回应:
痛点 1:会话状态绑死实例。 请求必须路由到持有会话的副本,集群形同虚设。对应方案:5.1 无状态化、6.4 部署架构演进。
痛点 2:握手流程冗余。 每个客户端先 initialize 再干活,能力协商、协议版本协商来回两三次。对应方案:5.1、6.2 代码改造。
痛点 3:能力碎片化。 传输、鉴权、长任务各自为政,没有统一标准。对应方案:5.2 到 5.5 的能力补齐。
痛点 4:企业落地无门。 没有统一授权、没有合规约束、无法私有化部署。对应方案:5.4 安全硬化。
三、前置认知:MCP 的定位与版本变迁
MCP 是什么
MCP(Model Context Protocol)是 AI 时代的通用通信底座,解决的是「AI 应用怎么安全地调用外部工具和数据」这个问题。它不解决 Agent 之间的对话(那是 A2A 的事),只管 Agent 和工具之间这一跳。
| 层级 | 角色 | 例子 |
|---|---|---|
| 协议层 | 定义消息格式、方法、错误 | JSON-RPC 2.0 封装,tools/call、resources/read |
| 传输层 | 定义消息怎么走 | stdio、Streamable HTTP、WebSocket |
| 调用方 | 谁发起请求 | AI 应用、编码 Agent、自动化脚本 |
我习惯把 MCP 理解成 AI 应用连接外部能力的标准插槽,和 USB-C 之于外设一个道理。
版本时间线
| 版本 | 关键变化 | 状态 |
|---|---|---|
| 2024-11-05 | MCP 开源发布,传输层为 stdio 与 HTTP with SSE | 有状态,initialize 握手为主流程 |
| 2025-03-26 | 规范更新,协议版本化 | 有状态,握手延续 |
| 2025-06-18 | 引入 Streamable HTTP 作为正式传输,HTTP+SSE 标记废弃 | 有状态,握手延续 |
| 2025-11-25 | 规范更新,握手仍是主流程 | 有状态,最后一个握手版本 |
| 2026-07-28 | 全协议无状态化,initialize 握手废弃为 legacy | 无状态,本次官宣重构 |
时间线上看得很清楚:initialize 握手在 2024-11-05 起就是主流程,2025-11-25 仍是握手版本,直到 2026-07-28 才废弃,中间所有生产问题几乎都源自这个会话绑定。
Session 握手为什么是枷锁
旧版协议流程是这样的:
① 客户端发 initialize 请求,带协议版本和能力声明。 ② 服务端返回 initialized 响应,同时建立会话。 ③ 后续请求通过 Mcp-Session-Id header 指向该会话。 ④ 服务端靠会话上下文处理请求,会话状态留在服务端。 ⑤ 连接断开后会话还要等清理。
这套机制有两个直接后果:请求必须路由到持有会话的副本(会话不在,请求直接失败);会话丢失或实例重启,所有在途请求跟着失败。分布式系统最怕隐式状态,我是在线上事故里才真正体会到的。
四、旧版有状态架构的硬伤复盘
集群部署:粘性 LB 与宕机不可用
回到开头那个案例。扩到 3 个副本,粘性 LB 把请求按会话标识哈希到固定实例。会话还留在旧副本上,新副本接不到对应请求;旧副本一宕,挂在上面的会话全部中断,40% 请求失败,其余请求打到空会话上继续报错。
故障链路完整复现一遍:扩容加副本 → 会话仍绑定旧副本 → 请求哈希到无会话的副本 → 502 或直接丢失。每一环都是协议设计造成的,不是运维失误。
协议设计:握手冗余与扩展性差
旧版每个新连接至少两次握手往返(initialize + 初始化确认),能力协商每次重复进行。更麻烦的是扩展性:想新增一种 tool 类型,旧版要动协议定义、改握手阶段的能力声明、客户端服务端同步升级。我在项目里加一个流式输出工具,光协议层改动就花了一周,还不兼容旧客户端。
反例在这里:协议一旦把能力声明绑定在握手阶段,后续每加一个能力都要动握手机制,扩展成本随能力数量线性上涨,最后碎片化。
企业落地:授权混乱与无法私有化
某次技术选型评审,安全合规部门直接问了三个问题:谁在调用这个 MCP Server?调用方有什么权限?数据经过哪些服务?旧版 MCP 一个都答不上来。没有统一授权流程,没有权限边界,会话机制还要求状态留在服务端,私有化部署意味着每套环境都要单独维护会话层。
否决项最终列出三个:授权混乱、无合规约束、无法私有化部署。采购流程当场终止。
五、五大核心架构升级
5.1 全协议无状态化
新版请求不需要任何会话上下文。一个工具调用长这样:
json
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tools/call",
"params": {
"name": "search_code",
"arguments": { "query": "TODO" }
}
}
旧版同样一个调用,多了一整套会话前置:先 initialize 握手,后续请求带上 Mcp-Session-Id header 指向会话:
json
{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tools/call",
"params": {
"name": "search_code",
"arguments": { "query": "TODO" }
}
}
makefile
Mcp-Session-Id: sess-8f3a
差异点至少三处:不再需要 initialize 握手前置;Mcp-Session-Id header 整个消失;连接建立从「握手建会话再请求」变成「直接请求」。因为请求自描述,任意副本都能独立处理,不需要知道请求来自哪条连接、之前在哪个实例。无状态是水平扩容的前提,也是这次重构的地基。
5.2 MCP Apps:补上人机交互
旧版 Agent 要收集用户输入,只能返回一段文本让用户重打一遍。新版 MCP Apps 允许 Server 返回可渲染组件,直接嵌在对话里。
json
{
"app": {
"type": "form",
"title": "代码搜索",
"fields": [
{ "name": "query", "label": "搜索关键词", "required": true },
{ "name": "scope", "label": "搜索范围", "type": "select" }
]
}
}
用户填完表单直接提交,不用在对话里手打 JSON。旧版的局限很明确:用户填错一个参数只能重发整句,交互全靠对话文本硬撑。Apps 把「对话」和「表单」两种交互合到一起,这个短板算是补上了。
5.3 Tasks:长任务转正
旧版没有长任务标准,跑十几分钟的批量任务全靠客户端硬等。新版 Tasks 扩展定义了完整生命周期:
创建任务 → 收到 taskId → 进度回调持续推送 → 中断后可续跑/恢复。
json
{
"notification": "tasks/progress",
"params": {
"taskId": "task-42",
"progress": 0.6,
"status": "running"
}
}
进度回调带上 taskId、progress、status 三个字段,客户端可以渲染进度条。连接断了也不怕,任务在服务端续跑,客户端重连后拿 taskId 恢复订阅。断点续跑和进度回调是长任务的刚需,这次从扩展转正为标准能力。
5.4 OAuth 统一授权与权限分级
企业接入第三方 MCP Server,授权流程统一成标准 OAuth 流程:
① 客户端发现 Server 的授权元数据。 ② 用户被引导到授权页,同意授权范围。 ③ 授权码换 token。 ④ 后续调用带 token 访问。 ⑤ 权限按范围校验。
权限分级至少两级:工具级(哪些工具能调)和资源级(哪些数据能读)。工具级配置示例:
json
{
"tool": "search_code",
"permission": "read",
"scope": ["repo:public"]
}
旧版的「裸连就能调」到此结束,授权、审计、吊销都有标准路径。
5.5 规范化:错误码、校验、流式传输、废弃策略
新版定义了标准错误码表,线上报错不再是一句 HTTP 500:
| 错误码 | 含义 | 处理建议 |
|---|---|---|
| -32700 | Parse error,JSON 解析失败 | 修复客户端序列化 |
| -32600 | Invalid Request,请求结构无效 | 校验请求格式 |
| -32601 | Method not found,方法不存在 | 检查方法名与版本 |
| -32602 | Invalid params,参数校验失败 | 按 schema 修正参数 |
| -32603 | Internal error,服务端内部错误 | 查服务端日志与异常捕获 |
| -32803 | Interrupted,请求被中断 | 检查是否被取消或超时 |
错误码统一后,监控可以直接按码聚合告警。传输层同步升级:流式传输支持增量返回,长结果不用等全量算完;废弃策略给每个旧字段标注版本和替代项,迁移期不会突然断兼容。
六、迁移实操:新旧版本改造清单
6.1 生产环境必改六大核心点位
对标官方 SEP 提案,迁移前逐项自检这六条:
- 移除 initialize 握手流程,请求改为直接发送。
- 删除会话的创建、存储、校验逻辑(含
Mcp-Session-Id处理)。 - 错误处理切换到标准错误码,告别裸 HTTP 状态码。
- 接入 OAuth 授权,替换裸连接。
- 传输层评估:stdio 换 Streamable HTTP 或 WebSocket。
- SDK 升级到支持 2026-07-28 规范的版本。
这几条都得动手改代码,光升级依赖不算完。
6.2 新旧代码对比:同一个工具调用
同一个 searchCodeTool,旧版实现:
js
// 旧版:依赖 session,handler 是 searchCodeTool
server.setRequestHandler("tools/call", async (req) => {
const session = sessions.get(req.sessionId); // 差异 1:会话查找
if (!session) throw new Error("session expired");
return searchCode(req.params.arguments.query);
});
新版实现:
js
// 新版:无状态自描述,handler 是 searchCodeTool
server.setRequestHandler("tools/call", async (req) => {
return searchCode(req.params.arguments.query); // 差异 3:直接执行
});
差异行至少三处:握手与初始化代码整个删除;sessionId 读写逻辑删除;请求处理从「先找会话再执行」变成「直接执行」。删掉的代码量通常是几十行起,删完感觉清爽不少。
6.3 灰度兼容:双版本共存
存量客户端不可能一夜升级。网关按请求头标记分流:带 MCP-Version: 2025-06-18 的请求走旧实例,带新版标识的走新实例,新旧并行。
回退条件提前定好:新版错误率超过 1% 时,网关把流量全部切回旧版,先止血再排查。我吃过没定回退条件的亏,那次线上出了半小时问题才手动切回来。
6.4 部署架构演进
| 维度 | 粘性会话集群 | 无状态集群 |
|---|---|---|
| 会话存储 | 实例内存,进程内 | 不需要,请求自描述 |
| LB 策略 | 粘性会话哈希 | 轮询或最小连接数 |
| 扩缩容方式 | 扩了也分摊不了会话 | 加副本即加容量 |
| 故障影响面 | 实例宕机,会话全挂 | 单点故障无感知 |
迁移路径分三步:先去会话化改造代码(6.1、6.2),再切 LB 策略为非粘性,最后逐步下线旧实例。每一步都可以独立验证、独立回退。
七、质量与稳定性
7.1 无状态架构的稳定性验证
三个场景我实际跑下来:
扩容:流量翻倍,从 10 个副本扩到 30 个,耗时约 30s,在途请求 0 中断,新请求自动分发到新副本。
重启:滚动重启全部实例,每批重启期间请求照常由存活副本处理,业务无感知。
故障:单个副本被 kill,LB 摘除后请求自动落到其他副本,错误率瞬时尖峰后归零。
有状态版本这三个场景我全踩过坑,无状态版本全部通过,差别只在会话这一层。
7.2 标准化错误体系:线上问题可定位
一次真实排查链路:
报错:工具调用返回 -32803 Interrupted。
查字段:日志里定位到 requestId: req-001,找到对应请求记录。
定位层级:错误码属于协议层中断(非业务异常),看服务端发现是客户端主动取消,业务代码没问题。
三层模型各司其职:协议层错误码、传输层连接状态、业务层工具异常。每一层都有标准字段,排查从「猜」变成「查」,这个体验变化很直接。
7.3 过渡期风险规避
迁移期最容易踩的坑,我列一下:
- 会话残留:服务端已无会话,客户端还在发
Mcp-Session-Idheader,报 400。规避:客户端清理所有会话相关代码,版本 2025-06-18 及更早的 SDK 必须升级。 - 废弃字段硬依赖:代码里还在读
initialized之类的握手字段。规避:按废弃策略表逐一替换,SDK 版本锁到 2026-07-28。 - SDK 版本适配:混合版本部署时新旧请求互相不认。规避:6.3 的网关分流兜底,等存量全部升级再收网。
八、生产级落地
8.1 统一工程化命令
项目根目录 package.json 里把常用操作统一成固定命令:
| 命令 | 作用 |
|---|---|
npm run dev |
本地开发,watch 模式启动 |
npm run build |
TypeScript 编译产物 |
npm run lint |
ESLint 修复代码风格 |
npm run test |
运行单元与集成测试 |
npm run start |
启动生产服务 |
对应 scripts 配置:
json
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.build.json",
"lint": "eslint src --fix",
"test": "vitest run",
"start": "node dist/index.js"
}
}
新成员接手,看 scripts 就知道怎么跑,不用问人。dev/build/lint/test/start 五条命令覆盖日常全部操作。
8.2 Docker 部署
适配无状态架构的 Dockerfile:
dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s \
CMD node -e "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
CMD ["node", "dist/index.js"]
多阶段构建把依赖和产物分开,镜像更小。HEALTHCHECK 探测 /health 端点,无状态服务健康检查简单直接。服务本身要捕获 SIGTERM 优雅退出:先停止接收新请求,再等存量请求处理完,最后关闭连接,容器滚动更新时请求不中断。
8.3 CI/CD:打包、发布、回滚
流水线三个阶段的固定步骤:
打包:pnpm build 产出 dist,docker build 构建镜像,推送到 registry。
发布:滚动更新 deployment,按批次替换副本,每批确认健康检查通过再继续。
回滚:发布后错误率超过 1% 阈值自动触发,kubectl rollout undo 回到上一个镜像,预期耗时 ≤ 5 分钟。
回滚得有触发条件、自动化动作和耗时目标,缺一个我都不敢合流水线。
8.4 运行期运维四件套
日志规范:每条日志带 requestId、错误码、耗时,形如 {"requestId":"req-001","code":-32803,"durationMs":230},排查时按 requestId 串起整条链路。
异常捕获:进程级兜底,未捕获异常统一记录并退出,配合 8.2 的优雅退出由编排系统拉起。
优雅退出:SIGTERM 后 30s 内完成存量请求收尾,超时强制退出,避免僵尸连接。
集群监控:核心指标 QPS、错误率、P99 延迟 三件套上告警,副本数与就绪状态进面板。这四项我都是踩过坑才补齐的。
九、生态长期变革
9.1 对开发者:轻量化接入
新版接入步骤:装 SDK → 注册工具 handler → 起服务,三步。旧版是五步:装 SDK → 实现握手 → 管理会话 → 注册 handler → 处理 session 清理。
具体到代码量,旧版一个最小服务初始化代码 60 行起(握手 + 会话管理 + 清理逻辑),新版 18 行。接入从「懂协议」变成「会调用」,这个变化对独立开发者很实在。
9.2 对架构师:规模化与高并发
无状态化直接换来了可量化的指标:单副本承载 2000 QPS,水平扩容到 50 副本线性叠加,扩容耗时从「迁移会话的 40 分钟」降到「加副本的 30 秒」。请求不绑定实例,负载均衡器怎么分发都行,高并发落地总算有了底。
9.3 对 AI 生态:MCP 与 A2A 各司其职
MCP 管的是 Agent 与工具之间:Agent 调数据库、调 API、调代码仓库,走 MCP。A2A(Agent-to-Agent)管的是 Agent 与 Agent 之间:两个 Agent 协作、交接任务,走 A2A。
两者边界清晰:MCP 无状态化之后,工具调用这一层标准化了,A2A 的每个 Agent 不用再关心底层工具怎么连,只管上层协作。MCP 把「工具连接」标准化,A2A 才有机会把「Agent 协作」标准化,这是生态发展的机制基础。
9.4 官方 Roadmap
官方后续方向(来源:MCP 官方公告与 SEP 提案):
- 传输层继续演进,WebSocket 与流式能力按规范迭代(来源:2026-07-28 规范)。
- 权限与审计深化,更细粒度的资源级管控(来源:SEP 提案路线)。
- 观测能力标准化,错误码与遥测字段持续扩展(来源:官方 changelog)。
十、总结:三个词读懂这次重构
去状态。 会话握手机制废弃,请求自描述,实例不再持有隐式状态。这是集群、扩容、上线、规模化全部问题的根源,也是本次重构最根本的一处改动。
标准化。 错误码、授权、校验、废弃策略全部统一。协议不再只是「能用」,而是能运维,线上问题从「猜」变成「查」。
可规模化。 无状态 + 标准化,水平扩容、灰度迁移、生产监控全部成为可能。架构改动只是表象,这三个词才是核心。
三个词落到一起,其实就是一件事:AI 连接里大量重复的事情,握手、会话、鉴权、错误处理、长任务、传输适配,以前每个项目都要各自写一遍,现在全部收敛成一个标准格式。我接入新服务时感受最直接,以前光对齐握手和会话就要翻一堆文档,现在打开规范,请求报文、错误码、授权流程都是同一套格式,照着写就行。
这次更新的终极目标,是让 MCP 从玩具协议变成 AI Agent 工业化基础设施。玩具可以容忍会话,基础设施不能。
你从旧版迁新版时 sessionId 残留是怎么处理的?灰度分流用的什么策略,回退条件定了多少错误率阈值?我在评论区等你分享迁移踩坑记录,也欢迎聊聊你那边踩到的坑。
附录:官方资源
官方资源汇总
- MCP 2026-07-28 规范 官方公告与协议全文,无状态化与错误码的权威定义
- 官方 SDK TypeScript SDK 仓库,升级与迁移示例
- SEP 提案 协议演进提案与讨论,灰度方案的官方依据
- Changelog 完整版本变更记录,废弃字段对照表