MCP传输协议演进:从SSE到Streamable HTTP

张开发
2026/4/13 6:44:24 15 分钟阅读

分享文章

MCP传输协议演进:从SSE到Streamable HTTP
前两天翻MCP的官方规范文档翻到传输协议那一页的时候我注意到一个细节。MCP把之前用的HTTPSSE传输方式标记成了deprecated就是「已弃用」。取而代之的是一个叫Streamable HTTP的新协议。我当时就愣了一下。MCP这个协议从2024年底发布到现在满打满算也就一年多传输协议就换了一版。这速度说实话有点超出我预期。要知道很多网络协议用了十几年都没动过MCP倒好一年不到就把自己的传输层给推翻重写了。但仔细想想又不意外。旧的HTTPSSE方案确实有一堆让人头疼的问题。所以今天想跟大家聊聊这个事。从SSE这个协议本身聊起然后说MCP是怎么用SSE的再说为什么MCP又把SSE给抛弃了最后看看新的Streamable HTTP到底是个什么东西。如果你完全不了解什么传输协议没关系我从头给你讲。一. MCP是什么先说MCP是干嘛的。MCP的全称是Model Context Protocol翻译过来就是模型上下文协议。它解决的核心问题是让大语言模型能够跟外部工具和数据源进行交互。你让Claude帮你查个数据库让GPT调一下你公司的内部API这些操作背后都需要一个标准化的通信协议。MCP就是干这个的。很多朋友可能不知道MCP现在已经不算小众了Anthropic在推微软也在接越来越多的AI应用开始接入这个协议。既然要通信就得有个传输方式。这就像两个人要交流你们得先决定是打电话还是面对面聊。MCP在传输方式上经历了三代演进。二. 传输协议三代演进需要远程能力解决工程痛点第一代stdio本地进程通信第二代HTTPSSE双endpoint第三代Streamable HTTP单endpoint 动态响应第一代叫stdio。这个最简单粗暴MCP客户端直接把MCP服务器当成一个子进程启动起来然后通过标准输入和标准输出也就是stdin和stdout来交换数据。怎么说呢就像你跟同事坐在同一个办公室里直接面对面说话不需要任何中间媒介。这个方案有个明显的好处就是快。数据不走网络不走HTTP就在本地进程之间传延迟几乎为零。但问题也很明显它只能在同一台机器上用。你的AI助手如果需要调一个远程服务器上的工具stdio就完全无能为力了。所以MCP需要一种能走网络的传输方案。这就是第二代HTTPSSE。但在聊MCP怎么用SSE之前得先把SSE本身给你讲明白。三. SSE协议详解SSE全称是Server-Sent Events翻译过来叫服务器推送事件是一种基于HTTP的服务器推送技术允许服务器通过一个持久的HTTP连接单向地向客户端推送数据。用一句话概括就是客户端发起请求服务器保持连接不断开持续往回推数据。它的工作方式是这样的客户端先向服务器发一个普通的HTTP GET请求但是告诉服务器「我要的是事件流」。服务器收到之后不关闭连接而是保持它一直开着然后源源不断地往这个连接里写数据。你想想看这就像你打了一个客服电话接通之后对面就一直不挂有事就跟你念叨一句没事就安静等着。注意这个通道是单向的。只有服务器能往客户端推数据客户端不能通过这个连接往服务器发东西。3.1 SSE vs WebSocket vs 轮询可能有小伙伴会问那跟WebSocket有什么区别WebSocket是双向的双方都能随时说话听起来不是更厉害吗来看看这个对比。特性SSEWebSocketHTTP轮询通信方向单向服务器→客户端双向单向客户端→服务器协议标准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并且不关闭连接持续往响应体里写数据。ServerClientServerClientloop[连接保持]GET /eventsAccept: text/event-stream200 OKContent-Type: text/event-streamCache-Control: no-cacheConnection: keep-alivedata: hello\n\ndata: world\n\n...持续推送...这里有几个关键的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编码。每条消息用双换行符隔开每行是「字段名: 值」的格式。event: user_login id: 42 retry: 5000 data: {user: alice, time: 2026-04-11T10:00:00Z} data: 这是一条简单消息 : 这是注释以冒号开头客户端会忽略 event: notification id: 43 data: 第一行数据 data: 第二行数据会用换行符拼接核心字段就一个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请求往服务器发消息。一个负责收一个负责发。MCP服务器MCP客户端MCP服务器MCP客户端两个独立endpoint 两条通信通道此连接必须一直保持不断连接断开 全部通信中断 无法恢复POST /sse/messages 发送请求POST /sse/messages 发送请求GET /sse 建立SSE长连接SSE 推送响应1SSE 推送响应2听着还行对吧。但你仔细想一下这里面有个很反直觉的地方。客户端通过POST发了一个请求响应为什么不直接在HTTP响应里返回而是要绕到另一条SSE连接上推回来我跟你说这就是双endpoint架构最别扭的地方。因为SSE连接是服务器往客户端推的唯一通道。如果服务器直接在POST的HTTP响应里返回结果那只有这一个请求的结果能回去。但如果服务器还需要推送进度通知呢如果服务器中途需要主动问你「要不要继续执行」呢这些都是POST的HTTP响应做不到的HTTP请求-响应一次就完了。所以MCP把所有从服务器发往客户端的东西不管是正常的结果、进度通知还是服务器的主动请求全部走SSE通道推。这就导致了一个很别扭的后果客户端发了一个POST请求之后得去另一条SSE连接上等响应。两条通道之间要做关联你说复杂不复杂。来我给你画一下完整的交互流程。MCP服务器MCP客户端MCP服务器MCP客户端1. 建立SSE长连接2. 初始化握手3. 调用工具SSE连接断开 所有通信中断GET /sseevent: endpointdata: /messages?sessionIdabcPOST /messages initializeSSE推送 InitializeResultPOST /messages tools/callSSE推送 进度50%SSE推送 最终结果客户端连上/sse之后服务器先推一个endpoint事件过来告诉客户端「你往这个地址POST消息」。这个地址里会带上一个sessionId用来标识你是谁。然后客户端开始发initialize请求服务器通过SSE推回初始化结果。接着客户端发tools/call请求调用工具服务器通过SSE推进度、推结果。全程SSE连接不能断。一断啥都没了。这块我第一次读规范的时候说实话来来回回看了三遍才理顺。然后在实际工程里面这套方案暴露出了一大堆问题。五. SSE方案的五大问题那个SSE连接必须是持久连接。客户端连上/sse之后这个连接就不能断了得一直挂着。你想想看如果有一万个客户端同时连接服务器就得同时维护一万个长连接。每个连接都要占内存占文件描述符占TCP资源。对服务器来说这是一个巨大的负担。两个endpoint的设计增加了复杂度。你得在服务端维护两套路由在客户端管理两个通信通道。而且这两个通道之间还得做状态同步就是我刚才说的那种POST发了请求得去SSE连接上等响应的别扭逻辑。没法断线重连。虽然SSE协议本身有Last-Event-ID支持断线续传但在MCP的HTTPSSE方案里如果SSE连接断了客户端跟服务器之间的整个通信通道就断了不仅仅是丢了几条消息那么简单。没有机制让你从断开的地方接着来 在网络环境不好的情况下这简直是个灾难。。。跟现代云原生基础设施不兼容。你想在AWS Lambda或者Cloudflare Workers这种无服务器环境里部署MCP服务器抱歉SSE要求持久连接而无服务器的核心设计理念就是请求来了就处理处理完就销毁根本不支持维持长连接。这就直接把一大类部署场景给堵死了。安全方面有隐患。旧的SSE方案在传递认证信息的时候有时候不得不把token塞在URL的查询参数里。但凡对安全有点了解的人都知道URL里的参数会出现在浏览器历史记录里会出现在服务器日志里会出现在代理服务器的缓存里。把认证令牌放在这种地方这不是一个好习惯。这里其实有个背景浏览器原生的EventSource API太简陋了构造函数只接受一个URL没地方传自定义Header所以只能在URL里塞token。你用Python的httpx发SSE请求当然想加什么Header都行但MCP的客户端有很多跑在浏览器环境里这个API限制就成了实际的痛点。反正我觉得一个好的协议设计就像一个好的API设计不是看它能做什么而是看它不能做什么的时候有多优雅。旧的HTTPSSE方案在理想情况下工作得还行但一旦遇到非理想情况各种短板就暴露无遗了。所以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交互流程MCP服务器MCP客户端MCP服务器MCP客户端1. 握手阶段2. 正常业务通信Accept: json, text/event-streamMcp-Session-Id: xxx3. 服务器主动推送 (可选)4. 会话终止POST /mcp initializeInitializeResult Mcp-Session-IdPOST /mcp notifications/initializedPOST /mcp tools/callJSON 直接返回 或 SSE 流式推送GET /mcp Accept: text/event-streamSSE 服务器主动通知DELETE /mcp200 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双方都会清理资源干干净净。对比一下旧的HTTPSSE方案你会立刻感受到差距。旧方案是「必须保持长连接必须有两个endpoint断线了就得重来」。Streamable HTTP是「可以长连接也可以短连接只需要一个endpoint断线了可以续上」。一个被动僵化一个灵活从容。你如果关注这个领域的话应该能感受到这个差距有多大。八. 写在最后我有时候觉得协议设计这件事跟城市交通规划特别像。旧的HTTPSSE就像是一个城市只有两条路一条只准往南走一条只准往北走而且这两条路必须同时通车任何一条堵了整个交通就瘫痪了。Streamable HTTP就像是一条多车道的高速公路简单的小车就是JSON请求走快车道快速通过大货车就是SSE流式传输走慢车道慢慢跑各有各的节奏互不干扰。回到协议本身我觉得MCP传输协议的这三代演进还挺让人感慨的。从stdio的简单直接到HTTPSSE的勉强能用再到Streamable HTTP的灵活优雅。每一代的变化都不是为了变而变而是因为遇到了真实的工程痛点。其实吧计算机领域有一个永恒的规律。所有伟大的协议和系统都是在不断的自我推翻中进化出来的。HTTP从1.0到1.1到2.0到3.0每一次都是对上一代的反思和重构。TCP/IP当年也是在跟OSI七层模型的竞争中走出来的。没有哪个设计是一步到位的。Streamable HTTP让我看到了MCP团队的务实。他们没有因为SSE是自己选的第一版方案就死撑着不改而是在发现问题的第一时间就承认问题重新设计。这种态度我觉得比协议本身更值得尊敬。

更多文章