MCP 传输方式实战:stdio、Streamable HTTP、WebSocket 该选哪个
结论先放这儿
| 场景 | 选哪种 | 一句话理由 |
|---|---|---|
| 本地开发、桌面客户端(Claude Desktop、Cursor) | stdio | 一条启动命令,不碰网络 |
| Server 部署到服务器、多客户端共享 | Streamable HTTP | 2025 年协议指定的远程方案 |
| 持续推送、长连接 | WebSocket | 全双工,生态还在完善 |
自建 MCP Server 那篇发出去之后,被问得最多的一个问题是「Server 搬到另一台机器上,stdio 还能用吗」。答案是不能。stdio 依赖进程间的管道,Server 和客户端必须挤在同一台机器上。要远程部署,就得换传输方式。
恰好一篇公开的实战文章把 stdio、HTTP+SSE、WebSocket 三种方式放在一起做了对比测试,框架值得借鉴。不过它有两处要修正。一是 2025-06-18 协议版本已经把旧的 SSE 传输标记废弃,远程部署的正解换成了 Streamable HTTP;二是文中部分 SDK API 与真实版本对不上,照抄会直接报错。本文沿用它的对比框架,代码以官方 SDK 实测为准。
一、传输层在协议里站什么位置
MCP 协议本身跟传输方式无关。它规定的是 JSON-RPC 2.0 的消息格式和握手、工具发现、调用这套流程,没规定消息走什么通道。跟 HTTP 可以跑在 TCP、TLS 或 QUIC 之上一个道理,通道换了,协议不变。
官方 Python SDK 里每种传输方式是独立模块,服务端代码写好之后,换传输只需动启动参数。这一点是后面所有选型讨论的基础,传输可替换,工具函数零改动。
二、stdio:本地开发的最短路径
客户端把 Server 当子进程拉起,stdin 发请求,stdout 收响应,stderr 留给日志。三步走完,没有端口、没有 URL。
python
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("tools")
@mcp.tool()
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
if __name__ == "__main__":
mcp.run() # 默认 stdio,无需参数
客户端一侧用 stdio_client 拉起同一个脚本:
python
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
params = StdioServerParameters(command="python", args=["server.py"])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("add", {"a": 1, "b": 2})
print(result.content[0].text)
代价也明确。同一台机器、一个进程只服务一个客户端、每次连接都要冷启动一次进程。开发阶段这些都不叫事,上了生产就是硬伤。
三、Streamable HTTP:远程部署的默认选择
2025-03-26 协议版本引入 Streamable HTTP,2025-06-18 版本把旧的「HTTP + SSE」传输标记废弃。新传输只保留一个 HTTP 端点,客户端 POST 消息上去,响应按需返回。简单调用回普通 JSON,需要推送时再升级成 SSE 流,一个端点干了两件事,旧方案里「请求走 POST、推送走 SSE 长连接」的双通道配置就不再需要了。
服务端改动只有一行:
python
mcp.run(transport="streamable-http") # 监听 8000 端口,端点路径 /mcp
客户端换成 streamablehttp_client,连接地址指向 /mcp:
python
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client("http://localhost:8000/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("add", {"a": 1, "b": 2})
print(result.content[0].text)
这个模式下 Server 可以挂在任何机器上,多客户端共享一个实例,前面还能加 Nginx 做反代、加鉴权做访问控制。之前扛量实战里的限流和熔断,也能直接套在这一层。
四、WebSocket:什么时候才轮到它
WebSocket 提供全双工通道,双方随时互发消息,天然适合「Server 持续推数据」的场景,比如日志流、监控数据的实时输出。
但实际项目里它排最后。官方 SDK 对 WebSocket 传输的支持还在完善,不少企业网络会拦截 WebSocket 连接,断线重连、心跳检测这些都要自己写。Streamable HTTP 在需要推送时升级成 SSE 流,延迟表现接近,工程成本低得多。拿不准的时候选 Streamable HTTP,踩坑概率小。
五、三种传输的切换成本
我做了一次真实切换,把上一篇的 Server 从 stdio 搬到 streamable-http。工具函数一行没动,改动只有两处。服务端 mcp.run() 加上 transport 参数,客户端连接代码从 stdio_client 换成 streamablehttp_client。总改动量个位数行。
| 维度 | stdio | Streamable HTTP | WebSocket |
|---|---|---|---|
| 通信范围 | 本地进程间 | 网络 | 网络 |
| 连接模式 | 一对一 | 多对一 | 多对一 |
| 推送能力 | 有(stderr 之外的双向管道) | 有(按需升级 SSE 流) | 有(原生全双工) |
| 多客户端 | 不支持 | 支持 | 支持 |
| 远程部署 | 不支持 | 支持 | 支持 |
| 配置复杂度 | 低 | 中 | 中高 |
| 适合阶段 | 开发/本地 | 生产/远程 | 实时推送 |
六、三个真实踩坑
坑一,stdio 里 print 毁连接。 stdout 被 MCP 协议消息独占,在 Server 里用 print 打日志,日志会混进协议流,客户端直接解析失败。日志一律走 stderr,或者交给 logging 模块:
python
import sys, logging
logging.basicConfig(level=logging.INFO, stream=sys.stderr)
坑二,旧客户端只认 /sse 路径。 2025-06-18 之前发布的客户端连的是 /sse 端点,升级服务端到 Streamable HTTP 后旧客户端会连不上。过渡期两个端点并存,等客户端都升完再下掉旧路径。
坑三,Nginx 缓冲截断流式响应。 Server 挂在 Nginx 后面时,SSE 流会被默认缓冲攒着,客户端半天收不到推送。反代配置要加这几行:
nginx
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_read_timeout 86400s;
另外调试 Streamable HTTP 不需要 SDK,一条 curl 就能看见协议原貌。手动发一个 initialize 请求,响应里的 JSON-RPC 结构一目了然,这一招在协议篇里也用过:
bash
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-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
写在最后
传输方式的选择没那么玄学。本地开发用 stdio,远程部署用 Streamable HTTP,持续推送才考虑 WebSocket。真正要记住的只有一件事,传输层是可替换的,工具代码跟通道无关,先把工具逻辑写对,通道随场景换。
有一篇公开的实战文章把这个话题的对比框架搭得很好,本文的选型思路受它启发,协议演进和代码部分按 2025-06-18 规范与官方 SDK 重新校过。
来源:一篇公开的实战文章(MCP 传输方式对比测试,blog.csdn.net/2401_89160889/article/details/167495587),仅借鉴对比框架与踩坑思路,代码与协议版本以官方 SDK 实测为准。