ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

传输层详解:stdio vs SSE vs Streamable HTTP

传输层详解:stdio vs SSE vs Streamable HTTP

摘要:MCP传输层支持stdio、SSE和Streamable HTTP三种模式。本文从性能、安全性、部署场景三个维度对比三种传输,给出选型建议和同一Server多传输模式切换方案。

传输层详解 stdio vs SSE vs Streamable HTTP

我把一个 MCP 服务端部署给异地团队用,第一版用 stdio,结果跨网络根本连不上。换成 SSE 能跑了,但又踩了双端点的坑。最后迁到 Streamable HTTP 才算稳。这篇把三种传输模式掰开揉碎讲清楚,配上可直接切换传输模式的代码,让你知道每种模式该用在哪。


三种传输模式总览

MCP 底层用 JSON-RPC 2.0 编码消息,消息必须是 UTF-8。规范在 2025-06-18 版本里只定义了两种标准传输,stdio 和 Streamable HTTP。SSE(准确说是 HTTP+SSE)是 2024-11-05 旧版的远程传输,现在已废弃,但很多老服务端还在用,所以得一起讲。

三种模式定位很清晰。stdio 给本地用,客户端把服务端当子进程拉起,通过标准输入输出通信。HTTP+SSE 给旧版远程用,靠两个 HTTP 端点配合 Server-Sent Events 推消息。Streamable HTTP 给新版远程用,单端点支持 POST 和 GET,可选 SSE 流式推送,还能做会话管理和断线续传。

stdio 本地传输

stdio 是最简单的模式。客户端把服务端作为子进程启动,服务端从 stdin 读 JSON-RPC 消息,往 stdout 写消息。消息之间用换行符分隔,单条消息内部不能有换行。日志只能往 stderr 写,客户端可以选择转发或忽略。

这套模式有几个硬约束。服务端不能往 stdout 写任何非 MCP 消息的内容,客户端也不能往服务端 stdin 写非协议内容。我第一次写服务端时习惯性用print()调试,结果打印的内容被客户端当成 JSON-RPC 消息解析,直接报错断连。stdio 只能本地用,因为它依赖父子进程的管道,跨网络没法用。

stdio 的好处是零配置、低延迟、无网络开销,适合本地工具和桌面客户端(比如 Claude Desktop)。客户端能完全控制服务端的启动参数和环境变量,安全性也好把控。

HTTP+SSE 旧版远程传输

2024-11-05 版本的远程传输用两个 HTTP 端点。客户端先 GET 一个 SSE 端点打开长连接,服务端通过这条连接推送消息,并在首个endpoint事件里告诉客户端往哪个 POST 端点发请求。之后客户端的请求都 POST 到那个端点,服务端的响应和通知通过 SSE 连接回传。

这套设计的问题在于连接模型割裂。请求走 POST,响应走 SSE,两条通道要协调好状态。服务端还要维护两个端点的路由,部署和调试都麻烦。规范在 2025-06-18 版本用 Streamable HTTP 替换了它。

旧版服务端如果还想兼容老客户端,可以同时保留 SSE 端点和新的 MCP 端点。新客户端会先尝试 POST InitializeRequest,失败再回退到 GET 探测 SSE。这套回退逻辑让新老版本能并存一段时间。

Streamable HTTP 新版远程传输

Streamable HTTP 是当前推荐的远程传输。服务端只暴露一个 MCP 端点(比如https://example.com/mcp),同时支持 POST 和 GET。

客户端发消息用 POST,请求头要带Accept: application/json, text/event-stream,表示两种响应都接受。服务端对请求可以返回普通 JSON,也可以升级成 SSE 流。升级成 SSE 流的好处是服务端能在返回最终响应前,先推送进度通知和子请求,长任务体验好很多。

客户端也能用 GET 打开一条 SSE 流,纯接收服务端的主动通知,跟任何请求解耦。这套单端点设计比旧版干净太多。

会话管理是 Streamable HTTP 的重要能力。服务端在初始化响应里带一个Mcp-Session-Id头,客户端后续所有请求都要带上这个 id。会话过期服务端返回 404,客户端要重新初始化。客户端不再需要会话时发 DELETE 显式终止。

断线续传靠 SSE 事件 id。服务端给 SSE 事件附全局唯一 id,客户端断线后用Last-Event-ID头重连,服务端从断点续传未送达的消息。这套机制对网络不稳定的远程场景很实用。

安全方面规范给了三条硬要求。服务端必须校验Origin头防 DNS 重绑定攻击,本地运行要绑定 127.0.0.1 而不是 0.0.0.0,所有连接要做认证。这三条少一条都可能被远程网页利用来攻击本地服务端。

三种模式对比与选型

下面这张表把三种模式的关键维度放在一起对比。

维度stdioHTTP+SSE(已废弃)Streamable HTTP
连接模型父子进程管道双端点,GET 流加 POST 请求单端点,POST 加可选 GET 流
消息方向stdin/stdout 双向请求 POST,响应 SSE 推POST 请求,响应 JSON 或 SSE
会话管理进程生命周期即会话无显式会话 idMcp-Session-Id 头管理
断线续传不适用不支持支持,Last-Event-ID
多客户端一对一支持支持,可多流并存
服务端推送支持(通知)支持(SSE)支持(SSE 流)
部署复杂度高(双端点)
安全控制进程级,本地信任需自定义Origin 校验加会话加认证
适用场景本地工具、桌面客户端兼容老客户端新版远程服务
协议状态当前标准废弃,保留兼容当前标准

选型建议很直接。本地工具和桌面集成一律用 stdio,简单可靠。新做的远程服务端直接上 Streamable HTTP,别再碰 SSE。只有要兼容还没升级的老客户端时,才保留 SSE 端点做过渡。

完整代码

先装依赖。

pipinstallfastmcp

服务端transport_server.py,通过命令行参数切换三种传输模式。

# transport_server.py# 演示同一服务端如何切换 stdio / SSE / Streamable HTTP 三种传输importsysfromfastmcpimportFastMCP# 创建服务端实例,名字会出现在初始化握手信息里mcp=FastMCP("TransportDemo")@mcp.tooldefping()->str:"""一个最简单的工具,返回 pong,用来验证连通性。"""return"pong"if__name__=="__main__":# 从命令行读传输模式,默认 stdiomode=sys.argv[1]iflen(sys.argv)>1else"stdio"ifmode=="stdio":# stdio 模式,默认传输,客户端以子进程方式拉起# 注意服务端别用 print,stdout 只能写 MCP 消息mcp.run()elifmode=="sse":# SSE 旧版远程传输,已废弃,仅用于兼容老客户端# 默认端点路径是 /ssemcp.run(transport="sse",host="127.0.0.1",port=8765)elifmode=="http":# Streamable HTTP 新版远程传输,推荐# 默认端点路径是 /mcpmcp.run(transport="streamable-http",host="127.0.0.1",port=8765)else:print(f"未知传输模式:{mode}",file=sys.stderr)sys.exit(1)

客户端transport_client.py,按模式连接对应传输并调用工具。

# transport_client.py# 演示客户端如何连接三种传输模式的服务端importasyncioimportsysfromfastmcpimportClientasyncdefmain():# 从命令行读模式,默认 stdiomode=sys.argv[1]iflen(sys.argv)>1else"stdio"# 根据模式选择连接源# Client 会根据传入内容自动推断传输方式ifmode=="stdio":# 传脚本路径,自动用 stdio 拉起子进程source="transport_server.py"elifmode=="sse":# 旧版 SSE,连接 /sse 端点source="http://127.0.0.1:8765/sse"elifmode=="http":# 新版 Streamable HTTP,连接 /mcp 端点source="http://127.0.0.1:8765/mcp"else:print(f"未知模式:{mode}")return# 构造客户端,async with 管理连接生命周期asyncwithClient(source)asclient:# 列出工具,确认握手成功tools=awaitclient.list_tools()print("可用工具:",[t.namefortintools])# 调用 ping 工具验证端到端通路result=awaitclient.call_tool("ping",{})print("ping 结果:",result.data)if__name__=="__main__":asyncio.run(main())

效果验证

stdio 模式直接跑客户端,它会自动拉起服务端子进程。

python transport_client.py stdio

输出“可用工具: [‘ping’]”和“ping 结果: pong”。

SSE 模式先起服务端再跑客户端,开两个终端。

# 终端 1,启动 SSE 服务端python transport_server.py sse# 终端 2,连接并调用python transport_client.py sse

Streamable HTTP 同理。

# 终端 1,启动 Streamable HTTP 服务端python transport_server.py http# 终端 2,连接并调用python transport_client.py http

两种远程模式输出和 stdio 一致。想看 SSE 流式推送的效果,把上一篇文章的进度通知服务端换成transport="streamable-http"部署,客户端用 HTTP 连接,进度回调照常触发。

常见问题与避坑

1. stdio 模式 print 污染协议流。这是最高频的坑。服务端里任何print()或第三方库往 stdout 的输出,都会被客户端当 JSON-RPC 消息解析,直接报错断连。调试日志一律走 stderr(print(..., file=sys.stderr))或用 logging 配置 stderr handler。被依赖库坑过一次,排查了两小时才定位是某个 SDK 在 stdout 打了版本号。

2. Streamable HTTP 忘了校验 Origin。规范明确要求校验 Origin 头防 DNS rebinding。本地服务端只绑 127.0.0.1 还不够,远程网页仍可能通过 DNS 重绑定访问。用 FastMCP 这类框架会内置校验,自己用低级 API 实现时务必手动加 Origin 白名单。

3. SSE 双端点连接顺序错。旧版 SSE 必须先 GET 打开 SSE 流,收到endpoint事件拿到 POST 地址后才能发请求。我一开始直接 POST,服务端不认。新项目别用 SSE 了,老项目迁移时注意这个顺序。

4. Mcp-Session-Id 没带上导致 400。Streamable HTTP 下,服务端初始化时返回会话 id,后续请求都要带上。用低级客户端自己拼请求时容易漏,框架客户端一般自动管理。收到 400 就检查是不是漏了会话头。

5. 跨网络硬上 stdio。stdio 只能父子进程本地用,有人想用 SSH 隧道或网络管道强行转发 stdin/stdout,延迟和稳定性都很差。跨网络就用 Streamable HTTP,别在 stdio 上折腾。

小结

传输层选型记住三句话。本地用 stdio,新版远程用 Streamable HTTP,SSE 只在兼容老客户端时保留。stdio 注意别污染 stdout,Streamable HTTP 注意 Origin 校验和会话头管理,SSE 别在新项目里用。下一篇把 MCP 和 Function Calling、OpenAPI 放一起对比,看不同场景该怎么选。


相关推荐

  • MCP协议全景:Host、Client、Server架构详解
    • 多传输模式切换:同一个Server支持stdio和HTTP
    • 部署上线:Docker容器化与云端部署
返回列表