深入浅出 MCP 系列教程(三):传输层实战

本系列共 5 篇。前两篇讲解了协议基础与三大核心原语,本篇讲解传输层:协议报文如何在进程之间、跨机器网络上传递。覆盖 stdio 与 Streamable HTTP 两种官方传输的完整规范、握手流程、鉴权、会话管理与高频坑点。

一、传输层与协议层的边界

先建立一条最重要的分界线:

  • 协议层:规定消息结构、method、字段、会话顺序、capability 协商;报文本身和”怎么走”无关;
  • 传输层:只负责字节流的打包、分割、投递、保序、连接维持;传输层不理解 tools/call、resources/read 的业务含义。

一条核心推论:只要能把 JSON-RPC 消息可靠、有序地双向投递,理论上就可以实现自定义传输(WebSocket、Unix Socket、串口等),只是官方只标准化 stdio 和 Streamable HTTP。

传输层必须保证的最小契约

  1. 消息边界可识别:接收方可以精准切分出一条条独立的 JSON-RPC 对象;
  2. 顺序:同一连接上发送顺序和接收顺序一致(流保序);
  3. 双向:Client ↔ Server 都可以主动发消息(通知、反向 sampling 请求);
  4. 连接断开感知:上层协议收到断开事件,自动销毁会话、订阅、状态。

传输层不负责

版本兼容、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
2
3
{"jsonrpc":"2.0","id":"init-001","method":"initialize","params":{...}}\n
{"jsonrpc":"2.0","id":"init-001","result":{...}}\n
{"jsonrpc":"2.0","method":"initialized","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
2
3
4
5
6
const cp = spawn("mcp-fileserver", [], {
env: { ROOT: "/home/user/docs" }
});
// cp.stdin -> 发消息
// cp.stdout -> 读消息
// cp.stderr -> 单独收集日志

2.6 优缺点与适用边界

优点:

  • 零网络配置、不需要端口、不需要鉴权;
  • 生态成熟,绝大多数开源 MCP Server 默认支持;
  • 隔离天然:每个 Server 独立进程,崩溃不会直接搞崩 Host。

缺点:

  • 只能本机使用,不能跨机器;
  • 进程启动有冷启动开销;进程管理复杂(僵尸进程、卡死进程);
  • 大流量场景下流阻塞风险(缓冲区满,互相卡住)。

2.7 stdio 高频坑清单

  1. 平台换行符:Windows \r\n,解析时必须同时兼容;
  2. 缓冲区:部分语言标准库会做行缓冲/块缓冲,stdout 没有自动 flush,消息会卡在缓冲区不发出去(Python / C++ 实现常踩);
  3. 二进制内容:blob base64 放在 JSON 内部,而不是直接裸流;
  4. 不要在初始化之前就输出任何 stdout;
  5. 超长单行 JSON 注意解析内存上限,防止超大报文 OOM。

三、Streamable HTTP(远程网络传输)

当 MCP Server 不在本机,跑在内网另一台机器、云端容器,就需要 Streamable HTTP。它是官方标准化的远程传输方案,核心是:HTTP POST 上行 + SSE(Server-Sent Events)下行。

为什么不用 WebSocket?官方设计选择:复用 HTTP 基础设施、更容易穿过反向代理、防火墙、鉴权中间件,同时保持双向消息通道。

3.1 架构总览

两条独立逻辑通道,复用同一个 HTTP 服务:

  1. 上行通道(Client → Server):POST 请求
    Client 把 JSON-RPC 消息放在 POST body 发给服务端点;服务返回 202 Accepted(接收成功)或错误码。

  2. 下行通道(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 完整握手流程

  1. Client 发起初始化请求,请求建立会话;
  2. Server 生成唯一 sessionId 返回;
  3. Client 使用这个 sessionId,打开 SSE GET 长连接,用于接收下行消息;
  4. Client 在同一会话内,通过 POST 发送第一条 initialize JSON-RPC 请求;
  5. Server 在 SSE 流返回 initialize 响应;
  6. Client 收到响应后,继续 POST 发送 initialized 通知;
  7. 握手完成,进入正常交互:Client 所有往外发的消息走 POST;Server 所有往外推送走 SSE;
  8. SSE 断开:Client 携带同一个 sessionId 重连;服务端会话可配置超时保留,避免每次断连全部重建。

MCP 协议层的握手顺序(initialize → 等待响应 → initialized)完全不变,只是报文承载在 HTTP/SSE 上。

3.4 SSE 事件格式

每一条 MCP JSON-RPC 消息封装成一个 event,event name 一般为 message,data 字段存放 JSON 字符串:

1
2
3
4
5
event: message
data: {"jsonrpc":"2.0","id":"init-001","result":{"protocolVersion":"0.1.0",...}}

event: message
data: {"jsonrpc":"2.0","method":"notifications/resources/updated","params":{"uri":"file:///demo.txt"}}

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 标准鉴权:

  1. Bearer Token(最常用):Authorization: Bearer xxx;
  2. API Key:header / query 参数;
  3. OAuth2 / OIDC:适合企业 SSO;
  4. mTLS:机器间可信通信,高安全内网。

安全提醒:

  • 绝对不要用 URL query 放长期密钥,日志容易泄露;优先 Authorization header;
  • sessionId ≠ 身份凭证:sessionId 只是会话句柄,鉴权独立于会话;
  • 服务端要做:会话超时、闲置回收、主动失效(登出)、限流。

3.7 重连、心跳、会话超时

网络传输一定会断:WiFi 切换、代理重启、负载均衡切流。标准实现策略:

  1. SSE 客户端监听 error / close 事件,按服务给出的 retry 间隔重试;
  2. 会话还在有效期:重连后继续接收消息,不需要重新走 MCP initialize;
  3. 会话已超时销毁:重连失败,必须全新创建 session、完整重新握手;
  4. 心跳:可用自定义 notification 双向心跳,探测对端是否存活;
  5. 服务端策略:闲置 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,不同用户看到不同工具白名单。

网关是传输层和安全层的组合,协议本身没有网关规范,属于上层工程模式。

五、传输层通用异常处理

不管哪种传输,都要处理这些共性问题:

  1. 消息超限:单条 JSON 过大,应拒绝并断开或截断,防止内存耗尽;
  2. 乱序、丢消息:stdio 流保序;HTTP+SSE 在代理异常场景可能丢推送,应用层可增加 sequence 序号做确认(协议层不强制);
  3. 背压:Server 处理慢、Client 持续高速发消息,缓冲区堆积;实现需要背压控制,暂停读/写流;
  4. 消息解析错误:一般策略是丢弃这条消息、打日志、不直接杀死整个会话;
  5. 通道半开:一边认为连接正常,另一边已经断开;心跳机制解决。

限流位置:stdio 一般在进程内部做;Streamable HTTP 可以在反向代理、网关、服务端多级限流,按 token、session、用户维度。

六、第三篇小结与高频误区

核心复盘

  1. 传输层只负责投递字节,不感知业务 method;协议报文可以在 stdio 和 Streamable HTTP 之间无缝迁移;
  2. stdio 依靠进程 + JSONL,stderr 隔离日志;最大坑点是 stdout 不能随便打印;会话和进程生命周期绑定;
  3. Streamable HTTP 使用 POST 上行、SSE 下行,依靠 sessionId 维持会话;鉴权复用 HTTP 体系;
  4. 远程场景天然带来会话超时、重连、鉴权、代理长连接超时等工程问题。

高频误区

误区 正解
POST 请求会直接返回 tool 结果 POST 只是投递消息;响应、推送全部从 SSE 返回
sessionId 是鉴权令牌 sessionId 只是会话句柄;鉴权独立
stdio 可以跨机器远程调用 stdio 是本机子进程机制,不适合远程
SSE 是双向通道 SSE 只能下行;上行依赖独立 POST
切换传输需要修改 tools/resources 报文结构 协议报文不变,只改投递方式

下一篇:《深入浅出 MCP 系列教程(四):进阶能力》,将详解 Sampling 反向模型调用、Roots 资源沙箱、完整通知体系、错误模型与协议扩展机制。