1. 传输层:MCP 的「血管」
如果把 MCP 比作 AI 应用的操作系统,那么传输层就是血管——它决定了客户端与服务器之间的消息如何流动,也决定了部署拓扑与安全边界。MCP 目前定义了三种传输方式:stdio、HTTP + SSE 与 Streamable HTTP。三者的共同底层是 JSON-RPC 2.0,但各自适合截然不同的场景。
一句话:传输层只解决「字节怎么走」,协议层才决定「消息怎么懂」;选错传输方式,等于在正确的协议上跑错了路。
1.1 三种传输方式一览
- stdio(标准输入输出):客户端作为父进程启动服务器子进程,通过
stdin发送请求、从stdout读取响应,stderr专门留给日志。零网络依赖,启动快,安全性天然隔离。 - HTTP + SSE(Server-Sent Events):客户端通过
GET /sse建立服务端到客户端的事件流,再通过POST /messages反向提交消息。适合跨机器、跨网络部署,但双端点为治理带来负担。 - Streamable HTTP(流式 HTTP):MCP 规范演进后的新传输,用单个
POST端点统一请求与响应,响应可退化为 SSE 流。兼容性最好,正成为远程部署的事实标准。
2. 三种传输方式对比与选型
选型之前先看清差异。下表从开发、部署、安全三个维度做了完整对比:
| 维度 | stdio | HTTP + SSE | Streamable HTTP |
|---|---|---|---|
| 通信通道 | stdin/stdout | GET /sse + POST /messages | 单 POST 端点 |
| 部署形态 | 本地子进程 | 独立 HTTP 服务 | 独立 HTTP 服务 |
| 跨机器 | 不支持 | 支持 | 支持 |
| 鉴权难度 | 无(进程级信任) | 需 OAuth/Bearer | 需 OAuth/Bearer |
| 流式输出 | 原生支持 | SSE 支持 | SSE 支持 |
| 连接生命周期 | 随进程 | 长连接 | 长连接 |
| 典型场景 | 桌面客户端、CLI | 企业内网服务 | 云托管、公网服务 |
| 复杂程度 | 最低 | 较高(双端点) | 中等 |
2.1 选型决策树
- 只在本地跑(如 Claude Desktop、VS Code 插件):无脑选 stdio,进程隔离即安全边界。
- 需要多台服务器共享一个 Agent:选 SSE 或 Streamable HTTP,把 MCP Server 部署为独立服务。
- 面向公网或云托管:优先 Streamable HTTP,单端点配合鉴权中间件最简单。
一句话:本地用 stdio、内网用 SSE、公网用 Streamable HTTP——这是一条可以闭眼走的路。
3. JSON-RPC 2.0 消息模型
MCP 的所有传输都承载同一种消息格式:JSON-RPC 2.0。理解三类消息是理解整个协议生命周期的钥匙。
3.1 请求(Request)
带 id 的消息是请求,对方必须回 Response。method 可以是 tools/call、resources/read、initialize 等:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "Beijing",
"units": "celsius"
}
}
}
3.2 通知(Notification)
不带 id 的消息是通知,发送方不期待任何响应。典型的如握手后的 notifications/initialized,以及日志通知 notifications/message:
{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
3.3 结果(Result)
响应由 id 关联到对应的请求。成功返回 result,失败返回 error:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "{\"city\":\"Beijing\",\"temperature\":22}" }
]
}
}
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32002,
"message": "Tool not found: get_weathr"
}
}
一句话:带
id的是「提问必须等回答」,不带id的是「说完就走不回头」——这是 JSON-RPC 的两条铁律。
4. initialize 握手与 capability 协商
MCP 会话不是「连上就干活」。连接建立后的第一件事,永远是 initialize 握手:双方互相亮明身份与能力,随后客户端发送 notifications/initialized 通知,会话才算正式进入工作态。
4.1 握手消息示例
客户端先发 initialize:
{
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"roots": { "listChanged": true },
"sampling": {}
},
"clientInfo": {
"name": "my-app",
"version": "1.0.0"
}
}
}
服务器回应自己的能力:
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": true },
"prompts": {}
},
"serverInfo": { "name": "weather-server", "version": "1.0.0" }
}
}
4.2 capability 协商规则
- 双方以较低版本的
protocolVersion作为会话版本,避免断崖式不兼容。 - 服务器必须声明自己实现了哪些能力(
tools、resources、prompts、logging、experimental),客户端只能调用被声明的能力。 - 若服务器没有声明
resources,客户端再发resources/read就会收到-32601(方法不存在)。
// TypeScript SDK 客户端在握手后自动完成协商
const client = new Client({ name: "test", version: "1.0" });
await client.connect(transport);
const serverCapabilities = client.getServerCapabilities();
console.log("服务器能力:", serverCapabilities);
// 判断后再决定是否调用资源能力
if (serverCapabilities.resources) {
const { resources } = await client.listResources();
console.log(resources.map((r) => r.uri));
}
一句话:握手是「自我介绍 + 亮出菜单」,菜单上没有的菜,点了也是 404。
5. 协议状态机
MCP 会话的生命周期可以抽象成一个小的状态机,几乎每种异常都能在状态迁移中找到根因。
| 状态 | 触发事件 | 关键约束 |
|---|---|---|
created | 传输连接建立 | 尚不允许业务消息 |
initializing | 发出 initialize | 等待服务器 result |
ready | 收到握手 result + 发送 notifications/initialized | 业务消息全开 |
disconnected | 传输中断 | 需重新走握手 |
failed | 协议版本不兼容 / 非法消息 | 记录原因,不可自动恢复 |
重连策略:所有传输都要求「重连即重新握手」。因此服务器端不能假设会话状态在重连后仍然有效;客户端也必须重新拉取工具列表,因为服务器可能在重启后换了一批工具。
6. 错误码与错误传播
JSON-RPC 2.0 预定义了通用错误码,MCP 在其之上补充了语义更细的错误码:
| 错误码 | 名称 | 含义 | 应对 |
|---|---|---|---|
-32700 | Parse Error | JSON 无法解析 | 检查消息格式 |
-32600 | Invalid Request | 缺字段或类型错误 | 校验参数 |
-32601 | Method Not Found | 方法不存在 | 核对 method 拼写 |
-32602 | Invalid Params | 参数不合法 | 用 schema 校验 |
-32603 | Internal Error | 服务器内部异常 | 查日志堆栈 |
-32002 | Tool Not Found | 工具不存在 | 检查工具列表 |
-32003 | Prompt Not Found | 提示词不存在 | 检查 prompts 列表 |
-32004 | Resource Not Found | 资源不存在 | 检查资源 URI |
// 客户端对错误的统一处理
try {
const result = await client.callTool({ name: "get_weather", arguments: { city: "Beijing" } });
if (result.isError) {
// 工具逻辑层错误,业务可感知
console.warn("工具执行失败:", result.content);
}
} catch (error) {
// 协议层错误,如方法不存在、参数非法
const code = (error as { code?: number }).code;
if (code === -32602) {
console.error("参数校验失败,请检查输入 schema");
} else if (code === -32002) {
console.error("工具不存在,请刷新工具列表");
} else {
console.error("未分类协议错误:", error);
}
}
7. 鉴权与流式传输
远程传输把 MCP 服务暴露到了网络上,鉴权就不可避免。
7.1 鉴权方案
- 本地信任(stdio):无需鉴权,进程启动权限即边界。
- Bearer Token(SSE/HTTP):最简单,适合内部服务。
- OAuth 2.0:官方推荐用于公网,通过授权端点换取访问令牌。
7.2 Streamable HTTP 流式响应
流式传输让工具可以「边算边给」。服务器对 tools/call 返回 Content-Type: text/event-stream,客户端逐行消费:
# Python 客户端读取流式响应
import requests
import json
with requests.post(
"https://mcp.example.com/mcp",
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": "stream_logs", "arguments": {}},
},
headers={"Authorization": "Bearer <token>"},
stream=True,
) as resp:
for line in resp.iter_lines():
if line:
event = json.loads(line.removeprefix("data: "))
# 每行都是 JSON-RPC 消息或流片段
print(event)
8. 传输层实战:一个可复用的选型模板
把上面的决策固化为配置文件,团队照表填写即可:
# mcp-transport-config.yaml
service:
name: my-mcp-server
version: "1.0.0"
transport:
mode: streamable-http # stdio | sse | streamable-http
endpoint: /mcp
port: 3000
security:
auth: oauth2 # none | bearer | oauth2
token_url: /oauth/token
streaming:
enabled: true
heartbeat_interval_ms: 15000
9. 总结
传输层是 MCP 工程化的第一道选择题,也是排查线上问题时的第一现场。全文核心收束如下:
| 主题 | 关键结论 | 常见误区 |
|---|---|---|
| 传输选型 | 本地 stdio / 内网 SSE / 公网 Streamable HTTP | 认为 SSE 是唯一远程方案 |
| 消息模型 | 带 id 必回、无 id 不回 | 在 notification 上等响应 |
| 握手协商 | 低版本兼容、能力先声明 | 调用未声明的能力 |
| 状态机 | 重连必须重新握手 | 假设会话状态可延续 |
| 错误码 | 协议层与业务层分开处理 | 把所有失败都当业务错误 |
| 远程鉴权 | 公网强制 OAuth/Bearer | 裸奔暴露到公网 |
掌握传输层后,下一步自然是把多个服务器接入同一个客户端——这正是「多服务器编排」要解决的问题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。