MCP Transport:stdio vs SSE,选错代价很大

周五下午的部署架构讨论会上,老李把他画的 MCP Server 部署图投影到大屏幕上。图里所有的 Server——包括文件操作、数据库查询、天气服务——全部被标注为“HTTP 服务”,旁边密密麻麻画着端口号、防火墙规则和负载均衡器。

“我统一了所有 MCP Server 的通信方式,”老李用激光笔在图上画圈,“全部走 HTTP。端口从 8801 到 8852,防火墙白名单我已经申请好了。”

小王盯着那张图,表情像是看到有人用菜刀开啤酒瓶——能开,但没必要。“老李,你那个文件操作 Server,是用来读写本地文件的。你把一个本地进程的通信,包装成 HTTP 服务,加了一层 TCP 握手、TLS 加密、HTTP 头解析——就为了在同一台机器上传递几个字节?”

“HTTP 是标准协议,走哪都没毛病。”老李抱起胳膊。

“老李你想啊,你在家跟你老婆说话,是直接开口,还是掏出手机打她电话?”

老李一愣。

“本地通信走 HTTP,就等于在家打电话——信号要从你家路由器绕一圈回来。能用,但你图啥?”


三种 Transport,三种使用场景

小王走到白板前,把老李那张部署图擦掉一半,重新画了三种连接方式。

“MCP 目前支持三种 Transport。不是你习惯什么就用什么,而是看你的 Server 跑在哪儿。”

stdio——本地进程间通信。Client 启动 Server 子进程,通过标准输入输出传递 JSON-RPC 消息。零网络开销,延迟不到 1 毫秒。适合文件系统操作、本地数据库、个人工具这类“只在这台机器上跑”的 Server。

SSE(Server-Sent Events)——远程单向推送。Client 通过 HTTP 连接 Server,Server 可以持续向 Client 推送事件。适合远程 API 调用、云端数据库、需要主动通知的第三方服务。

Streamable HTTP——这是最近新加入的传输方式,可以看作是 SSE 的升级版。它支持双向流式通信,既能像 HTTP 一样请求-响应,也能像 WebSocket 一样双向推送,还不需要 WebSocket 那种持久连接的开销。

图:本地用 stdio,远程用 SSE 或 Streamable HTTP——不同位置,不同 Transport


stdio:最简单的往往是最高效的

“stdio 有多简单?”老李问。

小王打开终端,写了一个最小化的 stdio Server 启动配置:“你上次用 Python SDK 写的那个天气 Server,默认就是 stdio。启动时不需要指定端口、不需要配置证书、不需要申请防火墙白名单。Client 直接 fork 一个子进程,往 stdin 写请求,从 stdout 读响应。消息就几个字节,延迟不超过 5 毫秒。”

typescript
// stdio Transport:极简配置
const transport = new StdioServerTransport();
await server.connect(transport);
// 不需要 IP、端口、TLS 证书
// Server 的生命周期与 Client 绑定

“但你把它改成 HTTP,”小王切换到一个对比配置,“你需要做下面这一堆事——”

typescript
// HTTP Transport:需要大量额外配置
import express from "express";
const app = express();
app.post("/mcp", async (req, res) => {
  // JSON-RPC 请求处理
});
app.listen(8801, "0.0.0.0", () => {
  console.log("MCP Server listening on port 8801");
});
// 需要配端口、防火墙、HTTPS 证书(生产环境)
// 需要处理连接中断、超时、限流

“而且你引入了一个安全风险——0.0.0.0 意味着同一局域网内任何人都能访问这个端口。你确定那个文件操作 Server 对外暴露没有风险?”

老李看着那几行对比代码,眉头皱了起来。他当初给文件 Server 配 HTTP,只是为了“统一标准”,结果引入了一堆不必要的网络配置和安全隐患。


SSE:远程服务的“实时推送”能力

“那远程 Server 呢?比如公司的数据库在云上,总不能 stdio 吧?”老李追问。

“远程当然要走网络。但 MCP 选的是 SSE,不是普通 HTTP 请求-响应。”小王在白板上画了两个时序图。

“普通 HTTP 的问题是——它只能客户端问、服务端答。如果服务端有状态更新,比如数据库里的合同条款被改了,服务端没法主动通知客户端。客户端只能靠轮询——每隔一段时间问一次‘有没有更新?有没有更新?’。这不仅浪费资源,还有时间延迟。”

“SSE 解决了这个问题。客户端建立一次 HTTP 连接后,服务端可以持续推送事件流。数据变了,服务端推一条 notifications/resources/updated,客户端立刻就能刷新缓存。AI 看到的始终是最新的数据。”

老李若有所思:“这不就是 WebSocket 吗?”

“像,但更轻。WebSocket 是全双工双向通信,需要升级协议、维护心跳、处理帧格式。SSE 是单向推送,基于标准 HTTP,实现简单,浏览器原生支持。MCP 的场景里,主要是 Server 推通知给 Client,SSE 的单向推送正好够用——不用杀鸡用牛刀。”


Streamable HTTP:新一代的平衡之道

“那 Streamable HTTP 又是什么?”老李指着小王在白板上写的第三个词。

“这是 MCP 协议最新加入的传输方式,可以看作是吸取了 stdio 和 SSE 各自优点的融合方案。它解决了一个痛点——SSE 需要保持长连接,而有些云服务(比如某些 Serverless 平台)不支持持久连接,只允许请求-响应模式。”

小王画了一个对比图。

“Streamable HTTP 的特点是——连接可以随时建立、随时断开,不需要保持。但同时又支持流式响应,Server 可以把一个大结果分块返回。更重要的是,它支持双向流式——Client 可以一边发送请求,一边接收 Server 的推送通知。这在多轮对话场景下特别有用。AI 一边生成回答,一边还能接收到 Server 推来的最新数据。”

图:Streamable HTTP 支持双向流式,连接可中断可恢复

“目前 Streamable HTTP 还在社区讨论和实验阶段。但方向很明确——本地用 stdio,远程优先用 Streamable HTTP,需要兼容旧环境时用 SSE。三种 Transport 各司其职,你不会只用其中一种。”


性能对比:数字不会说谎

老李看着白板上密密麻麻的对比,追问:“有实际的性能数据吗?”

小王打开一个对比表格,上面是他在同一台机器上跑的三组基准测试:

Transport平均延迟吞吐量 (msg/s)连接开销适用场景
stdio0.8ms50,000+本地工具、文件操作
SSE12ms8,000HTTP 握手 + 长连接远程 API、数据库
Streamable HTTP15ms7,500HTTP 握手(可复用)远程服务、Serverless
HTTP 轮询200ms+500每次全新连接不推荐用于 MCP

“你那个本地文件操作 Server,用 stdio 是 0.8 毫秒。你改成 HTTP 之后,每次调用要经历 TCP 握手、TLS 协商、HTTP 头解析——延迟飙到 45 毫秒。慢了 50 多倍。”

老李沉默了。他看着那张表,激光笔在手里转了五六圈,终于开口:“那把本地的那几个 Server 全改回 stdio。远程的数据库和天气 API,用 SSE。”

“那 Streamable HTTP 呢?”小王追问。

“等社区把规范稳定了再说。我们先把现有的三种 Transport 搞对。”老李站起来,走到白板前,把自己之前画的 52 个 HTTP 端口号一一擦掉,只保留了两个远程 Server 的标注。然后他在本地 Server 旁边重新写上“stdio”。

他转过身,端起保温杯喝了一口。枸杞已经凉了,但他的表情松弛了很多。“下不为例啊。以后新加 Server,先问自己三个问题——它跑在哪?它需要实时推送吗?它要不要双向流?然后才决定用哪种 Transport。”

走到门口,他又停住:“那什么,你刚才说 stdio 的生命周期跟 Client 绑定——那如果 Client 挂了,Server 跟着退出,还没保存的状态怎么办?有没有独立部署 stdio Server 的方案?给我看看那部分的进程管理代码。”

小王比了个 OK 的手势,拉开椅子。窗外夕阳正好,白板上那三种 Transport 的箭头并排而立,像是三条各自通向正确场景的小路,终于不再被一根 HTTP 的绳子捆在一起。

MCP 安全指南:OAuth 舞蹈、权限迷宫和信任链
MCP Tools 和 Prompts:Agent 的左膀右臂