MCP 传输协议详解:从 stdio 到 Streamable HTTP 的技术演进
前言
在第三篇中,我理清了 MCP、Skills、Hooks 三者的关系。如果把 MCP 比作一座桥梁,那么传输协议就是桥上的通行规则——决定了消息如何从一端传递到另一端,以及桥梁能承载多重的负荷、能跨越多远的距离。
本文将深入剖析 MCP 的三种传输机制:stdio、SSE、Streamable HTTP,理解它们的技术原理、架构差异,以及为什么官方在 2025 年 3 月用 Streamable HTTP 替代了传统的 HTTP+SSE 方案。
一、MCP 传输层概览
1.1 消息格式:JSON-RPC 2.0
无论使用哪种传输方式,MCP 的消息格式统一采用 JSON-RPC 2.0:
// 请求示例
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 1,
"params": {
"name": "read_file",
"arguments": {"path": "/src/main.py"}
}
}
// 响应示例
{
"jsonrpc": "2.0",
"id": 1,
"result": {"content": "print('Hello World')"}
}
关键规则:
- 所有消息必须使用 UTF-8 编码
- 消息使用换行符
\n分隔(不得包含嵌入式换行) - 这是三种传输方式的共同基础
1.2 三种传输方式的发展脉络

二、stdio 传输:最简单直接
2.1 工作原理
stdio(标准输入输出)传输是 MCP 最基础的通信方式,通过进程间通信实现:
- 客户端将 MCP Server 作为子进程启动
- Server 从
stdin读取 JSON-RPC 消息 - Server 通过
stdout返回响应 - Server 可选择通过
stderr输出日志
类比:就像两个人面对面交流,一个人说话(stdout),另一个人听(stdin),直接、即时。
2.2 架构流程图
┌─────────────────────────────────────────────────────────────┐
│ stdio 传输架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ stdin ┌──────────┐ stdout ┌─────┐ │
│ │ 父进程 │ ──────────► │ 子进程 │ ──────────► │父进程│ │
│ │ (客户端) │ 写入JSON │ (服务器) │ 返回JSON │(客户端│ │
│ └──────────┘ └──────────┘ └─────┘ │
│ │ │ │
│ │ 1. 启动子进程 │ │
│ ├───────────────────────►│ │
│ │ │ │
│ │ 2. 发送 JSON-RPC 请求 │ │
│ ├───────────────────────►│ │
│ │ │ 3. 处理消息 │
│ │ ├─────────────────────────►│
│ │ │ 4. 返回响应 │
│ │◄───────────────────────┤◄────────────────────────│
│ │ │ │
│ │ 5. 关闭 stdin │ │
│ ├───────────────────────►│ │
│ │ │ 6. 子进程退出 │
│ │◄───────────────────────┤ │
│ │
└─────────────────────────────────────────────────────────────┘
2.3 代码示例
客户端启动 Server:
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def run_client():
# 配置 Server 参数
server_params = StdioServerParameters(
command="python", # 启动命令
args=["-m", "my_mcp_server"], # 命令参数
env={"PYTHONPATH": "/path/to/server"} # 环境变量
)
# 建立连接
async with stdio_client(server_params) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
# 初始化会话
await session.initialize()
# 获取工具列表
tools = await session.list_tools()
print(f"可用工具: {[t.name for t in tools.tools]}")
# 调用工具
result = await session.call_tool("read_file", {"path": "/src/main.py"})
print(result.content)
Server 端实现:
from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("my-server")
@server.list_tools()
async def list_tools():
return [{"name": "read_file", "description": "读取文件"}]
async def main():
async with stdio_server() as streams:
await server.run(streams[0], streams[1])
2.4 优缺点分析
| 优势 | 劣势 |
|---|---|
| ✅ 实现最简单 | ❌ 仅限本地通信 |
| ✅ 延迟极低(无网络开销) | ❌ 无法跨机器 |
| ✅ 无需网络配置 | ❌ Server 必须与 Client 同进程组 |
| ✅ 天然安全(无网络暴露) | ❌ 难以做负载均衡 |
| ✅ 跨平台支持好 | ❌ 不支持分布式部署 |
最佳适用场景:本地开发调试、桌面应用集成、单机部署
三、SSE(HTTP+SSE):传统网络方案
3.1 工作原理
SSE(Server-Sent Events)传输基于 HTTP 长连接实现服务器向客户端的推送:
- 客户端通过 GET 请求建立 SSE 长连接
- 服务器通过这条连接单向推送消息
- 客户端通过 POST 请求发送消息到服务器
- 通过 UUID 会话标识符管理连接
架构模式:GET /sse 建立 SSE 流 + POST /messages/ 发送消息
3.2 架构流程图
┌─────────────────────────────────────────────────────────────┐
│ SSE 传输架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务器 │
│ │ │ │
│ │ 1. GET /sse (建立 SSE 长连接) │ │
│ ├────────────────────────────────────►│ │
│ │ │ │
│ │◄────────────────────────────────────┤ │
│ │ 2. HTTP 200 + Content-Type: │ │
│ │ text/event-stream │ │
│ │ (长连接保持,等待推送) │ │
│ │ │ │
│ │◄────────────────────────────────────┤ │
│ │ 3. 服务器推送消息 │ │
│ │ event: message │ │
│ │ data: {"jsonrpc":"2.0"...} │ │
│ │ │ │
│ │ 4. POST /messages/ (发送消息) │ │
│ ├────────────────────────────────────►│ │
│ │ │ │
│ │◄────────────────────────────────────┤ │
│ │ 5. HTTP 202 Accepted │ │
│ │ │ │
│ │ ... 持续双向通信 ... │ │
│ │ │ │
└─────────────────────────────────────────────────────────────┘
3.3 代码示例
Server 端配置:
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route, Mount
sse = SseServerTransport("/messages/")
async def handle_sse(request):
async with sse.connect_sse(
request.scope, request.receive, request._send
) as streams:
await app.run(streams[0], streams[1], app.create_initialization_options())
return Response()
routes = [
Route("/sse", endpoint=handle_sse, methods=["GET"]),
Mount("/messages/", app=sse.handle_post_message),
]
app = Starlette(routes=routes)
Client 端连接:
from mcp.client.sse import sse_client
async with sse_client(
url="http://localhost:8000/sse",
sse_read_timeout=300, # SSE 读取超时,根据网络环境调整
timeout=5 # HTTP 请求超时
) as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
# ... 调用工具等操作
3.4 关键缺陷:为什么被替代?
2025 年 3 月 26 日,MCP 官方用 Streamable HTTP 替代了 HTTP+SSE,原因如下:
| 问题 | 具体表现 |
|---|---|
| 连接不可恢复 | SSE 连接断开后,无法"从断点继续",只能重新开始,之前的上下文丢失 |
| 服务器压力大 | 必须维持高可用的长连接,一旦断开通信就中断 |
| 通信单向限制 | 服务器只能通过 SSE 通道推送消息,无法灵活响应 |
| 端点分离 | 需要 /sse 和 /messages/ 两个端点,架构复杂 |
这些缺陷在生产环境中会导致:
- 网络波动时用户体验差
- 服务器资源消耗大
- 难以做高可用部署
四、Streamable HTTP:新一代传输方案
4.1 核心设计思想
Streamable HTTP 不是传统意义上的"流式 HTTP",而是一种兼具以下特性的传输机制:
┌─────────────────────────────────────────────────────────────┐
│ Streamable HTTP 核心特性 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 统一端点:只需要一个 /mcp 端点,同时处理 POST 和 GET │
│ │
│ 2. 灵活响应:服务器可选择返回普通 JSON 或升级为 SSE 流 │
│ │
│ 3. 无状态支持:无需维持长连接,适合无服务器架构 │
│ │
│ 4. 会话管理:可选的会话 ID 机制,支持断点恢复 │
│ │
│ 5. 事件重放:通过 Last-Event-ID 实现断线后消息重放 │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 有状态 vs 无状态模式
无状态模式(Stateless):
请求流程:
POST /mcp (ToolListRequest)
↓
服务器直接返回 JSON 响应
↓
完成,无需保持任何状态
适用场景:简单工具查询、一次性请求
有状态模式(Stateful):
请求流程:
POST /mcp (InitializeRequest)
↓
服务器生成唯一会话 ID,通过 mcp-session-id 头返回
↓
客户端后续请求携带会话 ID
↓
服务器根据会话 ID 维护状态、实现断点恢复
适用场景:长时间交互、需要上下文的复杂任务
4.3 架构流程图
┌─────────────────────────────────────────────────────────────┐
│ Streamable HTTP 传输架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 客户端 服务器 │
│ │ │ │
│ │ 1. POST /mcp (初始化) │ │
│ ├───────────────────────────────────────►│ │
│ │ │ │
│ │◄───────────────────────────────────────┤ │
│ │ 2. 返回 mcp-session-id: abc123 │ │
│ │ (或直接返回 JSON,无会话) │ │
│ │ │ │
│ │ 3. POST /mcp (带会话 ID 发送请求) │ │
│ │ Header: mcp-session-id: abc123 │ │
│ ├───────────────────────────────────────►│ │
│ │ │ │
│ │◄───────────────────────────────────────┤ │
│ │ 4. 可选:返回 SSE 流(支持多消息) │ │
│ │ 或直接返回 JSON │ │
│ │ │ │
│ │ 5. GET /mcp (可选,启动 SSE 流) │ │
│ │ Header: Last-Event-ID: 5 │ │
│ ├───────────────────────────────────────►│ │
│ │ │ │
│ │◄───────────────────────────────────────┤ │
│ │ 6. 从事件 6 开始重放 │ │
│ │ │ │
│ │ 7. DELETE /mcp (终止会话) │ │
│ │ Header: mcp-session-id: abc123 │ │
│ ├───────────────────────────────────────►│ │
│ │ │ │
│ │◄───────────────────────────────────────┤ │
│ │ 8. 会话清理完成 │ │
│ │ │ │
└─────────────────────────────────────────────────────────────┘
4.4 会话恢复机制
这是 Streamable HTTP 最强大的特性之一:
# 客户端记录最后收到的事件 ID
last_event_id = "event-5"
# 连接断开后重连
headers = {
"mcp-session-id": "abc123",
"Last-Event-ID": last_event_id # 告诉服务器从哪里继续
}
# 服务器重放事件 6、7、8...(不会重放其他流的消息)
# 这意味着断线期间的消息不会丢失
4.5 为什么不用 WebSocket?
官方团队曾探讨是否使用 WebSocket,但最终决定暂不采用:
| 考虑因素 | WebSocket 的问题 |
|---|---|
| RPC 风格需求 | WebSocket 会引入不必要的连接维护开销 |
| 浏览器限制 | 无法像 HTTP 那样附加 Authorization 等请求头 |
| 升级复杂性 | 只有 GET 可升级为 WebSocket,POST 需要额外两步 |
| 兼容性碎片化 | 可选支持会导致客户端与服务器间兼容性组合爆炸 |
五、三种传输方式全面对比
5.1 架构差异
| 维度 | stdio | SSE | Streamable HTTP |
|---|---|---|---|
| 通信方式 | 进程间管道 | HTTP 长连接 | 灵活 HTTP 请求 |
| 端点数量 | 无 | 2 个(/sse + /messages/) |
1 个(/mcp) |
| 连接约束 | 本地进程 | 长连接必须保持 | 无状态可选 |
| 会话管理 | 无 | 通过 UUID | 可选会话 ID |
| 断点恢复 | 不支持 | 不支持 | ✅ 支持 |
5.2 性能与成本
| 维度 | stdio | SSE | Streamable HTTP |
|---|---|---|---|
| 延迟 | 最低(无网络) | 中(网络+长连接) | 中(网络) |
| 服务器资源 | 进程级 | 每连接独占 | 可共享 |
| 并发支持 | 一般 | 受长连接数限制 | 灵活扩展 |
| 负载均衡 | 不支持 | 复杂 | ✅ 容易 |
5.3 安全机制
| 安全措施 | stdio | SSE | Streamable HTTP |
|---|---|---|---|
| 环境变量过滤 | ✓ | - | - |
| DNS 重绑定保护 | - | ✓ | ✓ |
| 会话 ID 验证 | - | ✓ | ✓ |
| Origin 头验证 | - | ✓ | ✓ (必须) |
| CORS 配置 | - | ✓ | ✓ |
5.4 适用场景
| 传输方式 | 最佳适用场景 | 推荐指数 |
|---|---|---|
| stdio | 本地开发、桌面应用、单机部署 | ⭐⭐⭐⭐⭐ 简单场景首选 |
| SSE | 兼容旧客户端、浏览器集成 | ⭐⭐ 已逐步淘汰 |
| Streamable HTTP | 云端部署、分布式架构、微服务 | ⭐⭐⭐⭐⭐ 生产环境首选 |
六、实战选型指南
6.1 决策流程图
你的应用场景是什么?
│
├──► 本地开发、桌面工具
│ │
│ ▼
│ 使用 stdio ✓
│
├──► 云端部署、需要网络通信
│ │
│ ▼
│ 需要高可用、断点恢复?
│ │
│ ├── 否 → 无状态 Streamable HTTP
│ │
│ └── 是 → 有状态 Streamable HTTP
│
└──► 兼容旧版客户端
│
▼
SSE(过渡方案)
6.2 常见配置示例
本地开发(stdio):
// Claude Desktop 配置
{
"mcpServers": {
"my-local-server": {
"command": "python",
"args": ["-m", "my_server"],
"env": {"DEBUG": "true"}
}
}
}
云端部署(Streamable HTTP):
# Server 端
from mcp.server.streamable_http import StreamableHTTPServerTransport
transport = StreamableHTTPServerTransport(
mcp_session_id=uuid4().hex, # 可选,有状态模式
event_store=my_event_store, # 支持事件重放
)
# Client 端
from mcp.client.streamable_http import streamablehttp_client
# 注意:具体 API 以官方 SDK 版本为准
async with streamablehttp_client("http://api.example.com/mcp") as client:
await client.initialize()
# ...
6.3 迁移建议
如果你正在使用旧的 SSE 方案,建议逐步迁移:
- 新项目直接使用 Streamable HTTP
- 现有项目添加新端点
/mcp,保留旧端点供兼容 - 客户端优先尝试新端点,失败后 fallback 到旧端点
- 完成迁移后下线旧端点
总结
核心要点回顾
- stdio 最简单:本地进程通信,延迟最低,适合开发调试
- SSE 有缺陷:长连接脆弱、断线无法恢复,已被官方淘汰
- Streamable HTTP 是未来:灵活、健壮、支持断点恢复,生产环境首选
技术演进的意义
从 HTTP+SSE 到 Streamable HTTP 的演进,体现了 MCP 协议从"能用"到"好用"的转变:
- 无状态服务器支持 → 降低部署成本
- 统一端点 → 简化架构
- 会话管理与断点恢复 → 提升用户体验
给开发者的建议
- 本地开发用 stdio:最简单,开箱即用
- 生产环境用 Streamable HTTP:高可用、可扩展
- 理解底层原理:遇到问题时能快速定位
- 关注协议更新:MCP 还在快速发展中
参考资料
- MCP Transports 官方规范
- MCP 协议更新详解:从 HTTP+SSE 到 Streamable HTTP
- MCP 传输协议详解:Stdio、SSE 与 Streamable HTTP
- RFC: Replace HTTP+SSE with Streamable HTTP
本文是 MCP 学习系列第四篇,首发于 CSDN。理解传输协议是深入掌握 MCP 的关键一步,希望本文能帮你建立扎实的技术基础。
更多推荐



所有评论(0)