深入浅出 MCP 系列教程(三):传输层实战
本系列共 5 篇。前两篇讲解了协议基础与三大核心原语,本篇讲解传输层:协议报文如何在进程之间、跨机器网络上传递。覆盖 stdio 与 Streamable HTTP 两种官方传输的完整规范、握手流程、鉴权、会话管理与高频坑点。
一、传输层与协议层的边界
先建立一条最重要的分界线:
- 协议层:规定消息结构、method、字段、会话顺序、capability 协商;报文本身和”怎么走”无关;
- 传输层:只负责字节流的打包、分割、投递、保序、连接维持;传输层不理解
tools/call、resources/read的业务含义。
一条核心推论:只要能把 JSON-RPC 消息可靠、有序地双向投递,理论上就可以实现自定义传输(WebSocket、Unix Socket、串口等),只是官方只标准化 stdio 和 Streamable HTTP。
传输层必须保证的最小契约
- 消息边界可识别:接收方可以精准切分出一条条独立的 JSON-RPC 对象;
- 顺序:同一连接上发送顺序和接收顺序一致(流保序);
- 双向:Client ↔ Server 都可以主动发消息(通知、反向 sampling 请求);
- 连接断开感知:上层协议收到断开事件,自动销毁会话、订阅、状态。
传输层不负责
版本兼容、capability 校验、业务权限过滤、报文 Schema 校验、消息重试(重试是上层业务逻辑)。
二、stdio 传输(本地子进程传输)
stdio 是本地场景默认、最简单、目前生态最成熟的传输方式。典型场景:Claude Desktop、本地 IDE 插件在本机启动一个独立子进程作为 MCP Server。
2.1 整体模型
Host(Client)使用进程创建 API(spawn / fork)启动 Server 可执行文件,绑定三个标准流:
stdin:Client → Server,发给 Server 的所有 JSON-RPC 请求、通知;stdout:Server → Client,返回的响应、推送通知;stderr:独立日志流,不属于 MCP 协议通道。
2.2 消息分割规则:JSON Lines(JSONL)
stdio 是无边界字节流,约定:每一条独立的 JSON-RPC 消息单独占一行,以换行符 \n 作为消息分隔符。每一行 = 一条完整 JSON 对象,不能把一条消息拆成多行,也不能一行塞多条消息。
1 | {"jsonrpc":"2.0","id":"init-001","method":"initialize","params":{...}}\n |
接收方逻辑:持续读流,按 \n 切分行;对每一行 trim 后尝试 JSON parse;解析失败视为传输坏消息,上报日志、丢弃该行,不直接杀死整个会话。
2.3 stderr 的严格约定(最经典的坑)
Server 绝对不可以把协议消息输出到 stderr;同时,Server 不能随便往 stdout 打印调试日志。
- ✅ stdout:只输出合法、单行 JSON-RPC
- ✅ stderr:日志、调试、报错、堆栈
- ❌ 错误写法:在 Server 代码里随便
console.log("开始启动"),这条文本会混进 stdout,破坏 JSONL 流,导致 Client 解析报错、握手直接失败
新手最常见故障:启动 Server 后 Client 一直卡在初始化,报 JSON parse error,根源就是 Server 在 stdout 打了非 JSON 的调试文本。
2.4 进程生命周期与会话绑定
stdio 模式:一个进程实例对应唯一一条 MCP 会话。
- Client 启动进程 → 开启会话;进程退出(正常 exit、崩溃、被 kill),流直接关闭,会话销毁,所有订阅、状态全部丢失;
- 想重新会话必须重新 spawn 全新进程,从头走完整握手;
- Client 一般通过
SIGTERM请求 Server 优雅退出;如果 Client 先退出,子进程可能变成孤儿进程,好的实现要做进程组托管。
2.5 配置传递
启动子进程时,Client 通过命令行参数 + 环境变量给 Server 传递配置(数据库连接串、文件根目录、密钥、配置路径),而不是通过 MCP 报文。敏感密钥放环境变量比命令行稍微安全一点(命令行在进程列表更容易被看到),但仍然不是安全远程方案,stdio 仅限本机。
1 | const cp = spawn("mcp-fileserver", [], { |
2.6 优缺点与适用边界
优点:
- 零网络配置、不需要端口、不需要鉴权;
- 生态成熟,绝大多数开源 MCP Server 默认支持;
- 隔离天然:每个 Server 独立进程,崩溃不会直接搞崩 Host。
缺点:
- 只能本机使用,不能跨机器;
- 进程启动有冷启动开销;进程管理复杂(僵尸进程、卡死进程);
- 大流量场景下流阻塞风险(缓冲区满,互相卡住)。
2.7 stdio 高频坑清单
- 平台换行符:Windows
\r\n,解析时必须同时兼容; - 缓冲区:部分语言标准库会做行缓冲/块缓冲,stdout 没有自动 flush,消息会卡在缓冲区不发出去(Python / C++ 实现常踩);
- 二进制内容:blob base64 放在 JSON 内部,而不是直接裸流;
- 不要在初始化之前就输出任何 stdout;
- 超长单行 JSON 注意解析内存上限,防止超大报文 OOM。
三、Streamable HTTP(远程网络传输)
当 MCP Server 不在本机,跑在内网另一台机器、云端容器,就需要 Streamable HTTP。它是官方标准化的远程传输方案,核心是:HTTP POST 上行 + SSE(Server-Sent Events)下行。
为什么不用 WebSocket?官方设计选择:复用 HTTP 基础设施、更容易穿过反向代理、防火墙、鉴权中间件,同时保持双向消息通道。
3.1 架构总览
两条独立逻辑通道,复用同一个 HTTP 服务:
上行通道(Client → Server):POST 请求
Client 把 JSON-RPC 消息放在 POST body 发给服务端点;服务返回 202 Accepted(接收成功)或错误码。下行通道(Server → Client):SSE 长连接
Client 发起一个 GET 请求,请求头Accept: text/event-stream,建立 SSE;之后 Server 通过这个流持续推送响应、通知、sampling 请求。
关键点:两条通道解耦,靠一个会话标识符绑定在一起。
3.2 会话 ID(sessionId)
Streamable HTTP 的核心是 sessionId:
- Client 在握手阶段和服务协商得到一个
sessionId; - 后续所有 POST 上行请求、SSE 下行连接都必须带上它;
- Server 用 sessionId 把不同 HTTP 请求归到同一个 MCP 会话;
- sessionId 是服务端生成的不透明字符串,通常放在 HTTP header。
对比 stdio:stdio 天然一个进程 = 一个会话;HTTP 传输是无状态协议,必须用 sessionId 绑定会话上下文、订阅、能力协商结果。
3.3 完整握手流程
- Client 发起初始化请求,请求建立会话;
- Server 生成唯一
sessionId返回; - Client 使用这个 sessionId,打开 SSE GET 长连接,用于接收下行消息;
- Client 在同一会话内,通过 POST 发送第一条
initializeJSON-RPC 请求; - Server 在 SSE 流返回 initialize 响应;
- Client 收到响应后,继续 POST 发送
initialized通知; - 握手完成,进入正常交互:Client 所有往外发的消息走 POST;Server 所有往外推送走 SSE;
- SSE 断开:Client 携带同一个 sessionId 重连;服务端会话可配置超时保留,避免每次断连全部重建。
MCP 协议层的握手顺序(initialize → 等待响应 → initialized)完全不变,只是报文承载在 HTTP/SSE 上。
3.4 SSE 事件格式
每一条 MCP JSON-RPC 消息封装成一个 event,event name 一般为 message,data 字段存放 JSON 字符串:
1 | event: message |
SSE 自带 retry 字段,通知 Client 断线之后等待多久重试连接,刚好适配重连逻辑。
3.5 上行 POST 请求规范
- Content-Type:
application/json - Body:单条 JSON-RPC 对象(官方不鼓励 batch)
- Header:携带
Mcp-Session-Id: xxx - 成功接收:返回
202 Accepted,不把响应放在这个 POST 的 body;响应一定从 SSE 返回 - 参数非法、session 不存在:返回 400 / 401 / 404 / 429 等 HTTP 状态码
这是很多人理解偏差的地方:POST 只是”投递消息”,不是 RPC 同步等待返回;返回走 SSE。
3.6 鉴权方案
官方没有内置一套 auth,而是复用 HTTP 标准鉴权:
- Bearer Token(最常用):
Authorization: Bearer xxx; - API Key:header / query 参数;
- OAuth2 / OIDC:适合企业 SSO;
- mTLS:机器间可信通信,高安全内网。
安全提醒:
- 绝对不要用 URL query 放长期密钥,日志容易泄露;优先 Authorization header;
- sessionId ≠ 身份凭证:sessionId 只是会话句柄,鉴权独立于会话;
- 服务端要做:会话超时、闲置回收、主动失效(登出)、限流。
3.7 重连、心跳、会话超时
网络传输一定会断:WiFi 切换、代理重启、负载均衡切流。标准实现策略:
- SSE 客户端监听 error / close 事件,按服务给出的 retry 间隔重试;
- 会话还在有效期:重连后继续接收消息,不需要重新走 MCP initialize;
- 会话已超时销毁:重连失败,必须全新创建 session、完整重新握手;
- 心跳:可用自定义 notification 双向心跳,探测对端是否存活;
- 服务端策略:闲置 N 分钟销毁会话,释放订阅、文件句柄、数据库连接。
3.8 优缺点
优点:
- 支持跨机器、跨网络、云端部署;可放在反向代理、负载均衡后面;
- 天然支持标准 HTTP 鉴权、限流、日志、审计;
- Host 和 Server 可以独立扩缩容。
缺点:
- 架构复杂很多,需要管理 session、SSE、超时、重连;
- 网络延迟高于本地 stdio;
- 防火墙、代理可能截断长 SSE 连接(很多代理默认有长连接超时);
- 状态托管在服务端,内存、连接资源成本更高。
四、两种传输选型与网关模式
4.1 选型对照表
| 维度 | stdio | Streamable HTTP |
|---|---|---|
| 部署位置 | 本机子进程 | 内网/云端远程 |
| 鉴权 | 不需要(本机信任) | 必须设计鉴权 |
| 会话绑定 | 进程绑定 | sessionId 绑定 |
| 双向通道 | stdin/stdout 流 | POST + SSE |
| 重连 | 断了就销毁,重启进程 | 支持会话重连 |
| 适合场景 | 桌面客户端、本地 IDE、单机工具 | 企业共享 MCP 服务、云 MCP、多客户端共用 |
简单选型口诀:
- 只给自己本机用 → stdio;
- 多个用户、多台机器共享一套 MCP 能力 → Streamable HTTP;
- 混合架构:本地 Host 通过 Streamable HTTP 接入内网中心 MCP 网关,网关再内部调用多个 stdio 本地服务。
4.2 MCP 网关模式(企业常用)
企业落地经常不会让 Host 直连各个后端 MCP Server,而是加一层网关:
- Host 只对接网关(Streamable HTTP);
- 网关负责鉴权、路由、限流、审计;
- 网关背后可以同时对接 stdio 本地服务、远端 HTTP MCP 服务;
- 网关做能力聚合:把多个后端 Server 的 tools / resources 合并,对外统一入口;
- 网关做权限裁剪:同一个后端 Server,不同用户看到不同工具白名单。
网关是传输层和安全层的组合,协议本身没有网关规范,属于上层工程模式。
五、传输层通用异常处理
不管哪种传输,都要处理这些共性问题:
- 消息超限:单条 JSON 过大,应拒绝并断开或截断,防止内存耗尽;
- 乱序、丢消息:stdio 流保序;HTTP+SSE 在代理异常场景可能丢推送,应用层可增加 sequence 序号做确认(协议层不强制);
- 背压:Server 处理慢、Client 持续高速发消息,缓冲区堆积;实现需要背压控制,暂停读/写流;
- 消息解析错误:一般策略是丢弃这条消息、打日志、不直接杀死整个会话;
- 通道半开:一边认为连接正常,另一边已经断开;心跳机制解决。
限流位置:stdio 一般在进程内部做;Streamable HTTP 可以在反向代理、网关、服务端多级限流,按 token、session、用户维度。
六、第三篇小结与高频误区
核心复盘
- 传输层只负责投递字节,不感知业务 method;协议报文可以在 stdio 和 Streamable HTTP 之间无缝迁移;
- stdio 依靠进程 + JSONL,stderr 隔离日志;最大坑点是 stdout 不能随便打印;会话和进程生命周期绑定;
- Streamable HTTP 使用 POST 上行、SSE 下行,依靠 sessionId 维持会话;鉴权复用 HTTP 体系;
- 远程场景天然带来会话超时、重连、鉴权、代理长连接超时等工程问题。
高频误区
| 误区 | 正解 |
|---|---|
| POST 请求会直接返回 tool 结果 | POST 只是投递消息;响应、推送全部从 SSE 返回 |
| sessionId 是鉴权令牌 | sessionId 只是会话句柄;鉴权独立 |
| stdio 可以跨机器远程调用 | stdio 是本机子进程机制,不适合远程 |
| SSE 是双向通道 | SSE 只能下行;上行依赖独立 POST |
| 切换传输需要修改 tools/resources 报文结构 | 协议报文不变,只改投递方式 |
下一篇:《深入浅出 MCP 系列教程(四):进阶能力》,将详解 Sampling 反向模型调用、Roots 资源沙箱、完整通知体系、错误模型与协议扩展机制。