文章

MCP 协议新版本解析:从有状态会话到无状态协议

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,则可以压缩成三个里程碑:

  1. stdio和HTTP+SSE都有明显的连接生命周期,初始化结果通常在这段生命周期内复用。
  2. Streamable HTTP将请求与连接解耦,但保留了协议session。
  3. 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只改变了传输方式,上面的初始化流程仍然存在:

  1. Client用第一个POST发送initialize请求。此时还没有Mcp-Session-Id
  2. Server返回InitializeResult。如果Server需要维护有状态session,可以在这个HTTP响应中签发Mcp-Session-Id
  3. Client用下一个POST发送notifications/initialized。如果Server刚才签发了ID,这次POST就要带上它。
  4. 之后的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-182025-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可能同时混合三件事:

  1. 协议版本和capabilities等元数据。
  2. HTTP连接、SSE流或stdio子进程等传输生命周期。
  3. 购物车、浏览器和数据库连接等应用状态。

协议版本和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 / initializedMcp-Session-Id。原来只在握手时交换的信息,改成随请求携带:

  • JSON-RPC _meta必须携带io.modelcontextprotocol/protocolVersionio.modelcontextprotocol/clientCapabilitiesio.modelcontextprotocol/clientInfo是建议携带的可选字段。下文分别简称为protocolVersionclientCapabilitiesclientInfo
  • 使用HTTP时,还必须用MCP-Protocol-Version Header镜像协议版本,供网关等中间设施读取。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/discovercapabilities.tools是对象,表示Server支持Tools及其子能力。{}表示基本支持,{ listChanged: true }还表示支持列表变更通知
tools/listresult.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_idconnection_idsandbox_idworkflow_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/gettasks/updatetasks/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-MethodMcp-Name暴露方法与目标名称。网关无需解析JSON body,就能完成路由、审计和策略匹配。

这类信息原本藏在JSON-RPC消息中。把它们镜像到HTTP Header后,MCP请求就能直接复用现有的HTTP基础设施。

6. 总结

MCP 2026-07-28可以压缩成四个结论:

  1. MCP从面向连接的会话模型,转向面向请求的协议模型。
  2. 应用状态没有消失。业务对象用handle引用,多轮输入用requestState续办,长时间执行用task ID管理。
  3. 这些标识解除了请求与原连接的绑定,但跨实例恢复仍取决于Server的存储和恢复机制。
  4. 长连接仍然存在,只是收敛到流式响应和subscriptions/listen等明确用途。

我对这次改版的理解是:它没有消灭分布式系统的复杂性,而是不再让语义模糊的session统一承担这些复杂性。

状态被放回语义明确的对象中,Server可以分别设计鉴权、存储和生命周期。连接无状态不等于Server内部没有状态,这也是理解新版最重要的边界。

7. 参考

本文由作者按照 CC BY 4.0 进行授权