一个端点搞定一切:深入解析 MCP 2025-03-26 新传输协议
协议背景
Streamable HTTP 是 MCP(Model Context Protocol)在 2025 年 3 月 26 日 引入的新传输协议,用于替代旧的 HTTP+SSE 传输方式。
核心设计理念
Streamable HTTP 并不是传统意义上的"流式 HTTP",而是一种混合传输机制:
普通 HTTP 请求 + 按需升级为 SSE 流 = Streamable HTTP
关键特性:
- 基于标准 HTTP 协议(POST/GET/DELETE)
- 服务端可按需将响应升级为 SSE 流
- 无强制长连接要求
- 支持有状态和无状态两种部署模式
- 统一端点设计(无需单独的
/sse端点)
协议工作原理
1. 统一端点设计
所有通信都通过一个 HTTP 端点完成(例如 /api/tools/mcp):
| HTTP 方法 | 用途 |
|---|---|
POST |
发送 JSON-RPC 请求 |
GET |
建立 SSE 长连接(接收服务端推送) |
DELETE |
终止会话 |
2. 客户端 → 服务端(POST 请求)
客户端通过 POST 请求发送 JSON-RPC 消息:
请求格式:
POST /api/tools/mcp HTTP/1.1
Accept: application/json, text/event-stream
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { ... }
}
响应方式(服务端可选):
| 场景 | 响应方式 | Content-Type |
|---|---|---|
简单请求(如 tools/list) |
直接返回 JSON | application/json |
| 复杂请求(需要流式输出) | 升级为 SSE 流 | text/event-stream |
| 纯通知/响应(无返回) | HTTP 202 Accepted | 无响应体 |
代码实现(route.ts):
export async function POST(req: NextRequest) {
// 1. 提取 sessionId(从 query 或 header)
const sessionId = url.searchParams.get('sessionId') ||
req.headers.get('Mcp-Session-Id')
// 2. 查找或创建 transport
if (!transport) {
// 新会话:创建 transport + server
transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID(),
onsessioninitialized: (newId: string) => {
saveTransport(newId, transport!)
},
})
await server.connect(transport)
}
// 3. 处理请求(自动选择 JSON 或 SSE 响应)
const webRes = await transport.handleRequest(webReq)
return new NextResponse(webRes.body, {...})
}
3. 服务端 → 客户端(SSE 流)
客户端通过 GET 请求建立 SSE 长连接,接收服务端主动推送:
请求格式:
GET /api/tools/mcp?sessionId=xxx HTTP/1.1
Accept: text/event-stream
Mcp-Session-Id: xxx
SSE 流内容:
- JSON-RPC 请求(服务端主动调用客户端能力)
- JSON-RPC 通知(如进度通知、日志通知)
代码实现(route.ts):
export async function GET(req: NextRequest) {
// 1. 验证 sessionId
if (!sessionId) return NextResponse.json({error: 'Missing Mcp-Session-Id'}, {status: 400})
// 2. 查找 transport
const transport = getTransport(sessionId)
if (!transport) return NextResponse.json({error: 'Session not found'}, {status: 404})
// 3. 建立 SSE 流
const webRes = await transport.handleRequest(webReq)
return new NextResponse(webRes.body, {...})
}
为什么需要 GET 端点?POST 不是也能返回 SSE 流吗?
这是一个常见的疑问:POST 请求本身就能返回 SSE 事件流,为什么还需要额外的 GET 端点?
核心原因是两者的角色和时序完全不同:
| 对比维度 | POST(客户端→服务端) | GET SSE(服务端→客户端) |
|---|---|---|
| 通信方向 | 客户端主动发起请求 | 服务端主动推送消息 |
| 触发方式 | 客户端发请求 → 服务端响应 | 客户端先建立连接 → 服务端随时推送 |
| 生命周期 | 响应完当前请求即关闭 | 长连接,持续接收推送 |
| 典型内容 | 对特定请求的响应(result/error) | 通知、进度、服务端发起的请求 |
GET SSE 解决的三个核心场景:
场景 1:服务端主动通知
服务端需要告诉客户端:"日志模块出问题了"
但客户端没有发请求,服务端无法主动连接客户端
→ 只能通过已建立的 GET SSE 长连接推送
在没有 GET SSE 的情况下,服务端只能被动响应,无法主动通知客户端任何事。
场景 2:多步异步操作
1. 客户端 POST 发起长时间任务(如大批量数据处理)
2. 服务端立即返回 202 Accepted(任务已接受)
3. 客户端通过 GET SSE 连接接收任务进度通知
4. 任务完成后,服务端通过 SSE 推送最终结果
此类场景中,POST 的响应已经结束,后续进度只能通过 GET SSE 接收。
场景 3:服务端反向调用客户端
服务端需要调用客户端能力(如 sampling / elicitation)
→ 服务端通过 GET SSE 通道向客户端发送 JSON-RPC 请求
→ 客户端处理后,通过 POST 返回结果
简单总结:
- POST = 客户端说"我要…",服务端回答(请求-响应模型)
- GET SSE = 服务端说"顺便告诉你…",主动推送(推送模型)
- 两者是互补关系,不是替代关系。HTTP 本身是请求-响应协议,要让服务端能主动推送,必须在客户端预先建立一条长连接通道
4. 会话管理机制
会话 ID 生成:
- 服务端在
initialize阶段生成唯一会话 ID - 通过响应头
Mcp-Session-Id返回给客户端 - 后续请求必须携带此 ID(在 header 或 query 参数中)
会话存储(session-store.ts):
// 使用 globalThis 实现跨请求、跨热重载的全局单例
const globalForMcp = globalThis as unknown as {
__mcpSessionStore?: SessionStore
}
// 保存 transport(每个会话一个 transport)
export function saveTransport(sessionId: string, transport: ...): void {
getStore().transports.set(sessionId, transport)
}
// 获取 transport
export function getTransport(sessionId: string): ... | undefined {
return getStore().transports.get(sessionId)
}
会话终止(route.ts):
export async function DELETE(req: NextRequest) {
// 1. 验证 sessionId
if (!sessionId) return NextResponse.json({error: 'Missing Mcp-Session-Id'}, {status: 400})
// 2. 查找 transport
const transport = getTransport(sessionId)
if (!transport) return NextResponse.json({error: 'Session not found'}, {status: 404})
// 3. 关闭 transport + 删除会话
await transport.close()
deleteTransport(sessionId)
return NextResponse.json({ ok: true })
}
5. 完整交互流程
客户端与 MCP 服务端交互必须遵循以下步骤,不可跳过初始化阶段直接调用工具:
┌──────────┐ ┌──────────┐
│ Client │ │ Server │
└────┬─────┘ └────┬─────┘
│ │
│ (1) initialize ────────────────────►│ 建立会话,获取 Mcp-Session-Id
│◄── ServerCapabilities + sessionId ──│
│ │
│ (2) notifications/initialized ─────►│ 通知服务端初始化完成
│ │
│ (3) tools/list ────────────────────►│ 获取可用工具列表
│◄── tools[] ─────────────────────────│
│ │
│ (4) tools/call ────────────────────►│ 调用具体工具
│◄── result / SSE stream ─────────────│
│ │
│ (5) DELETE ────────────────────────►│ 终止会话
│◄── { ok: true } ────────────────────│
│ │
(1) initialize — 建立会话
客户端发送 initialize 请求建立会话:
curl --location 'http://localhost:19410/api/tools/mcp' \
--header 'Accept: application/json, text/event-stream' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}'
成功响应中会返回服务端能力信息,响应头 Mcp-Session-Id 中包含本次会话 ID。
(2) notifications/initialized — 通知初始化完成
客户端必须发送一个无 id 字段的通知:
curl 'http://localhost:19410/api/tools/mcp' \
--header 'Content-Type: application/json' \
--header 'Mcp-Session-Id: <SESSION_ID>' \
--data '{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}'
注意:跳过此步骤直接调用
tools/list或tools/call会得到-32000 "Server not initialized"错误。
(3) tools/list — 获取工具列表
curl 'http://localhost:19410/api/tools/mcp' \
--header 'Content-Type: application/json' \
--header 'Mcp-Session-Id: <SESSION_ID>' \
--data '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}'
(4) tools/call — 调用工具
tools/call 的 params 必须包含 name 和 arguments 两个字段:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
name |
string | 是 | 要调用的工具名称 |
arguments |
object | 是 | 工具所需的参数键值对 |
正确格式:
curl 'http://localhost:19410/api/tools/mcp' \
--header 'Accept: application/json, text/event-stream' \
--header 'Content-Type: application/json' \
--header 'Mcp-Session-Id: <SESSION_ID>' \
--data '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "<工具名称>",
"arguments": {
"key1": "value1",
"key2": "value2"
}
}
}'
常见错误:
| 错误码 | 原因 | 示例 |
|---|---|---|
-32000 / Server not initialized |
未先执行 initialize + initialized | 直接调 tools/list |
-32603 / invalid_type |
params 缺少 name 或 arguments |
把工具参数直接放在 params 下 |
(5) DELETE — 终止会话
curl -X DELETE 'http://localhost:19410/api/tools/mcp' \
--header 'Mcp-Session-Id: <SESSION_ID>'
断点续传与可恢复性
Streamable HTTP 支持连接中断后恢复:
服务端:
- 为 SSE 事件添加
id字段(全局唯一) - 客户端重连时,通过
Last-Event-ID头告知最后接收的事件 ID - 服务端从该位置重放消息
示例:
GET /api/tools/mcp HTTP/1.1
Accept: text/event-stream
Last-Event-ID: event-12345
与旧协议(HTTP+SSE)对比
| 对比维度 | 旧协议(HTTP+SSE) | 新协议(Streamable HTTP) |
|---|---|---|
| 端点设计 | 需要单独的 /sse 端点 |
统一使用 /mcp 端点 |
| 连接要求 | 强制维持 SSE 长连接 | 无强制长连接,按需升级 |
| 状态支持 | 依赖长连接维持状态 | 支持无状态 + 有状态(会话 ID) |
| 连接恢复 | 中断后无法恢复 | 支持断点续传 |
| 服务端推送 | 仅能通过 /sse 通道推送 |
可在响应中升级为 SSE 流 |
| 基础设施兼容性 | 需要 SSE 支持 | 完全基于标准 HTTP |
三种典型部署场景
1. 纯无状态服务器(最简单)
- 所有请求返回标准 JSON-RPC 响应
- 无需维持会话状态
- 适合轻量级工具服务
2. 支持流式输出的无状态服务器
- 收到请求后,通过 SSE 流返回进度和结果
- 适合需要流式输出的场景(如长时间任务)
3. 有状态服务器(本项目的实现)
- 客户端首次请求时生成会话 ID
- 后续请求携带会话 ID
- 支持服务端主动推送通知/请求
- 适合复杂交互场景
安全要求
- 验证
Origin头:防范 DNS 重绑定攻击 - 本地服务绑定 127.0.0.1:不能绑定 0.0.0.0
- 所有连接必须认证
- 会话 ID 必须加密安全:使用 UUID、JWT 等
本项目实现对照
route.ts 实现了 Streamable HTTP 规范的完整功能:
| 特性 | 实现状态 |
|---|---|
| POST 方法 - JSON-RPC 请求处理 | 已实现 |
| GET 方法 - SSE 长连接 | 已实现 |
| DELETE 方法 - 会话终止 | 已实现 |
| 会话管理 - session-store.ts | 已实现 |
| Web Standard API 兼容 | 使用 WebStandardStreamableHTTPServerTransport |
核心优势总结
- 更灵活的部署:支持无状态和有状态模式
- 更好的兼容性:基于标准 HTTP,兼容所有 HTTP 基础设施
- 更低的服务端压力:无强制长连接要求
- 更强的恢复能力:支持断点续传
更多推荐


所有评论(0)