最近在技术社群里看到好几个朋友被同一个问题绊住:MCP server到底应该用SSE连接还是streamable-http连接?有人照着旧教程配了sse://,有人按官方新文档写成https://xxx/mcp,两边看起来都对,但一换就报错。其实问题不在配置,而在协议本身换代了。这篇就把MCP中的streamable-http和SSE协议的区别从头到尾拆一遍,包括设计动机、连接模型、会话管理、重连机制,以及从旧方案迁移到新方案的具体操作步骤和踩坑记录。
1. MCP传输层在解决什么问题
1.1 MCP协议到底管哪一段
MCP(Model Context Protocol)本质是一套基于JSON-RPC 2.0的通信规范,核心目的是让AI应用(宿主)和外部工具资源(服务器)之间做到“即插即用”。你可以把它理解成AI世界的USB-C接口:模型侧预留一个标准口子,工具侧也做成标准口子,插上就能通信。
关键点是MCP并不规定AI的逻辑怎么实现,它只规定“消息的格式”和“消息怎么送出去”。后者就是传输层。MCP支持多种传输方式,早期主流是stdio,也就是客户端进程拉起一个server子进程,通过标准输入输出通信。stdio在本地开发场景下很爽,但跨机器、跨服务就不行了。于是官方加入了HTTP传输,而HTTP传输内部又出现了两代方案:旧版叫HTTP+SSE,新版叫Streamable HTTP。
很多教程把这两个方案并列着讲,会造成误会。实际上它们不是并列关系,而是替代关系。HTTP+SSE已经在规范中被标记为DEPRECATED(弃用),Streamable HTTP才是当前推荐的HTTP传输实现。所以搞清楚为什么换代,比死记区别更有价值。
1.2 把SSE和streamable-http放进MCP的坐标系
先明确一个容易混淆的地方:SSE本身不是MCP发明的协议,它是W3C的标准,全称Server-Sent Events,一种基于HTTP的服务器推送技术。而streamable-http是MCP规范中定义的HTTP绑定方案,它内部也用了SSE的事件流格式,但两者的“协议边界”完全不同。
我的理解是:SSE是“底层载体”,streamable-http是“建筑方案”。同样用的是SSE格式的text/event-stream,MCP旧版把SSE作为独立通道来用,新版则把SSE压缩成一种可选的响应编码方式,嵌入到更完整的HTTP会话设计里。这也是为什么你会看到streamable-http的请求和响应里带着SSE的影子,但使用方式跟旧版大不相同。
2. 旧版HTTP+SSE:双通道架构的前世
2.1 SSE协议本身是怎么工作的
SSE的优势是“轻”。它不需要像WebSocket那样先握手升级协议,它就是一次普通的HTTP请求,服务器端把Content-Type设为text/event-stream,然后连接就一直开着,服务器往里面持续写入事件文本。每条事件的基本格式是这样的:
data: {"key": "value"}以两个换行符作为事件分隔,事件里可以带event:、id:、retry:等字段。浏览器端的EventSource对象还能自动重连,这是SSE的一个招牌能力。
在浏览器场景里,SSE用得很顺手,因为EventSource API简单,自动重连、事件分发都帮你封装好了。但是要注意,EventSource本身是单向的,客户端只能接收,不能通过这个连接发送数据。如果你需要在同一个连接上双向发送,SSE就不够用,得再开一个POST通道或者换WebSocket。
2.2 MCP旧版如何把SSE拼成双向通信
MCP旧版HTTP+SSE就是典型“SSE收消息 + POST发消息”的双通道架构。整体流程大致是这样:
- 客户端先发一个
GET /sse,建立SSE事件流。 - 服务器通过SSE流推送给客户端一个
endpoint事件,这个事件告诉客户端“你后续的JSON-RPC消息请POST到这个地址”。 - 客户端拿到
endpoint后,每次发请求就POST到那个地址。 - 服务器处理完请求,不是直接给POST返回结果,而是把结果包装成
message事件,通过之前建立的SSE流推送给客户端。
也就是说,客户端发请求是一根管子,收响应是另一根管子。这种设计在MCP早期解决了“服务器主动推送”的问题,但也带来了一个隐患:两管子的生命周期不一致。POST请求可以随时发,但SSE流一旦断开,所有后续响应就丢了,除非你重新建立SSE并重新走一遍初始化。
2.3 旧版方案的三个痛点
我实际用下来,旧版SSE方案至少有三个明确痛点:
第一,协议状态散落两处。客户端必须维护两个连接:SSE订阅连接和POST消息连接,消息配对完全依赖JSON-RPC里的id字段。一旦网络抖动,SSE断了,你根本不知道哪些响应还没回来。
第二,重连成本高。浏览器EventSource虽然会自动重连,但MCP server有自己的会话状态。重连后如果不重新走initialize流程,服务器可能不认你。这就导致MCP客户端必须自己实现一套“断了全量重来”的逻辑,复杂度不低。
第三,对HTTP中间设施不友好。SSE是长时间占用连接,很多网关、负载均衡、代理服务器默认会缓冲响应或者设置空闲超时。如果中间设备不支持流式透传,就会出现那种“数据迟迟不回来”或者“连接被凭空掐断”的情况。这也是为什么我后来遇到很多部署在云服务后面的MCP server,用SSE方案总是莫名其妙掉线。
3. Streamable HTTP:一个端点解决所有问题
3.1 新方案的设计哲学
Streamable HTTP的设计目标很明确:把旧版的双通道合并成单通道,同时保留服务器推送能力。官方在协议里给出的核心思路是“从头到尾都是POST请求-响应周期,但允许响应以流式(stream)方式返回”。
这句话信息量很大。我拆开理解:
- “都是POST请求”意味着客户端发消息的路径统一了,不再需要SseServerTransport单独管理消息订阅。
- “允许流式响应”意味着服务器可以在一次POST请求的响应里,持续向客户端推送多条消息,包括最终响应、进度通知、日志消息等。
- “响应可能是普通JSON”意味着如果不是流式场景,服务器可以直接返回一个
application/json的普通响应,降级到最普通的HTTP形态。
所以你看到了:streamable-http里面依然能看到text/event-stream,但它是作为“响应的一种编码方式”存在的,而不是作为独立通道。一个很形象的类比:旧版是打电话(POST)和收传真(SSE)两套设备;新版是一台带传真功能的电话——你打过去,对方可以直接口述答案(JSON),也可以从同一线路把文件传真过来(stream)。
3.2 通信流程拆解
Streamable HTTP的典型流程是这样的:
- 客户端向服务端URL发送
POST,请求体是JSON-RPC消息。 - 请求头里的
Accept字段可以声明支持application/json和text/event-stream。 - 服务器根据处理逻辑选择返回方式:
- 如果返回普通JSON,客户端一次性拿到完整响应;
- 如果返回
text/event-stream,客户端需要持续读取流,直到流结束; - 如果请求是通知类消息(不需要响应),服务器可以返回
204 No Content。
- 初始化成功后,服务器通过
Mcp-Session-ID响应头返回会话标识,客户端后续请求需要带上这个header。
这里有一个细节值得注意:MCP规范要求客户端必须同时支持流式和非流式两种响应,但服务器可以只支持一种。初始化请求的响应,服务器一般会优先走流式,因为需要用同一个流向客户端推送initialized通知。实际上我遇到的大多数server,初期消息都会用流式返回。
3.3 会话管理与重连
Streamable HTTP的会话管理比旧版成熟得多。旧版SSE里,会话状态绑定在SSE连接上,连接一断,状态基本等于没有。Streamable HTTP则通过Mcp-Session-ID把状态和“连接”解耦了。
具体场景是这样的:客户端和服务器之间连接断开后,客户端可以用之前的Mcp-Session-ID重新发起initialize请求。服务器如果支持会话恢复,就不需要你重新注册所有工具,直接在原上下文基础上继续;如果不支持,它会忽略旧session,建立新会话。
我实际验证下来,这个机制在“客户端网络切换、代理重连、服务端重启”场景下仍有差异。服务端重启会丢失内存会话,这个没法靠协议解决;但如果是纯粹的网络断开,用session id恢复的成功率还是很高的。所以客户端实现时一定要把Mcp-Session-ID存好,别每次连接都丢弃。
4. streamable-http和SSE的关键差异对照
4.1 协议定位与连接模型
先给一张核心差异表,方便快速对照:
| 维度 | 旧版HTTP+SSE | Streamable HTTP |
|---|---|---|
| 规范状态 | DEPRECATED(弃用) | RECOMMENDED(推荐) |
| 连接模型 | 双通道:SSE订阅 + POST消息 | 单通道:POST请求-响应 |
| 响应形式 | 只能通过SSE事件流推送 | JSON、SSE流、204三类 |
| 会话标识 | 无显式机制,靠SSE连接绑定 | Mcp-Session-ID显式管理 |
| 重连恢复 | 需要重新初始化 | 支持会话恢复 |
| 代理友好性 | 较差,长连接易被中间层掐断 | 较好,更贴合REST语义 |
| 消息配对 | 依赖JSON-RPC id | 依赖POST请求与响应对应 |
这张表里最核心的就是“连接模型”。旧版是两个连接,一个收一个发;新版是每次请求一个连接,响应时灵活选择编码。这个差异决定了后面所有行为的不同。
4.2 为什么说Streamable HTTP更适合现代网络环境
现代网络环境里,应用前面通常有API网关、负载均衡、CDN、WAF这些中间设施。旧版SSE方案要求一个GET连接长时间挂在那里,很多网关默认会对这类长连接做缓冲,甚至因为空闲超时直接把连接掐掉。我遇到过好几次这种情况:服务端明明在处理,前端却收不到响应,查看网关日志才发现是“upstream read timeout”。
Streamable HTTP把请求和响应收敛在POST周期内,中间设施对POST响应的处理要宽容得多。特别是在流式响应场景,规范允许服务器在关闭流之前持续发送数据,这和普通HTTP chunked传输的语义一致,网关更容易透传。所以如果你要把MCP部署到生产环境,我会直接建议优先上Streamable HTTP。
4.3 一个容易被忽略的细节:Accept头筛选
Streamable HTTP对响应格式的选择,依赖客户端的Accept头。规范建议客户端发送请求时带这样的头:
Accept: application/json, text/event-stream服务器读取这个头之后,决定用JSON还是流式响应。如果客户端只写application/json,服务器将不会返回流式响应,那么你依赖的进度通知、过程日志就会收不到。反过来,如果客户端声明支持两者,但服务器实际返回了流式响应,客户端就必须把流解出来处理。这个细节是很多迁移项目踩坑的源头。
5. 从SSE迁移到Streamable HTTP的实操记录
5.1 客户端改造(以Python生态为例)
MCP官方Python SDK同时保留了旧版SSE客户端和新版Streamable HTTP客户端,改造其实不复杂。旧版连接方式:
from mcp.client.sse import sse_client async with sse_client("http://localhost:8000/sse") as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools()新版连接方式:
from mcp.client.streamable_http import streamable_http_client async with streamable_http_client("http://localhost:8000/mcp") as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools()改动点只有两处:import路径和URL路径。但内部行为完全不同:新版客户端会自动维护Mcp-Session-ID、根据响应头切换流式/非流式解析、处理会话恢复逻辑。对于上层业务代码,几乎无感。
这里有个小提醒:如果你用的是旧版客户端连接新版server,会有一部分能力退化。比如进度通知、日志通知在旧版客户端里解析不完整。所以不光server要换,client也要升级到匹配版本。
5.2 服务端适配FastAPI的示例
在FastAPI上运行MCP,新老版本的启动方式也不一样。旧版需要自己挂SSE transport和POST消息路由:
from mcp.server.sse import SseServerTransport新版则简洁很多,一般直接用SDK提供的FastAPI应用工厂:
from mcp.server.streamable_http import streamable_http_app from mcp.server.fastmcp import FastMCP mcp = FastMCP("demo-server") @mcp.tool() def add(a: int, b: int) -> int: return a + b app = streamable_http_app(mcp)这样就把一个MCP服务挂到了Web应用里,uvicorn起来之后,客户端访问http://localhost:8000/mcp即可。
我建议上线前先看下mcp.server.streamable_http的源码,确认SDK版本接口有没有变化。2025年之后MCP迭代很快,函数签名、参数命名在不同小版本之间有过调整,依赖固定版本很重要。
5.3 验证连通性的调试思路
迁移完成后,验证阶段不要直接上复杂工具调用,我习惯分三步走。
第一步,用curl看握手是否正常:
curl -X POST http://localhost:8000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"debug","version":"1.0"}}}'观察返回的Content-Type。如果返回text/event-stream,说明server走了流式;如果返回application/json,说明走的普通响应。两种都算正常。
第二步,确认Mcp-Session-ID响应头有没有返回。如果返回了,保存它,在后续请求中带上:
curl -X POST http://localhost:8000/mcp \ -H "Mcp-Session-ID: 上一步拿到的id" \ -H "Content-Type: application/json" \ ...第三步,发出的请求如果期望服务器主动推送,比如请求内包含进度通知支持,那就盯着终端确认流里是否出现data:事件。这一步能直接验证server是否真的在用SSE流推送。
5.4 代码里的超时参数不能忽略
Streamable HTTP比SSE更依赖合理的超时配置。我见过不少线上问题都是客户端默认超时太短,导致从发起请求到收到流式数据之间被提前断开。尤其是服务器冷启动、首次初始化工具列表时,可能要几秒钟才有第一个事件。建议把连接超时、读取超时分别配置,不要共用一个小值。Python SDK里可以通过底层HTTP客户端(如httpx)的timeout参数控制,生产环境我会给到30秒起步。
6. 常见问题与排查技巧实录
6.1 连接建立失败
现象:客户端连/mcp返回404或者405。
原因:路径不对或者方法不匹配。Streamable HTTP只接受POST请求,不接受GET。如果你在浏览器直接访问这个URL,看到405 Method Not Allowed,是对的。如果看到404,说明端点路径没对上。
处理:确认server的挂载路径,比如app = streamable_http_app(mcp)默认挂载在根路径,如果你用FastAPI再包一层,路径可能变成/mcp或自定义路径。用curl先验证。
6.2 请求发出去了,但迟迟拿不到响应
现象:POST请求发出去,服务端日志显示正在处理,客户端却一直等不到数据。
原因:最常见的是健康检查或网关把长连接掐断了。我用Nginx代理时遇到过,需要在proxy_read_timeout和proxy_buffering off配合调整。Nginx默认会缓冲上游响应,导致SSE流里的数据积压在缓冲区,客户端感知不到。
处理:在Nginx里对MCP端点关闭缓冲并调大超时:
location /mcp/ { proxy_pass http://backend; proxy_buffering off; proxy_read_timeout 300s; proxy_http_version 1.1; }6.3 会话恢复失败,每次都要重新初始化
现象:客户端保存了Mcp-Session-ID,重连时也带上了,但服务端还是要求重新初始化,甚至直接报错。
原因:服务端可能不支持会话恢复,或者服务端重启导致内存中的session状态丢失。还有可能是客户端发送的session id是从响应头里拿的,但被中间层改写了。
处理:先确认服务端版本和配置是否启用会话管理。如果服务端是无状态设计,就应该允许用旧session初始化后返回新session id。客户端需要做到“拿不到恢复就自动重来一次initialize”,不能卡死。
6.4 流式响应解析中断,数据只有一半
现象:客户端能收到流式响应,但是解析到一半就报错,或者事件不完整。
原因:一种情况是事件被中间设备截断了,比如代理做了chunk大小限制;另一种是客户端出现了SSE解析器实现问题,对多行data:字段处理不当。
处理:在客户端侧打印原始数据,用最朴素的字符串分割验证\n\n分割是否正常。如果数据完整,那就是解析器问题。确保用的是标准SSE事件解析库,不要自己手写一个按行读的解析器就完事。MCP的SSE流里,一个事件可能有多行data:,解析规则必须遵循W3C标准。
6.5 常见问题速查表
| 问题现象 | 可能原因 | 建议处理 |
|---|---|---|
| 404 | 端点路径配置不对 | 核对server挂载路径与客户端URL |
| 405 | 用了GET访问POST端点 | 确认使用POST方法 |
| 请求超时 | 客户端超时设置太短 | 调大connect/read timeout |
| 流数据卡住 | 网关缓冲/代理超时 | 关闭代理缓冲、调整read timeout |
| 会话失效 | server重启或session过期 | 客户端实现自动重连重初始化 |
| SSE解析报错 | 事件被截断或解析器实现问题 | 打印原始数据,使用标准SSE解析库 |
7. 一点个人体会
迁移到Streamable HTTP之后,我最大的感受是“心智负担下降了”。以前排查SSE传输问题,要同时盯两个连接,理清POST响应和SSE事件的关系;现在只需要盯一次请求、一次响应,不管它返回的是JSON还是流,问题域都收敛了很多。如果你正在新写MCP server,直接跳过旧版SSE,用Streamable HTTP起步;如果是老项目迁移,先把客户端和服务端的SDK版本对齐,再逐项对照本文的流程和速查表,半天内基本能搞定。最后分享一个小技巧:调试时在客户端把HTTP响应头完整打出来,Mcp-Session-ID一出现就能确认握手是否进入正轨,比什么都直观。