ClaudeMap

·MCP 服务器

MCP 2026 实战讲解两种传输层(stdio 本地子进程 vs Streamable HTTP 远端)——JSON-RPC 帧、握手、OAuth 2.1、session 状态、可调试性与选型决策表,外加传输层事故现场五种坑与 2026-07-28 规范变更(无状态化、Mcp-Session-Id 移除)的完整影响。

MCP 传输层全解:stdio vs Streamable HTTP,哪种该用、怎么选(2026)

MCP 2025-11-25 规范定义了 两种传输层——stdio(本地子进程)和 Streamable HTTP(远端 HTTP + SSE)——它们不仅是「端口 vs 管道」的区别,而是信任模型、鉴权边界、会话状态、性能特征的全面不同。本文基于 MCP 2025-06-18 transports 规范 和 2025-11-25 文档重写,逐项拆解:两种传输在 JSON-RPC 帧、鉴权、session 维护、性能、可调试性上的差异,以及 2026 年的工程选型决策表。

TL;DR

  • stdio:Host 把 server 当子进程;OS 进程边界即信任边界;零网络攻击面;本地 / CLI / 内部工具首选
  • Streamable HTTP:server 跑在独立机器;HTTP POST + SSE;必须 OAuth 2.1;团队共享 / SaaS / 多 Host 复用首选
  • 选哪种传输 = 选哪种信任模型——不是性能问题
  • 2025-11-25 规范用 Streamable HTTP 替代了旧的 HTTP+SSE transport,新项目不要再用旧协议
  • 本文覆盖:JSON-RPC 帧、握手协议、鉴权、session、调试、性能、5 类生产场景决策表

stdio:OS 进程即信任边界

stdio 传输下,Host 启动 server 进程作为自己的子进程,双向通信通过 stdin/stdout 走 JSON-RPC 帧。这是一种「最朴素但最强」的本地集成方式——没有网络、没有端口、没有 TLS、没有鉴权协议、攻击面为零。Host 和 server 之间只隔一个 OS 进程边界,而 OS 进程边界就是信任边界。

JSON-RPC 帧格式:每条消息以换行符 \n 分隔(newline-delimited JSON / NDJSON),按行读取,UTF-8 编码。client → server 写 stdin、server → client 写 stdout。stderr 留给 server 自己写日志——这是约定,不能违反。如果 server 写一条 console.log 到 stdout,Host 收到的就是一条非 JSON 的字节流,JSON-RPC 解析器立即报错。

生命周期:Host 启动 → fork 子进程 → 双方跑 initialize 握手 → client 发 notifications/initialized 通知 server「我准备好了」→ server 不回任何东西(这是单向通知)→ 进入正常 tools/list / tools/call 流程 → Host 退出时发 shutdown 请求并关闭 stdin,子进程自然 EOF 退出。

调试优势:因为是同一台机器上的两个进程,调试工具天然成熟——gdb / lldb 直接 attach、strace 看 syscall、console.error 直接进 stderr。MCP Inspector 之所以默认走 stdio,就是因为它「打开就能调试」。但局限也在这里:server 必须和 Host 同机,无法跨网络、无法团队共享、无法被多个 Host 同时复用。

Streamable HTTP:跨网络的契约

Streamable HTTP 传输在 MCP 2025-11-25 规范 中引入,替代了 2024-11-05 规范的旧 HTTP+SSE 协议。新项目一律用 Streamable HTTP,不要再用旧协议——它会在 2026 年内被移除。

架构:client 通过 HTTP POST 一个 MCP endpoint 发送 JSON-RPC 请求;server 通过同一个 endpoint 的 GET 流(SSE, Server-Sent Events)推送 notifications/* 事件——一个 endpoint 同时承载请求和事件,Mcp-Session-Id header 维持跨请求的会话状态

JSON-RPC 帧格式:client → server 是单条 POST 请求体里的 JSON;server → client 的 SSE 流每条 event 是 data: {json}\n\n所有 frame 都是完整 JSON 对象——不是 NDJSON,没有行分隔的歧义。

鉴权:必须 OAuth 2.1(MCP authorization 规范)。client 先 POST /oauth/authorize → 重定向到 Anthropic / 你的 IdP → 用户授权 → 拿 access_token → 后续请求带 Authorization: Bearer <token>鉴权从 OS 边界转移到 token——server 面对公开网络,必须显式授权每个 call。

会话状态Mcp-Session-Id 是 server 分配的 UUID,client 后续所有请求必须带这个 header。server 用它维持「这个 client 的 tools/call 上下文」(比如 OAuth scope、连接池索引、subagent 状态)。多个 client 共享一个 server 时,session 隔离是 server 端必须显式实现的责任——不能简单按 IP 切。

调试优势:可以跨网络、可以被多个 Host 同时调用、可以团队共享。调试麻烦:要在 server 与 client 之间架代理(mcp-proxy)才能看到协议流;SSE 事件流断点难定位;OAuth 失败经常表现为「401 with no body」。

五维对比:什么时候用哪种

| 维度 | stdio | Streamable HTTP | |---|---|---| | 信任模型 | OS 进程边界 = 信任边界 | OAuth 2.1 + Bearer Token | | 鉴权 | 无(Host 用户的全部权限) | 必须RFC 8707 Resource Indicators 强制 audience 绑定) | | 部署 | 必须与 Host 同机 | 任意机器,HTTPS 即可 | | 跨 Host 复用 | 不可(每个 Host fork 自己的子进程) | 可(多个 Host 共享一个 server) | | session 状态 | 进程内变量 | Mcp-Session-Id header 跨请求 | | 调试工具 | Inspector / socat / gdb | mcp-proxy / DevTools / OAuth debug page | | 启动延迟 | 进程 fork + JSON-RPC 握手(~100-300ms 冷启动) | TLS 握手 + OAuth refresh(如有)+ JSON-RPC(50-200ms) | | 并发 | 受限于子进程模型;多个 Host 各自 fork | 一台 server 多个 client;用 session 池 | | 网络攻击面 | 零(不出本机) | HTTPS / DNS rebinding / Origin 校验 |

5 类生产场景的选型决策

  1. 个人本地工具 / CLI 包装:stdio。例如把 ffmpeg 包装成 Claude 可调用的工具。
  2. 团队内部工具(≤10 人):stdio + 共享启动脚本(每个开发者本机跑自己的 server)。简单、安全、零运维。
  3. SaaS 工具 / 公共 catalog:Streamable HTTP + MCP Registry(modelcontextprotocol/registry)。这是 2026 年最常见的部署形态。
  4. 多 Host 复用同一组工具:Streamable HTTP 才有意义——stdio 下每个 Host 各跑一份,配置无法同步。
  5. server 自己要调用外部 API(GitHub / Slack / 数据库):stdio 或 HTTP 都可以,关键是 server 内部用 SDK 与外部服务对话(official MCP SDKs)。

反模式:把 stdio 当作「够用就行」部署到远端——你只是把一个本地进程问题换成「server 死锁在断网重连」问题。一旦需要团队共享、跨机器、或被多个 Host 同时调用,立刻升级到 Streamable HTTP。

三层握手:client 启动时到底发生了什么

无论是 stdio 还是 HTTP,client 启动都按 MCP 生命周期规范 走三步握手——但每步在不同传输下行为不同:

第一步:initialize(请求/响应)

  • stdio:client 写到 stdin、server 从 stdin 读到、server 写 JSON 响应到 stdout
  • HTTP:client POST /mcpinitialize JSON-RPC body、server 返回 200 OK + 响应

server 响应里有两个关键字段:

  • protocolVersion:例如 "2025-11-25"不匹配是 2026 年最常见的握手失败——client 升级到 11-25、server 还停在 06-18(或反之),就会报 UnsupportedProtocolVersionError
  • capabilities:server 声明自己支持哪些原语(tools / resources / prompts)。capabilities 里没有的,client 不会发对应请求——例如 server 没声明 tools,client 就不会发 tools/call

第二步:notifications/initialized(client → server 通知)

client 发一个没有 id 字段的 JSON-RPC 通知,告诉 server「我已就绪」。server 收到后不回任何东西——这是单向通知。HTTP 传输下 server 仍返回 202 Accepted(HTTP 语义),但 body 一定是空。

易错点:漏发 notifications/initialized 是 OAuth scope 失败的最常见原因——server 在这一步初始化 OAuth context,漏了后续 tools/call 会因为找不到 scope 而 401。

第三步:进入 tools/list / tools/call 主循环

  • stdio:client 在 stdin 持续发请求、server 在 stdout 持续回响应。子进程一直活着直到 Host 退出。
  • HTTP:client 持续 POST 请求;如果 server 要主动推 notifications/resources/updated,client 会先 GET /mcp 拿一个 SSE 流,server 走那条流推事件。两条通道独立

典型 401 排错流程

  1. 抓 client 启动时的 initialize 响应,看 protocolVersioncapabilities——是不是符合预期?
  2. 抓 client 启动后发的 notifications/initialized 通知——是否真的发出?
  3. 抓第一次 tools/list 的请求与响应——server 返回的 scope 是否覆盖该 tool 需要的 resource?

自定义 transport:什么时候该写

MCP 规范没限制你只用 stdio 和 Streamable HTTP——只要能承载 newline-delimited JSON-RPC 帧(stdio)或 HTTP POST + SSE(HTTP),任何 transport 都合法。常见三种自建场景:

Unix domain socket:在同台机器上跨进程通信,但避免暴露端口。比 TCP 端口更安全(仅本机可访问),比 stdio 灵活(多 server 共享 socket 路径)。MCP Python SDK 的 in-memory transport 是参考实现。

WebSocket:HTTP+SSE 的替代——单连接、双向、长连接友好。但MCP 规范目前未标准化 WebSocket transport,写之前要确认 server 与 client 用同一个自定义实现。

gRPC / MessagePack:性能场景——JSON-RPC 帧序列化到二进制,单消息大小压缩 30-50%。但牺牲了调试便利性(不再能 cat 看消息),且生态工具链不支持。

决策原则:90% 场景下用 stdio 或 Streamable HTTP 就够了。只有当 (1) 现有传输性能不够且你愿意为该 10% 场景投入工程、(2) 有现成的 transport 代码(如 Unix socket 库)、(3) 团队接受调试复杂度损失——才考虑自建。

常见坑与反模式:传输层事故现场

坑 1:stdio server 的 stdout 被日志污染。 现象:间歇性「消息格式错乱」。原因:stdout 是 JSON-RPC 协议通道,print / console.log 的每个字节都在破坏帧边界。修法:一切诊断输出改走 stderr;把这条写进 server 的 lint 规则。

坑 2:反向代理缓冲了流式响应。 现象:本地直连一切正常,过了 nginx / 云负载均衡后消息成批延迟或超时。原因:代理对 event-stream 类响应做了缓冲。修法:关掉 proxy buffering(nginx 的 X-Accel-Buffering: no),并确认读超时配置覆盖长连接场景。

坑 3:Streamable HTTP 部署没校验 Origin。 现象:安全扫描发现浏览器里的恶意页面可以驱动你的 server(DNS rebinding 攻击面)。原因:本地或内网 HTTP server 没有校验 Origin 头。修法:按规范要求校验 Origin,拒绝合法清单之外的浏览器来源请求。

坑 4:把「无会话」误当「无状态」。 现象:升级到 2026-07-28 规范后,靠 Mcp-Session-Id 串联的多步流程全部断裂。原因:新版移除了协议级会话(SEP-2567),但你的业务状态并没有自动消失——它只是不再由传输层托管。修法:跨调用状态改用 server 签发的显式句柄,作为普通工具参数在请求间传递。

坑 5:initialize 握手代码残留。 现象:新客户端连不上旧 server,或升级后双向报协议版本错误。原因:2026-07-28 移除了 initialize / notifications/initialized 握手,协议版本与能力改由每个请求的 _meta 携带(SEP-2575),版本不匹配返回 UnsupportedProtocolVersionError。修法:升级 SDK、实现 server/discover 探测端点,删掉手写握手代码。

2026-07-28 规范变更:传输层改了什么

2026 年 7 月 28 日发布的规范是传输层近一年最大的一次重构,官方 changelog 列出九项 Major change,其中六项直接落在传输层:

| 变更 | 影响 | 出处 | |---|---|---| | 移除协议级会话与 Mcp-Session-Id | list 端点不再随连接变化;跨调用状态用显式句柄传参 | SEP-2567 | | 移除 initialize 握手,MCP 转为无状态 | 每个请求在 _meta 携带协议版本与能力;新增 server/discover 探测 RPC | SEP-2575 | | 移除 SSE 断线续传(Last-Event-ID 与事件 ID) | 响应流中断即丢失在途请求,客户端必须换个请求 ID 重发 | SEP-2575 | | subscriptions/listen 取代 GET 端点与 resources/subscribe | 单条长连 POST 流承载 server→client 通知,按类型 opt-in | SEP-2575 | | ping / logging/setLevel 移除 | 日志级别改由每请求 _meta 的 logLevel 控制 | SEP-2575 | | HTTP+SSE 旧 transport 正式标记 Deprecated | 最短 12 个月弃用窗口,新实现禁止采用 | SEP-2596 |

给 server 作者的三条行动建议:

  1. 还在用 HTTP+SSE 的立刻排迁移——它已进入正式弃用注册表,迁移路径就是本文的 Streamable HTTP。
  2. 无状态化是红利不是负担:会话移除后,Streamable HTTP server 天然适配横向扩容与容器编排——K8s 重启不再撕断会话,部署模型与普通 REST 服务对齐。
  3. list 端点新增 ttlMscacheScope 字段(CacheableResult 接口,SEP-2549):客户端据此缓存 tools/list 等响应、减少轮询;规范同时 SHOULD 要求 list 结果排序确定,以提升客户端 prompt cache 命中率——你的 tools/list 实现要跟上。

同一批公告里,Roots、Sampling、Logging 三个功能也进入 Deprecated(与 transport 无关但直接影响 server 设计):目录与文件改走工具参数或资源 URI 传入,日志改走 stderr(stdio 场景)或 OpenTelemetry。

常见问题

我该选 stdio 还是 Streamable HTTP?

看三点:(1) server 是不是必须与 Host 同机?是 → stdio;否 → HTTP。(2) 是不是会被多个 Host 同时调用?是 → HTTP(stdio 下每个 Host 各 fork 一份,配置无法同步)。(3) 能不能接受 OAuth 运维成本?不能 → stdio;能 → HTTP 获得团队共享能力。反问自己一句:如果你的 server 只能跑在一台机器上且只服务一个 Host,stdio 是最稳的选择。

stdio 传输的 server 怎么上线到生产?

不能直接搬。stdio 意味着 server 与 Host 同机,要团队共享必须用 Streamable HTTP。迁移步骤:(1) 把 server 拆成「业务逻辑 + transport 适配」两层,业务逻辑可复用;(2) 选 官方 SDK 用 Streamable HTTP transport 重新包一层;(3) 部署到远端机器或云上;(4) 配 OAuth 2.1 + Bearer Token(参考 authorization 规范);(5) security best practices 检查 Origin 校验 + DNS rebinding 防御。

旧 HTTP+SSE transport(2024-11-05 规范)还能用吗?

能,但新项目不要再用。2025-11-25 规范的 Streamable HTTP 替代了它——前者要求 client 接受 text/event-stream 单向流 + 单独的 POST endpoint;后者统一为单 endpoint、双向、可选 SSE。升级路径:server 端从旧 transport 切到 StreamableHttpServerTransport(Python SDK)或 @modelcontextprotocol/server 包的对应 TS 实现;client 端只要 SDK 升级到 2025-11-25 版本会自动适配。

怎么测试 server 同时支持两种 transport?

写 server 时把业务逻辑与 transport 适配解耦,单元测试用 in-memory transport(Python)或 MemoryTransport(TypeScript)跑——两个 client 实例,一个走 stdio、一个走 HTTP,验证行为一致。集成测试用 MCP Inspector 跑 stdio 路径,用 curl 模拟 POST 请求跑 HTTP 路径。

MCP 2026-07-28 之后 Mcp-Session-Id 还能用吗?

不能——协议级会话与 Mcp-Session-Id 头已从 Streamable HTTP 传输中移除(SEP-2567)。跨调用需要状态时,由 server 签发显式句柄、作为普通工具参数在请求间传递。这一改动让 Streamable HTTP 服务器天然无状态,横向扩容不再受会话粘性约束。

stdio 服务器能同时被多个 client 连接吗?

不能——stdio 的模型是「一个 client、一个子进程」:Host 把 server 作为子进程拉起,stdin/stdout 是这条一对一连接的协议通道。多个 client 各自拉起各自的进程实例,互不共享;想被多 client 共享就部署成 Streamable HTTP 网络服务。

官方参考资料

本文基于截至 2026 年 8 月的 MCP 2025-11-25 规范版本。Streamable HTTP transport 在 2025-11 引入并替代旧 HTTP+SSE;如使用 2024-11-05 规范的旧 transport,建议尽快迁移。

选型速查表

| 场景 | 推荐 | 原因 | |---|---|---| | 个人本地工具 | stdio | 零网络、零鉴权、零运维 | | 团队内部工具(≤10 人) | stdio + 启动脚本 | 简单安全,无需 OAuth | | 跨团队共享 server | Streamable HTTP | 单一 server 多个 Host 复用 | | SaaS 化 MCP server | Streamable HTTP + MCP Registry | 公共发现层 + 鉴权 | | 容器化部署(Docker / k8s) | Streamable HTTP | 容器一般跨 Host 跑 | | 调试服务器 | stdio + Inspector | 最简调试路径 |

一句话总结:stdio 跑得最简,HTTP 跑得最远。选 stdio 是因为不想要 OAuth 运维;选 HTTP 是因为必须服务多个 Host 或跨网络。