MCP传输协议演进:从SSE到Streamable HTTP
前两天翻MCP的官方规范文档,翻到传输协议那一页的时候,我注意到一个细节。MCP把之前用的HTTP+SSE传输方式标记成了deprecated,就是「已弃用」。取而代之的,是一个叫Streamable HTTP的新协议。
我当时就愣了一下。
MCP这个协议从2024年底发布到现在,满打满算也就一年多,传输协议就换了一版。这速度说实话有点超出我预期。要知道很多网络协议用了十几年都没动过,MCP倒好,一年不到就把自己的传输层给推翻重写了。
但仔细想想又不意外。旧的HTTP+SSE方案,确实有一堆让人头疼的问题。
所以今天想跟大家聊聊这个事。从SSE这个协议本身聊起,然后说MCP是怎么用SSE的,再说为什么MCP又把SSE给抛弃了,最后看看新的Streamable HTTP到底是个什么东西。如果你完全不了解什么传输协议,没关系,我从头给你讲。
一. MCP是什么
先说MCP是干嘛的。MCP的全称是Model Context Protocol,翻译过来就是模型上下文协议。它解决的核心问题是,让大语言模型能够跟外部工具和数据源进行交互。你让Claude帮你查个数据库,让GPT调一下你公司的内部API,这些操作背后都需要一个标准化的通信协议。MCP就是干这个的。很多朋友可能不知道,MCP现在已经不算小众了,Anthropic在推,微软也在接,越来越多的AI应用开始接入这个协议。
既然要通信,就得有个传输方式。这就像两个人要交流,你们得先决定是打电话还是面对面聊。MCP在传输方式上,经历了三代演进。
二. 传输协议三代演进
flowchart LR
A["第一代<br/>stdio<br/>本地进程通信"] -->|需要远程能力| B["第二代<br/>HTTP+SSE<br/>双endpoint"]
B -->|解决工程痛点| C["第三代<br/>Streamable HTTP<br/>单endpoint 动态响应"]
style A fill:#e8f5e9
style B fill:#fff3e0
style C fill:#e3f2fd
第一代叫stdio。这个最简单粗暴,MCP客户端直接把MCP服务器当成一个子进程启动起来,然后通过标准输入和标准输出,也就是stdin和stdout来交换数据。怎么说呢,就像你跟同事坐在同一个办公室里,直接面对面说话,不需要任何中间媒介。这个方案有个明显的好处,就是快。数据不走网络,不走HTTP,就在本地进程之间传,延迟几乎为零。但问题也很明显,它只能在同一台机器上用。你的AI助手如果需要调一个远程服务器上的工具,stdio就完全无能为力了。
所以MCP需要一种能走网络的传输方案。
这就是第二代,HTTP+SSE。但在聊MCP怎么用SSE之前,得先把SSE本身给你讲明白。
三. SSE协议详解
SSE全称是Server-Sent Events,翻译过来叫服务器推送事件,是一种基于HTTP的服务器推送技术,允许服务器通过一个持久的HTTP连接,单向地向客户端推送数据。用一句话概括就是,客户端发起请求,服务器保持连接不断开,持续往回推数据。
它的工作方式是这样的,客户端先向服务器发一个普通的HTTP GET请求,但是告诉服务器「我要的是事件流」。服务器收到之后,不关闭连接,而是保持它一直开着,然后源源不断地往这个连接里写数据。你想想看,这就像你打了一个客服电话,接通之后对面就一直不挂,有事就跟你念叨一句,没事就安静等着。
注意,这个通道是单向的。只有服务器能往客户端推数据,客户端不能通过这个连接往服务器发东西。
3.1 SSE vs WebSocket vs 轮询
可能有小伙伴会问,那跟WebSocket有什么区别?WebSocket是双向的,双方都能随时说话,听起来不是更厉害吗?来看看这个对比。
| 特性 | SSE | WebSocket | HTTP轮询 |
|---|---|---|---|
| 通信方向 | 单向(服务器→客户端) | 双向 | 单向(客户端→服务器) |
| 协议 | 标准HTTP | 独立协议 (ws://) | 标准HTTP |
| 自动重连 | 内置支持 | 需手动实现 | 不适用 |
| 断点续传 | 内置 (Last-Event-ID) | 需手动实现 | 不适用 |
| 基础设施兼容性 | 优秀(普通HTTP) | 一般(需代理支持) | 优秀 |
| 复杂度 | 低 | 中 | 低 |
| 适用场景 | 实时通知、数据流 | 聊天、游戏 | 简单轮询 |
确实,WebSocket功能更强。但它也有代价,它用的是一套独立的协议,不是标准HTTP,这意味着它跟现有的网络基础设施,比如代理服务器、CDN、防火墙,不一定能很好地配合。SSE就不一样了,它从头到尾都是标准HTTP,什么网络环境都能跑,什么代理都能穿透。
而且SSE还有一个杀手级的功能,内置自动重连。如果连接断了,浏览器会自动帮你重新连接,并且告诉服务器「我上次收到这里了,从这之后继续给我推」。WebSocket想实现这个功能得你自己写逻辑。SSE的客户端,也就是浏览器里的EventSource,天然就帮你搞定了。
3.2 建立连接
SSE的连接建立过程,说到底就是一个普通的HTTP GET请求,只不过服务器返回的Content-Type是text/event-stream,并且不关闭连接,持续往响应体里写数据。
sequenceDiagram
participant C as Client
participant S as Server
C->>S: GET /events<br/>Accept: text/event-stream
S-->>C: 200 OK<br/>Content-Type: text/event-stream<br/>Cache-Control: no-cache<br/>Connection: keep-alive
loop 连接保持
S-->>C: data: hello\n\n
S-->>C: data: world\n\n
S-->>C: ...持续推送...
end
这里有几个关键的HTTP头需要知道。客户端发请求的时候带Accept: text/event-stream告诉服务器「我要SSE流」,服务器回应的时候必须带Content-Type: text/event-stream声明这是事件流,同时还会带上Cache-Control: no-cache禁止缓存,Connection: keep-alive保持TCP连接。如果客户端断线重连,会带上Last-Event-ID头告诉服务器从哪里继续。
3.3 数据格式
SSE的数据格式也特别简单,就是纯文本,UTF-8编码。每条消息用双换行符隔开,每行是「字段名: 值」的格式。
1 | event: user_login |
核心字段就一个,data,就是消息内容,多个连续的data行会用换行符拼接。除此之外还有几个可选的,event用来标记事件类型,省略时触发通用的message事件,id用来给事件编号方便断线续传,retry用来告诉客户端断线后等多少毫秒再重连。以冒号:开头的行是注释,客户端会忽略,常用来做心跳保活。整个协议就这些东西,极其轻量。
回到MCP。MCP为什么需要SSE呢?因为MCP的工具调用场景有些特殊。你调一个查天气的工具可能秒回,但如果你调一个搜索网页的工具呢?可能要跑十几秒甚至更久。在这个过程中,服务器需要持续推送进度通知,告诉你「搜了30%了」「搜了60%了」。而且MCP基于JSON-RPC协议,JSON-RPC有个特点,服务器也可以主动给客户端发请求,比如弹个确认框让你授权某个操作。普通的HTTP请求-响应模式,一个请求只能有一个响应,响应完了连接就断了,根本搞不定这种场景。所以MCP需要一条常驻的推送通道,让服务器能随时往客户端推消息。
SSE正好能干这个事。
四. MCP如何使用SSE
那MCP具体是怎么用SSE的呢。它搞了一套双endpoint架构。一个/sse,客户端用GET请求连上去,建立一条SSE长连接,服务器通过这条连接给客户端推消息。另一个/sse/messages,客户端用POST请求往服务器发消息。一个负责收,一个负责发。
sequenceDiagram
participant C as MCP客户端
participant S as MCP服务器
Note over C,S: 两个独立endpoint 两条通信通道
C->>S: POST /sse/messages 发送请求
C->>S: POST /sse/messages 发送请求
C->>S: GET /sse 建立SSE长连接
Note right of S: 此连接必须一直保持不断
S-->>C: SSE 推送响应1
S-->>C: SSE 推送响应2
Note over C,S: 连接断开 = 全部通信中断 无法恢复
听着还行对吧。
但你仔细想一下,这里面有个很反直觉的地方。客户端通过POST发了一个请求,响应为什么不直接在HTTP响应里返回,而是要绕到另一条SSE连接上推回来?
我跟你说,这就是双endpoint架构最别扭的地方。因为SSE连接是服务器往客户端推的唯一通道。如果服务器直接在POST的HTTP响应里返回结果,那只有这一个请求的结果能回去。但如果服务器还需要推送进度通知呢?如果服务器中途需要主动问你「要不要继续执行」呢?这些都是POST的HTTP响应做不到的,HTTP请求-响应一次就完了。所以MCP把所有从服务器发往客户端的东西,不管是正常的结果、进度通知还是服务器的主动请求,全部走SSE通道推。
这就导致了一个很别扭的后果,客户端发了一个POST请求之后,得去另一条SSE连接上等响应。两条通道之间要做关联,你说复杂不复杂。
来,我给你画一下完整的交互流程。
sequenceDiagram
participant C as MCP客户端
participant S as MCP服务器
Note over C,S: 1. 建立SSE长连接
C->>S: GET /sse
S-->>C: event: endpoint<br/>data: /messages?sessionId=abc
Note over C,S: 2. 初始化握手
C->>S: POST /messages initialize
S-->>C: SSE推送 InitializeResult
Note over C,S: 3. 调用工具
C->>S: POST /messages tools/call
S-->>C: SSE推送 进度50%
S-->>C: SSE推送 最终结果
Note over C,S: SSE连接断开 = 所有通信中断
客户端连上/sse之后,服务器先推一个endpoint事件过来,告诉客户端「你往这个地址POST消息」。这个地址里会带上一个sessionId,用来标识你是谁。然后客户端开始发initialize请求,服务器通过SSE推回初始化结果。接着客户端发tools/call请求调用工具,服务器通过SSE推进度、推结果。全程,SSE连接不能断。一断,啥都没了。
这块我第一次读规范的时候,说实话来来回回看了三遍才理顺。
然后在实际工程里面,这套方案暴露出了一大堆问题。
五. SSE方案的五大问题
- 那个SSE连接必须是持久连接。客户端连上
/sse之后,这个连接就不能断了,得一直挂着。你想想看,如果有一万个客户端同时连接,服务器就得同时维护一万个长连接。每个连接都要占内存,占文件描述符,占TCP资源。对服务器来说这是一个巨大的负担。 - 两个endpoint的设计增加了复杂度。你得在服务端维护两套路由,在客户端管理两个通信通道。而且这两个通道之间还得做状态同步,就是我刚才说的那种,POST发了请求,得去SSE连接上等响应的别扭逻辑。
- 没法断线重连。虽然SSE协议本身有
Last-Event-ID支持断线续传,但在MCP的HTTP+SSE方案里,如果SSE连接断了,客户端跟服务器之间的整个通信通道就断了,不仅仅是丢了几条消息那么简单。没有机制让你从断开的地方接着来 = = 在网络环境不好的情况下,这简直是个灾难。。。 - 跟现代云原生基础设施不兼容。你想在AWS Lambda或者Cloudflare Workers这种无服务器环境里部署MCP服务器???抱歉,SSE要求持久连接,而无服务器的核心设计理念就是请求来了就处理,处理完就销毁,根本不支持维持长连接。这就直接把一大类部署场景给堵死了。
- 安全方面有隐患。旧的SSE方案在传递认证信息的时候,有时候不得不把token塞在URL的查询参数里。但凡对安全有点了解的人都知道,URL里的参数会出现在浏览器历史记录里,会出现在服务器日志里,会出现在代理服务器的缓存里。把认证令牌放在这种地方,这不是一个好习惯。这里其实有个背景,浏览器原生的EventSource API太简陋了,构造函数只接受一个URL,没地方传自定义Header,所以只能在URL里塞token。你用Python的httpx发SSE请求当然想加什么Header都行,但MCP的客户端有很多跑在浏览器环境里,这个API限制就成了实际的痛点。
反正我觉得,一个好的协议设计就像一个好的API设计,不是看它能做什么,而是看它不能做什么的时候有多优雅。旧的HTTP+SSE方案在理想情况下工作得还行,但一旦遇到非理想情况,各种短板就暴露无遗了。
所以MCP团队重新设计了传输层。
六. Streamable HTTP来了
先说这个名字,Streamable HTTP,直译过来就是「可流式传输的HTTP」。这个名字起得其实挺精准的,因为它说到底还是HTTP,但加了流式传输的能力。
它最核心的改变是把两个endpoint合并成了一个。就一个,/mcp。客户端所有的请求都往这一个地址发,不再需要维护两个通信通道了。这块我觉得是整个新协议设计里最漂亮的一笔。
然后是最精妙的部分。客户端发POST请求的时候,会在HTTP头里带上一个Accept字段,声明自己能接受两种格式的响应,一种是普通的JSON,另一种是SSE流。服务器看情况决定用哪种方式回应你。如果是一个简单的查询结果,直接返回JSON就行,快速干净。如果是一个需要持续推送的流式响应,就升级成SSE流,保持连接不断。
你想想看,这里面的区别有多大。
服务器不再是被迫维持长连接,而是可以自由选择。简单的请求用完即走,复杂的请求才保持连接。这就像你跟同事沟通,简单的事发个微信消息就搞定了,复杂的事才需要开个语音电话慢慢聊。两种方式都很自然,什么时候用哪种,看情况。
对于无服务器环境来说,这个设计简直是救星。如果你不需要服务器主动推送消息,你可以把MCP服务器设计成完全无状态的。每个请求来了就处理,处理完就返回,不需要维护任何连接状态。AWS Lambda和Cloudflare Workers终于可以愉快地跑MCP了。
再说说会话管理。Streamable HTTP引入了一个叫Mcp-Session-Id的机制。服务器在第一次跟客户端完成握手之后,会给客户端分配一个会话ID。之后客户端每次发请求,都会在HTTP头里带上这个ID,服务器就知道你是谁了。但关键是,这个会话ID是可选的。如果你的服务器不需要状态,完全可以不分配。这就是「可选项」的优雅之处,它不强制你用,但你需要的时候它就在那里。
还有一个很贴心的设计是断线重连。如果SSE流断了,客户端可以在下次请求的时候带上一个Last-Event-ID的头,告诉服务器「我上次收到到这里了,从这之后继续给我推」。服务器就能从断开的地方接着来。再也不用担心网络抖动丢消息了。
会话终止也设计得干净利落。客户端不想聊了,直接发一个DELETE请求过去,服务器收到就知道这个会话结束了。或者服务器觉得这个会话太久了,也可以主动返回一个404状态码,客户端就知道该重新握手了。没有模糊地带。
安全方面也改进了不少。认证信息现在走的是标准的Authorization头,不再需要塞在URL参数里。跟CORS兼容,能正常通过WAF和API网关,跟现有的网络安全基础设施完全对接上了。
说完设计理念,跟大家聊聊实际的交互流程。我第一次读这段规范的时候,说实话来来回回看了三遍才理顺,我用大白话给你捋一遍。
七. Streamable HTTP交互流程
sequenceDiagram
participant C as MCP客户端
participant S as MCP服务器
Note over C,S: 1. 握手阶段
C->>S: POST /mcp initialize
S-->>C: InitializeResult + Mcp-Session-Id
C->>S: POST /mcp notifications/initialized
Note over C,S: 2. 正常业务通信
C->>+S: POST /mcp tools/call
Note right of C: Accept: json, text/event-stream
Note right of C: Mcp-Session-Id: xxx
S-->>-C: JSON 直接返回 或 SSE 流式推送
Note over C,S: 3. 服务器主动推送 (可选)
C->>S: GET /mcp Accept: text/event-stream
S-->>C: SSE 服务器主动通知
Note over C,S: 4. 会话终止
C->>S: DELETE /mcp
S-->>C: 200 OK 会话关闭
先是握手。客户端往/mcp发一个POST请求,请求体里装的是一个JSON-RPC的initialize方法调用,就是跟服务器打招呼说「我是谁,我支持哪些功能」。服务器收到之后返回一个InitializeResult,告诉客户端「我是谁,我支持哪些功能」,同时在HTTP响应头里附上Mcp-Session-Id,把会话ID给你。然后客户端再发一个notifications/initialized通知,相当于跟服务器说「好了,我准备好了,正式开干」。到这一步,握手完成。
然后就是正常的业务通信了。客户端需要调用工具就发POST,每次都带着会话ID。服务器这边呢,看情况给你响应。简单的就一个JSON直接返回,复杂的就开一个SSE流慢慢给你推。坦率的讲,这种动态选择响应格式的设计,在协议层面确实很少见,至少我做协议研究这么久,没怎么见过这么灵活的方案。
如果客户端需要接收服务器主动推送的消息,比如服务端的状态变更通知,客户端可以发一个GET请求到/mcp,在Accept头里声明自己要接收SSE流。服务器就会保持这个连接,有消息就推过来。这块需要注意一下,这个GET请求是可选的,不是每个客户端都需要这么干。
最后当会话结束的时候,不管是客户端主动发DELETE还是服务器返回404,双方都会清理资源,干干净净。
对比一下旧的HTTP+SSE方案,你会立刻感受到差距。旧方案是「必须保持长连接,必须有两个endpoint,断线了就得重来」。Streamable HTTP是「可以长连接也可以短连接,只需要一个endpoint,断线了可以续上」。一个被动僵化,一个灵活从容。你如果关注这个领域的话,应该能感受到这个差距有多大。
八. 写在最后
我有时候觉得协议设计这件事,跟城市交通规划特别像。
旧的HTTP+SSE就像是一个城市只有两条路,一条只准往南走,一条只准往北走,而且这两条路必须同时通车,任何一条堵了整个交通就瘫痪了。Streamable HTTP就像是一条多车道的高速公路,简单的小车,就是JSON请求,走快车道快速通过,大货车,就是SSE流式传输,走慢车道慢慢跑,各有各的节奏,互不干扰。
回到协议本身,我觉得MCP传输协议的这三代演进还挺让人感慨的。从stdio的简单直接,到HTTP+SSE的勉强能用,再到Streamable HTTP的灵活优雅。每一代的变化都不是为了变而变,而是因为遇到了真实的工程痛点。
其实吧,计算机领域有一个永恒的规律。所有伟大的协议和系统,都是在不断的自我推翻中进化出来的。HTTP从1.0到1.1到2.0到3.0,每一次都是对上一代的反思和重构。TCP/IP当年也是在跟OSI七层模型的竞争中走出来的。没有哪个设计是一步到位的。
Streamable HTTP让我看到了MCP团队的务实。他们没有因为SSE是自己选的第一版方案就死撑着不改,而是在发现问题的第一时间就承认问题,重新设计。这种态度,我觉得比协议本身更值得尊敬。




