MCP 协议新版本解析:从有状态会话到无状态协议
1. MCP解决了什么问题
1.1 外部能力接入为何会碎片化
大模型刚开始调用外部系统时,每个AI应用都在重复解决同一类问题:如何描述工具和参数,如何调用数据库、GitHub或文件系统,又如何把结果送回模型。
没有统一协议时,一个AI应用接入N种外部系统,就要维护N套适配逻辑。换一个AI应用,这些接入往往还要重做。MCP最初要解决的,就是这个碎片化问题。
1.2 MCP如何统一能力模型
MCP用统一的JSON-RPC消息和能力模型,将对外能力归纳为Tools、Resources和Prompts等基本原语。
于是,工具提供方可以实现一次MCP Server,再被不同AI应用复用;AI应用也不必为每个外部系统发明一套私有接口。这是MCP到今天仍然最重要的价值。
1.3 Host、Client与Server的分工
MCP同时将这个接入过程拆成了三个角色:
- Host是用户直接使用的AI应用或Agent运行环境,例如Claude Desktop、Cursor、Cherry Studio和Claude Code。它负责管理模型、对话、用户授权和多个MCP连接。
- Client是Host内部负责讲MCP协议的组件。一个Host通常会为每个MCP Server维护一个Client连接。
- Server是对外暴露Tools、Resources和Prompts的服务。它可以是Host通过stdio拉起的本地子进程,也可以是通过HTTP访问的远程服务。
1.4 一次工具调用如何完成
一次典型调用是:用户向Host提问,Host中的模型决定调用某个工具,对应Client按MCP协议向Server发起tools/call,再把结果交还给Host。
flowchart LR
U[用户] --> H[Host<br/>模型、对话与授权]
H --> C[MCP Client<br/>协议连接]
C --> S[MCP Server<br/>Tools / Resources / Prompts]
2. session是如何进入MCP的
2.1 从版本演进中找到session主线
MCP的第一份正式规范发布于2024年11月。当时最常见的形态是,Host拉起本地MCP Server进程,通过stdio调用工具或读取资源。
这种部署很简单,进程生命周期也天然提供了交互边界。
随着MCP开始连接远程服务和企业系统,后续版本持续补齐传输、授权和复杂交互能力:
| 版本 | 主要进展 | 解决的问题 |
|---|---|---|
2024-11-05 | 建立Tools、Resources、Prompts,定义stdio和HTTP+SSE transport | 统一AI应用与外部能力的接入方式 |
2025-03-26 | 用Streamable HTTP替代旧HTTP+SSE transport,引入OAuth 2.1授权框架 | 让远程MCP更容易部署和受保护 |
2025-06-18 | 将MCP Server明确建模为OAuth Resource Server,增加结构化输出和elicitation | 补齐授权发现、可机器处理结果和人机交互 |
2025-11-25 | 实验性引入Tasks,继续完善Extensions、OAuth和elicitation | 支持持久任务和更复杂的生产场景 |
从能力上看,MCP在一步步补齐远程部署和企业场景。如果只看session,则可以压缩成三个里程碑:
- stdio和HTTP+SSE都有明显的连接生命周期,初始化结果通常在这段生命周期内复用。
- Streamable HTTP将请求与连接解耦,但保留了协议session。
2026-07-28将session移出协议核心。
第三个里程碑是后文的重点。在此之前,先沿着【进程边界→连接会话→协议session】这条线,看看session是如何一步步进入MCP的。
2.2 stdio天然具有进程生命周期
最初的MCP提供两种标准transport:stdio和HTTP+SSE。它们都传输JSON-RPC消息,但建立双向通道的方式不同。
在stdio中,Host启动本地MCP Server子进程。Client向stdin写入消息,再从stdout读取消息。进程和管道会长时存在,因此实现通常在这段生命周期内复用初始化结果和上下文。
2.3 HTTP+SSE用两条通道拼接会话
换成远程HTTP后,MCP需要复制这种双向通信能力。HTTP+SSE的做法有些反直觉:用两个单向的HTTP endpoint,组合出一条逻辑上的双工通道。
- Client通过POST endpoint向Server发消息。
- Server通过SSE endpoint向Client发消息。
sequenceDiagram
participant C as MCP Client
participant LB as Load Balancer
participant A as Server实例A
participant B as Server实例B
C->>LB: GET /sse
LB->>A: 建立SSE长连接
A-->>C: endpoint事件<br/>/messages?sessionId=abc
C->>LB: POST /messages?sessionId=abc
LB->>A: 粘性路由或session路由
Note over B: 不持有session abc的SSE连接
A-->>C: 通过原SSE连接返回JSON-RPC响应
Client先连接SSE endpoint。Server随即发送一个endpoint事件,告诉Client后续应该向哪里POST。实现中通常会把路由标识放进这个地址,用来找回对应的SSE连接。
于是,一条SSE长连接和它对应的POST endpoint,共同组成了一个逻辑会话。连接断开,或承载它的Server实例消失,这个会话就很难继续。
2.4 逻辑会话带来实例绑定
真正麻烦的地方也在这里:GET和POST必须汇合到同一个逻辑会话。如果SSE连接只保存在实例A的内存中,后续POST就必须找回实例A。
负载均衡要么做粘性路由,要么通过共享session registry或消息总线,把POST转发给持有SSE连接的实例。节点扩容、缩容或异常退出时,还要处理这些连接的归属和恢复。
所以,这套transport虽然将双工通信搬到了HTTP,却保留了很强的连接绑定。这是后续版本首先要解开的问题。
2.5 initialize在会话内保存协议状态
传输通道建好后,MCP还要完成一次协议初始化:
1
2
3
4
initialize
-> InitializeResult
-> notifications/initialized
-> tools/call、resources/read ...
initialize用来协商协议版本和capabilities。Client收到InitializeResult后,再用notifications/initialized表示准备完成。到这一步,才能进入正常的工具和资源交互。
这些都是JSON-RPC消息。stdio通过stdin / stdout传输;HTTP+SSE则由Client发到POST endpoint,Server再通过SSE返回结果。
进程或SSE连接提供的是传输通道,
initialize/initialized完成的是MCP协议初始化。
2.6 Streamable HTTP将请求与连接解耦
要解开上面的绑定,就要让请求不再依赖某一条固定的SSE连接。
先看熟悉的Web API。一个用户的多次请求,不必始终使用同一条TCP连接,也不必落在同一台Server上。
如果应用需要识别登录会话,可以用Cookie携带session ID。连接可以换,会话仍然能被找回。
2025-03-26引入的Streamable HTTP采用了类似思路。Server只需提供一个MCP endpoint,同时支持POST和GET:
- POST用于发送JSON-RPC消息。每条消息都是独立的HTTP请求,Server可以返回JSON,也可以打开SSE stream。
- GET用于建立可选的SSE stream,接收Server主动发起的消息。
启用session管理时,一个session可以对应多条SSE stream,Server也可以支持stream断线恢复。至此,连接不再等于session。
2.7 连接解耦后仍保留协议session
但Streamable HTTP只改变了传输方式,上面的初始化流程仍然存在:
- Client用第一个POST发送
initialize请求。此时还没有Mcp-Session-Id。 - Server返回
InitializeResult。如果Server需要维护有状态session,可以在这个HTTP响应中签发Mcp-Session-Id。 - Client用下一个POST发送
notifications/initialized。如果Server刚才签发了ID,这次POST就要带上它。 - 之后的
tools/call等消息继续使用独立POST。如果Server签发了ID,后续POST都要带上它。
Mcp-Session-Id很像Web开发者熟悉的Cookie:Server签发一个不透明标识,Client在后续请求中带回,Server再用它找到会话上下文。
当然,它并不是HTTP Cookie。Mcp-Session-Id没有Cookie Jar、Domain、Path和SameSite等语义,而是由MCP Client显式管理的Header。
它也是可选的。Server不需要有状态session时,可以不签发。
2025-06-18和2025-11-25继续沿用这一模型。MCP已经从“连接承载会话”走到“请求携带session标识”,但还没有真正移除协议session。连接层的问题缓解了,session本身的语义问题开始暴露出来。
3. 旧session模型的问题
Streamable HTTP解决了第一层问题:POST不再必须找回某一条固定的SSE连接。
但还有第二层问题。Server仍可能依赖Mcp-Session-Id,用它找回协商结果和应用状态。
3.1 连接解耦后仍有实例绑定
假设Server把session abc放在某个实例的内存中,后续请求就必须回到这个实例:
sequenceDiagram
participant C as MCP Client
participant LB as Load Balancer
participant S1 as Server A
participant S2 as Server B
C->>LB: initialize
LB->>S1: initialize
S1-->>C: Mcp-Session-Id = abc
C->>LB: tools/call + abc
LB->>S1: 必须回到知道abc的实例
Note over S2: 无法处理该session
负载均衡要么做sticky session,要么让所有实例共享session store。无论选哪种,扩缩容、实例下线和跨区域调度都会更复杂。
3.2 Client无法统一session生命周期
更棘手的问题不在存储,而在语义。
不同MCP Host对session生命周期的理解可能完全不同。它可能只持续一次tool call,也可能跟着IDE进程长期存在。
Server如果把浏览器实例、数据库连接或购物车绑定到session,业务对象的生命周期就被交给了Client的实现细节。
以购物车为例,一种有状态实现会在Server内部维护session abc -> basket的映射。后续调用只带session ID,不再显式说明操作哪个购物车:
1
2
3
4
5
6
initialize()
-> Mcp-Session-Id = "abc"
create_basket() # Header: Mcp-Session-Id: abc
add_item(item="book") # Header: Mcp-Session-Id: abc
checkout() # Header: Mcp-Session-Id: abc
有的Client每次调用都换session,状态活不到下一次调用;有的Client长期复用session,不同对话又可能共享了本不该共享的状态。同一个Server因此无法从session本身判断它应该存活多久。
3.3 session混合了三类状态
问题不是“有状态一定不好”,而是旧session可能同时混合三件事:
- 协议版本和capabilities等元数据。
- HTTP连接、SSE流或stdio子进程等传输生命周期。
- 购物车、浏览器和数据库连接等应用状态。
协议版本和capabilities用于解释请求,购物车、浏览器和数据库连接则是应用对象。它们的生命周期完全不同,却被塞进了同一个会话边界。
3.4 传输生命周期侵入业务生命周期
这种混合让Server很难回答一个简单问题:session结束时,里面的业务对象是否也应该销毁?
如果答案取决于Host何时断开连接,业务生命周期就被传输层反向决定了。
对本地stdio Server来说,这种绑定通常还能接受。到了远程共享服务,连接与业务对象已经不再是一对一的关系。
3.5 远程部署放大了这些问题
在网关、负载均衡和Serverless环境中,请求可能落到不同实例,实例也会随时扩缩容或退出。如果session同时保存协议上下文和应用状态,调度、恢复和清理都会变得麻烦。
因此,新版要解决的不只是“请求能否切换连接”,而是“不同状态是否拥有各自明确的边界”。
4. 2026-07-28如何拆解session
4.1 从隐式session转向自包含请求
2026-07-28的回答很直接:删除强制的initialize / initialized握手和Mcp-Session-Id,将协议核心改为stateless-first。
这不是给旧协议增加一个可选模式。新版要求每个请求携带解释该请求所需的协议元数据。Server不能依赖同一连接上的历史请求推断协议版本或Client capabilities。
先从整体上看一遍新版本的关键变化:
| 层次 | 主要变化 | 解决的问题 |
|---|---|---|
| 协议核心 | 删除握手和协议级session | 请求在协议层面不再绑定原连接 |
| 能力发现 | 新增server/discover | 按需获取版本和Server capabilities |
| 多轮交互 | 引入Multi Round-Trip Request(MRTR) | 无需依赖持久连接便可补充输入 |
| 长任务 | Tasks进入官方Extension | 用task ID查询延迟结果 |
| 能力演进 | Extensions Framework | 复杂能力不再全部塞进协议核心 |
| 远程部署 | 路由Header和OAuth发现加强 | 让MCP更好地融入标准HTTP基础设施 |
这些变化最终都指向同一个设计选择:
2026-07-28消除的是【协议层隐式session】,不是所有应用状态。请求不再依赖原连接;状态能否跨实例恢复,仍取决于Server如何实现和存储它。
4.2 session的职责被拆给不同机制
旧session承担的职责被拆到了不同机制中:
| 旧session承载的内容 | 新版的处理方式 |
|---|---|
| 协议版本、Client信息和capabilities | 每个请求的_meta |
| 购物车、浏览器等业务对象 | 应用自行设计的显式handle |
| 一次调用中途补充输入 | MRTR与requestState |
| 长时间执行 | Tasks Extension与task ID |
| 长期通知 | subscriptions/listen |
下面按这条对应关系逐层展开:先拆协议状态,再拆应用状态,最后处理多轮请求、长任务和长期通知。
4.3 协议元数据随每个请求携带
新版删除initialize / initialized和Mcp-Session-Id。原来只在握手时交换的信息,改成随请求携带:
- JSON-RPC
_meta必须携带io.modelcontextprotocol/protocolVersion和io.modelcontextprotocol/clientCapabilities。io.modelcontextprotocol/clientInfo是建议携带的可选字段。下文分别简称为protocolVersion、clientCapabilities和clientInfo。 - 使用HTTP时,还必须用
MCP-Protocol-VersionHeader镜像协议版本,供网关等中间设施读取。Header与body不一致时,Server必须拒绝请求。
这一步解除了协议状态对历史请求的依赖。下一个问题是:Client如何知道Server支持哪些版本和能力?
4.4 server/discover按需发现能力
Server的版本和capabilities可通过server/discover按需发现。
server/discover回答的是“这个Server支持哪些版本和能力”,不会返回具体工具。工具列表仍然由tools/list回答。
Client如果已经知道目标能力,不必先discover,可以直接发起RPC。
只看result中的关键字段,两个响应的区别很直观:
1
2
3
4
5
6
7
8
9
10
server/discover
→ { resultType: "complete",
supportedVersions: ["2026-07-28"],
capabilities: { tools: { listChanged: true } },
_meta: { "io.modelcontextprotocol/serverInfo":
{ name: "shop", version: "1.0.0" } } }
tools/list
→ { resultType: "complete",
tools: [{ name: "checkout", inputSchema: { ... } }] }
4.5 能力声明不等于工具列表
这里的两个tools很容易混淆,可以直接对照理解:
| 字段 | 含义 |
|---|---|
server/discover 的capabilities.tools | 是对象,表示Server支持Tools及其子能力。{}表示基本支持,{ listChanged: true }还表示支持列表变更通知 |
tools/list 的result.tools | 是数组,包含具体的工具定义 |
server/discover告诉Client“能不能用Tools”,tools/list才告诉Client“具体有哪些Tools”。把能力协商与对象列表分开后,Client可以按需获取更具体的信息。
4.6 discover不会建立会话
server/discover只是按需发现能力,不会建立会话。后续请求仍然自带协议元数据,因此不必回到完成discover的连接或实例:
sequenceDiagram
participant C as MCP Client
participant LB as Load Balancer
participant S1 as Server A
participant S2 as Server B
C->>LB: tools/call + 每请求元数据
LB->>S1: 按普通请求路由
S1-->>C: result
C->>LB: tools/call + 每请求元数据
LB->>S2: 按普通请求路由
S2-->>C: result
这里解除的是【协议状态】对连接的依赖。如果一次工具调用还要访问购物车或浏览器实例,能否切换Server实例,要看这些应用状态如何存储。下一层接着处理这个问题。
4.7 handle让应用状态显式化
handle可以理解为【句柄】。它不是状态本身,而是引用某个持续对象的不透明标识。调用方只保存并传回这个标识,不依赖它的内部格式。
可以类比Linux文件描述符。open返回一个整数fd,后续读取和关闭文件时都传回它:
1
2
3
open("orders.csv") -> fd = 3
read(fd = 3)
close(fd = 3)
fd = 3不包含文件内容。操作系统会用它找到当前进程打开的文件对象。
MCP中的handle也是类似的引用。不过要先说清楚:handle不是MCP新增的标准字段、数据类型或协议对象。它只是一种工具设计模式,Server通过普通工具结果返回ID,Client再通过普通工具参数将其带回。
协议无状态不等于应用没有状态。还是上面的购物车,新版可以用显式handle引用它:
1
2
3
4
5
create_basket()
-> basket_id = "bsk_123"
add_item(basket_id="bsk_123", item="book")
checkout(basket_id="bsk_123")
basket_id是普通的工具结果和参数,不是隐藏在传输层的session ID。同样的模式还可以用于browser_id、connection_id、sandbox_id或workflow_id。
4.8 handle不会替Server管理状态
handle只是让状态依赖显式化,并不会自动消除Server端状态。Server仍要把bsk_123映射到购物车对象。
这个映射可以放在数据库或Redis中,也可以编码进一个可验证的自包含token。如果只存在某个实例的内存里,请求仍然不能随意切换实例。
显式化之后,Server要定义handle的生命周期:什么时候创建,多久过期,如何销毁,丢失后如何恢复。Client也要在上下文压缩后,保留仍然活跃的handle。
另一个容易误解的点是,handle不是权限。在已认证系统中,Server每次都应检查(handle, auth_context)。请求者知道一个ID,不代表它有权访问对应对象。
4.9 MRTR续接一次逻辑请求
显式handle解决了业务对象的引用问题,但有些调用无法一次完成。Server可能执行到一半,需要Client补充输入。
一次请求发出并收到响应,可以看作一个round trip。MRTR即【多轮往返请求】,用于同一次逻辑操作需要多轮请求和响应才能完成的场景。
例如,Client请求删除一批文件,Server执行前需要用户确认。Server先返回InputRequiredResult,里面包含待补充的inputRequests和不透明的requestState。
Client收集用户回答后,重新发起原请求。新请求带上inputResponses,并原样传回requestState。
requestState保存的是这次逻辑请求的恢复信息。Client只负责传回,不能解析或修改。
它更像一次工作流的【续办凭证】:Server返回,Client在重试原请求时原样交回。它只服务于当前逻辑请求,不能拿去恢复其他请求。
MRTR的基本工作流要求重试请求可以独立处理。Server可以把恢复信息编码进受完整性保护的requestState。如果实现还要查询外部状态,该状态就必须能被处理重试的实例访问。
协议解除了连接绑定,但不会替Server建设共享存储。
至此,可以把三类引用放在一起对比:
| 机制 | 持续范围 | 主要用途 |
|---|---|---|
| 业务handle | 多次独立工具调用 | 引用浏览器、购物车等应用对象 |
MRTR requestState | 一次逻辑请求的多个round trip | 等待用户确认或Client补充输入 |
| task ID | 跨时间和连接 | 管理长时间执行的工作 |
4.10 Tasks承载长时间执行
工具调用如果需要运行几分钟甚至几小时,一直占着HTTP连接等结果就不太合适。Tasks Extension允许Server把一次请求建模为可跨请求访问的Task,先返回task ID。
stateDiagram-v2
[*] --> working
working --> input_required: 需要补充输入
input_required --> working: tasks/update
working --> completed
working --> failed
working --> cancelled: tasks/cancel
input_required --> cancelled: tasks/cancel
Client用tasks/get获取状态和最终结果,用tasks/update补充输入,也可以用tasks/cancel尝试取消。Task还会给出ttlMs和建议轮询间隔pollIntervalMs。
是否创建Task由Server决定。Client声明自己支持Tasks Extension,Server再根据执行方式返回直接结果或task ID。
规范把Task定义为durable state machine,并要求返回ID前,tasks/get已经能查到它。但durable不等于永久保存:Task仍可受TTL约束,具体存储和重启恢复能力由Server实现并兑现。
MCP Task容易让人联想到A2A Task,但两者不在同一个协议层次。A2A Task是Agent对外承接委派和协作的工作单元,可以积累Message并产出Artifact;MCP Task只承载某次MCP请求的延迟执行。它们可以嵌套:Agent通过A2A委派工作,执行方内部再用MCP Task运行耗时工具。
4.11 subscriptions/listen承载长期通知
无状态不等于没有长连接。新版仍然支持流式响应和长期通知,subscriptions/listen就是一个显式的长期请求。
HTTP Client用POST发起listen,Server通过这次请求的SSE response stream发送通知。连接断开后,Client重新发起listen。
长连接现在只服务于“持续收通知”这个明确需求,不再顺便承载能力协商、业务对象和任务恢复。
5. 无状态改造之外的工程化变化
2026-07-28还有Extensions、授权发现和HTTP路由信息等变化。它们与session改造不是同一条主线,这里只保留必要背景。
5.1 Extensions让能力独立演进
Extensions Framework可以理解为【能力登记和协商机制】。它不是新的HTTP Header,也不是另一套传输协议。
每个Extension有自己的ID。Client和Server通过extensions映射声明支持的能力:
1
2
3
4
5
{
"extensions": {
"io.modelcontextprotocol/tasks": {}
}
}
空对象表示“支持该Extension,没有额外配置”。这个映射只负责协商,不定义具体行为。
以Tasks为例,双方先确认支持io.modelcontextprotocol/tasks,再使用tasks/get、tasks/update和tasks/cancel等JSON-RPC方法交互。SDK仍然需要实现这些行为。
Extension可以独立演进。关于兼容性,记住两点即可:
- Framework没有统一的版本字段。具体Extension可以在settings中定义版本,无法兼容的大改则使用新ID。
- 对方不支持时,实现要么回退到核心协议,要么明确拒绝请求。
5.2 OAuth沿用标准发现机制
远程MCP继续完善了OAuth Protected Resource发现。Server可以在401响应的WWW-Authenticate中给出RFC 9728元数据地址,Client据此找到Authorization Server和所需scope。
这一流程沿用HTTP和OAuth的challenge-response模型,与stateless改造相互独立。MCP只是把远程Server如何声明授权入口这件事标准化,本文不再展开。
5.3 路由信息对网关可见
HTTP请求可以用Mcp-Method和Mcp-Name暴露方法与目标名称。网关无需解析JSON body,就能完成路由、审计和策略匹配。
这类信息原本藏在JSON-RPC消息中。把它们镜像到HTTP Header后,MCP请求就能直接复用现有的HTTP基础设施。
6. 总结
MCP 2026-07-28可以压缩成四个结论:
- MCP从面向连接的会话模型,转向面向请求的协议模型。
- 应用状态没有消失。业务对象用handle引用,多轮输入用
requestState续办,长时间执行用task ID管理。 - 这些标识解除了请求与原连接的绑定,但跨实例恢复仍取决于Server的存储和恢复机制。
- 长连接仍然存在,只是收敛到流式响应和
subscriptions/listen等明确用途。
我对这次改版的理解是:它没有消灭分布式系统的复杂性,而是不再让语义模糊的session统一承担这些复杂性。
状态被放回语义明确的对象中,Server可以分别设计鉴权、存储和生命周期。连接无状态不等于Server内部没有状态,这也是理解新版最重要的边界。
7. 参考
- MCP
2026-07-28Specification - MCP
2024-11-05Transports - MCP
2025-03-26Transports - MCP
2025-03-26Key Changes - MCP
2025-06-18Key Changes - MCP
2025-11-25Key Changes - SEP-2575: Make MCP Stateless
- SEP-2567: Sessionless MCP via Explicit State Handles
- SEP-2663: Tasks Extension
- MCP Authorization
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 9110: HTTP Semantics
- A2A Protocol