第 18 章 · 传输层与生命周期
本章目标:理解 MCP 传输层作为"绑定"的职责边界,掌握 stdio 与 Streamable HTTP 两种标准传输的工作方式,弄清 JSON-RPC 2.0 消息格式与协议版本协商机制。
12.1 传输层:只管"怎么送",不管"送什么"
官方规范对传输层(transport)的定义非常克制:传输是一个绑定(binding),它定义三件事——
- 消息如何帧化与投递;
- 请求元数据如何携带;
- 取消与终止如何被信号化。
它不定义消息的含义:消息模式(请求/响应/通知)属于核心协议,在所有传输上完全一致。这带来一个重要保证——你的 Server 业务逻辑换传输不需要改代码,本地开发用 stdio,上生产切 Streamable HTTP,只是启动参数不同。
MCP 有两种标准传输绑定:
| 传输 | 形态 | 典型场景 |
|---|---|---|
| stdio | 客户端启动的子进程,标准输入/输出流上的换行分隔消息 | 本地 Server(文件系统、本地数据库) |
| Streamable HTTP | 每条消息 POST 到单一 MCP 端点;响应是 JSON 对象或该请求作用域的 SSE 流 | 远程托管 Server(多客户端共享) |
规范也允许实现自定义传输,只要满足同样的帧化与投递契约。
12.2 stdio 传输
stdio 是最简单也最常用的本地传输。工作方式:
- Client 以子进程方式启动 Server 程序;
- 双方通过子进程的 stdin/stdout 交换消息;
- 每条消息是一行 UTF-8 编码的 JSON-RPC(换行分隔,禁止内嵌裸换行);
- 日志必须写到 stderr,绝不能污染 stdout。
# server_stdio.py —— stdio Server 的完整骨架
from mcp.server.fastmcp import FastMCP
import sys
mcp = FastMCP("local-notes")
@mcp.tool()
def add_note(text: str) -> str:
"""添加一条笔记"""
with open("notes.txt", "a") as f:
f.write(text + "\n")
return "已保存"
if __name__ == "__main__":
mcp.run() # FastMCP 默认走 stdio 传输客户端配置(以 Claude Desktop 为例):
{
"mcpServers": {
"notes": {
"command": "python",
"args": ["/absolute/path/to/server_stdio.py"]
}
}
}调试时的头号坑:print() 默认输出到 stdout,会破坏消息流导致协议解析失败。所有调试输出一律 print(..., file=sys.stderr) 或用 logging 配置到 stderr。
12.3 Streamable HTTP 传输
远程场景使用 Streamable HTTP:客户端把每条消息作为 HTTP POST 发到一个统一的 MCP 端点;服务端可以选择直接返回一个 JSON 对象,或者返回该请求作用域的 SSE 流(适合长任务的多条进度通知)。此外还支持可选的 GET SSE 端点用于服务端主动推送。
它的设计要点:
- 协议语义零改动:还是那些 JSON-RPC 消息,只是信封换成了 HTTP;
- 元数据镜像:Streamable HTTP 会把请求体里的关键元数据镜像到 HTTP 头(如
MCP-Protocol-Version),让中间件(网关、代理)无需解析消息体就能路由和检查请求; - 天然多租户:一个远程 Server 可以同时服务大量 Client,这是它与"单 Client 子进程"的 stdio 的本质区别。
12.4 消息格式:JSON-RPC 2.0
无论哪种传输,消息体都是 JSON-RPC 2.0,共三种类型:
// ① 请求(有 id,期待响应)
{ "jsonrpc": "2.0", "id": 7,
"method": "tools/call",
"params": { "name": "get_weather", "arguments": { "city": "北京" } } }
// ② 响应(与请求 id 对应)
{ "jsonrpc": "2.0", "id": 7,
"result": { "content": [{ "type": "text", "text": "晴,26℃" }] } }
// ③ 通知(无 id,不需要响应,如工具列表变更提醒)
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }三条铁律(源自规范的 message patterns):Client 发请求、Server 回响应;Server 也可以向 Client 发请求(如 elicitation 向用户要补充信息);通知不需要响应。
12.5 版本协商与兼容性
协议版本管理是理解 MCP 演进的关键。当前规范(2026-07-28 起)采用现代模式:不再有握手,每个请求都在 _meta 字段里声明自己使用的协议版本(HTTP 上同时放在 MCP-Protocol-Version 头里)。服务端逐请求独立接受或拒绝:
- 版本支持 → 正常返回结果;
- 不支持 → 返回错误码
-32022的UnsupportedProtocolVersionError,并列出自己支持的版本,客户端可换版本重试:
{
"jsonrpc": "2.0", "id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": {
"supported": ["2026-07-28", "2025-11-25"],
"requested": "1900-01-01"
}
}
}历史包袱的处理也有明确术语:2025-11-25 及更早版本属于遗留模式(Legacy)——它们靠一次性的 initialize 握手建立会话、协商版本;同时支持新旧两套的实现称为**双时代(Dual-era)**实现。SDK 通常替你处理了这些差异,但排查跨版本兼容问题时必须知道这两套机制的存在。
12.6 会话管理与演进简史
早期 MCP 远程传输采用 HTTP+SSE 双端点方案,但存在重连、状态保持等工程痛点;Streamable HTTP 将其简化为单端点 + 请求作用域 SSE,成为当前标准。会话层面,无状态的现代模式下每个请求自包含(带 _meta 版本、能力与身份信息),发现结果(server/discover)可缓存复用;遗留模式则由 initialize 握手建立有状态会话。
实践建议:新项目一律使用最新 SDK 与 Streamable HTTP;维护旧集成时先确认对端处于哪个"协议时代",再决定升级路径。
本章小结
- 传输是"绑定":只管帧化投递、元数据携带与取消终止,协议语义跨传输完全一致;
- stdio = 子进程 + stdin/stdout 上的换行分隔 JSON-RPC,日志必须走 stderr;
- Streamable HTTP = 单一 MCP 端点的 POST,响应为 JSON 或请求作用域 SSE 流,天然支持多客户端;
- 消息三形态:请求(有 id)、响应(对应 id)、通知(无 id);底层统一为 JSON-RPC 2.0 UTF-8;
- 版本协商已从"initialize 握手"演进为"每请求
_meta声明 + 服务端逐请求校验",不支持时报-32022并附支持列表。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. MCP 规范中"传输层(transport / binding)"的职责边界是什么?
2. stdio 传输下,Server 打印调试日志的正确做法是?
3. Streamable HTTP 传输中,客户端发出一条消息后,服务端的响应形式是什么?
4. 现代版 MCP 规范(2026-07-28)的版本协商机制是怎样的?
🛠️ 动手实践
- 把第 11 章的笔记 Server 同时以 stdio 和 Streamable HTTP 两种传输启动,用 MCP Inspector 分别连接,验证工具行为完全一致。
- 故意在 stdio Server 里加一行
print("debug info")到 stdout,观察客户端报什么错,再用 stderr 修复它。 - 用 curl 手工构造一条 JSON-RPC 请求 POST 到你的 Streamable HTTP 端点,观察原始响应结构;再发送一个不存在的协议版本,验证
-32022错误与 supported 列表。
完成练习后,进入下一章:编写第一个 MCP Server。