当你第一次看到这段代码时,可能会感到困惑:
typescript展开代码// server/api/mcp.post.ts
const transport = new StreamableHTTPServerTransport(...)
await transport.handleRequest(req, res, await readBody(event))
同一个 StreamableHTTPServerTransport,为什么既能处理 Cursor 的同步 HTTP 请求,又能处理 Inspector 的流式 SSE 连接?
这篇文章将揭开这个"魔法"背后的技术原理,带你理解 MCP 协议、HTTP vs Streamable HTTP 的本质区别,以及如何用一套代码优雅地支持两种模式。
JSON-RPC 是一种轻量级的远程过程调用(RPC)协议,它的核心格式非常简单:
请求示例:
json展开代码{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "get_hotest_latest_news",
  "params": {
    "id": "zhihu",
    "count": 10
  }
}
响应示例:
json展开代码{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {"type": "text", "text": "[新闻标题](链接)"}
    ]
  }
}
关键点:
method 字段指定要调用的方法id 字段关联请求和响应MCP (Model Context Protocol) 是 Anthropic 推出的协议,用于 AI 应用与外部工具/资源的通信。它基于 JSON-RPC 2.0,定义了一套标准化的方法:
initialize - 初始化连接tools/list - 列出可用工具tools/call - 调用工具logging/setLevel - 设置日志级别(可选)MCP 的设计理念:协议层与传输层分离,这意味着:
这就是为什么 MCP 可以同时支持多种传输方式!
这是最传统的 请求-响应 模式:
展开代码客户端 服务端 | | |-- POST /api/mcp -------->| (发送 JSON-RPC 请求) | | (处理请求) |<----- 200 OK ------------| (返回完整响应) | | 连接关闭 连接关闭
特点:
实际 HTTP 请求:
bash展开代码curl -X POST http://107.173.199.53:9044/api/mcp \
  -H "Content-Type: application/json" \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
响应:
json展开代码{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [...]
  }
}
这是一种 双通道 模式:
展开代码客户端 服务端 | | |-- GET /api/mcp --------->| 建立 SSE 通道(持续连接) |<==== 保持连接 ==========| (心跳、事件流) | | |-- POST /api/mcp -------->| 发送 JSON-RPC 请求 | | (不立即返回) |<==== SSE: event ========| 通过 SSE 推送响应 |<==== SSE: event ========| 可以推送多条消息 | |
特点:
SSE 通道示例:
bash展开代码curl -N http://107.173.199.53:9044/api/mcp
# 连接保持打开,服务端可以持续推送:
# data: {"jsonrpc":"2.0","id":1,"result":{...}}
#
# data: {"jsonrpc":"2.0","id":2,"result":{...}}
StreamableHTTPServerTransport 如何同时支持两种模式?让我们看看 @modelcontextprotocol/sdk 的源码逻辑(简化版):
typescript展开代码class StreamableHTTPServerTransport {
  async handleRequest(req, res, body?) {
    // 关键判断:根据 HTTP 方法选择模式
    if (req.method === 'GET') {
      // GET 请求 → 建立 SSE 通道
      return this.handleSSE(req, res)
    } else if (req.method === 'POST') {
      // POST 请求 → 处理 JSON-RPC
      return this.handleJSONRPC(req, res, body)
    }
  }
  private handleSSE(req, res) {
    // 设置 SSE 响应头
    res.setHeader('Content-Type', 'text/event-stream')
    res.setHeader('Cache-Control', 'no-cache')
    res.setHeader('Connection', 'keep-alive')
    
    // 保持连接,将响应通道存储起来
    this._sseConnection = res
    
    // 定期发送心跳防止超时
    this._heartbeat = setInterval(() => {
      res.write(': ping\n\n')
    }, 30000)
  }
  private async handleJSONRPC(req, res, body) {
    const request = JSON.parse(body)
    
    // 检查是否有活跃的 SSE 连接
    if (this._sseConnection) {
      // 有 SSE 通道 → Streamable HTTP 模式
      // 处理请求但不立即返回,而是通过 SSE 推送
      const result = await this.processRequest(request)
      this._sseConnection.write(`data: ${JSON.stringify(result)}\n\n`)
      res.status(202).end() // 202 Accepted
    } else {
      // 没有 SSE 通道 → 传统 HTTP 模式
      // 直接处理并返回结果
      const result = await this.processRequest(request)
      res.json(result)
    }
  }
}
关键点:
让我们看看你的 NewsNow 项目中的实现:
server/api/mcp.post.ts - 处理 POST 请求
typescript展开代码export default defineEventHandler(async (event) => {
  const req = event.node.req
  const res = event.node.res
  const server = getServer()
  
  // 关键:兼容不同客户端的 Accept 头
  const accept = req.headers["accept"] || req.headers["Accept"]
  if (typeof accept !== "string" || 
      !(/application\/json/i.test(accept) && /text\/event-stream/i.test(accept))) {
    // 如果客户端没有声明支持 SSE,就补上
    req.headers["accept"] = "application/json, text/event-stream"
  }
  // 创建传输层
  const transport = new StreamableHTTPServerTransport({ 
    sessionIdGenerator: undefined 
  })
  
  // 连接 MCP 服务器
  await server.connect(transport)
  
  // 让 SDK 自动判断使用哪种模式
  await transport.handleRequest(req, res, await readBody(event))
  
  return res
})
server/api/mcp.get.ts - 处理 GET 请求(建立 SSE)
typescript展开代码export default defineEventHandler(async (event) => {
  const req = event.node.req
  const res = event.node.res
  const server = getServer()
  
  const transport = new StreamableHTTPServerTransport({ 
    sessionIdGenerator: undefined 
  })
  
  await server.connect(transport)
  
  // 注意:GET 请求不传 body
  await transport.handleRequest(req, res)
  
  return res
})
Cursor 使用 HTTP 模式时:
展开代码1. Cursor 发送: POST /api/mcp Body: {"jsonrpc":"2.0","id":1,"method":"tools/list"} 2. Nitro 路由到 mcp.post.ts 3. StreamableHTTPServerTransport.handleRequest() → 检测到 POST 且无活跃 SSE → 进入传统 HTTP 模式 4. 处理 JSON-RPC 请求 → 调用 server.tool() 注册的函数 → 返回结果 5. 直接响应: Status: 200 Body: {"jsonrpc":"2.0","id":1,"result":{...}}
Inspector 使用 Streamable HTTP 模式时:
展开代码1. Inspector 建立 SSE: GET /api/mcp 2. Nitro 路由到 mcp.get.ts → StreamableHTTPServerTransport 建立 SSE 通道 → 响应头: Content-Type: text/event-stream → 连接保持打开 3. Inspector 发送请求: POST /api/mcp Body: {"jsonrpc":"2.0","id":1,"method":"tools/list"} 4. Nitro 路由到 mcp.post.ts → StreamableHTTPServerTransport.handleRequest() → 检测到活跃的 SSE 连接 → 进入 Streamable HTTP 模式 5. 处理 JSON-RPC 请求 → 调用 server.tool() 注册的函数 6. 不直接返回,而是: POST 响应: Status: 202 Accepted (空 body) SSE 推送: data: {"jsonrpc":"2.0","id":1,"result":{...}} 7. Inspector 通过 SSE 通道收到结果
你可能注意到代码中有这段"魔法":
typescript展开代码const accept = req.headers["accept"]
if (!(/application\/json/i.test(accept) && /text\/event-stream/i.test(accept))) {
  req.headers["accept"] = "application/json, text/event-stream"
}
HTTP Accept 头告诉服务端"客户端能接受什么格式的响应":
http展开代码Accept: application/json
→ 我只接受 JSON
http展开代码Accept: application/json, text/event-stream
→ 我既接受 JSON,也接受 SSE 流
StreamableHTTPServerTransport 内部会检查 Accept 头:
typescript展开代码// SDK 内部逻辑(简化)
if (!acceptHeader.includes('application/json') || 
    !acceptHeader.includes('text/event-stream')) {
  return res.status(406).send('Not Acceptable')
}
为什么要这么严格?
但实际使用中会遇到问题:
application/json*/*text/event-stream如果严格按 SDK 要求,这些客户端都会收到 406 错误!
在服务端"宽松处理",自动补全 Accept 头:
typescript展开代码// 如果客户端没有声明支持 SSE,我们替它声明
if (!accept.includes('text/event-stream')) {
  req.headers["accept"] = "application/json, text/event-stream"
}
效果:
Accept: application/json → 服务端改成 application/json, text/event-streamNitro 是一个服务端框架(Nuxt 3 的底层),特点:
server/api/xxx.ts 自动成为 /api/xxx 端点defineEventHandler 封装请求处理你的项目结构:
展开代码server/ api/ mcp.post.ts → POST /api/mcp mcp.get.ts → GET /api/mcp
Nitro 会自动:
server/api/ 目录.get.ts / .post.ts)绑定 HTTP 方法等价于 Express 中的:
javascript展开代码app.get('/api/mcp', (req, res) => { ... })
app.post('/api/mcp', (req, res) => { ... })
defineEventHandler 解析typescript展开代码export default defineEventHandler(async (event) => {
  const req = event.node.req  // 原生 Node.js IncomingMessage
  const res = event.node.res  // 原生 Node.js ServerResponse
  
  // 读取请求体
  const body = await readBody(event)
  
  // 直接操作响应
  res.setHeader('Content-Type', 'application/json')
  res.end(JSON.stringify({...}))
  
  return res
})
event 对象提供的能力:
event.node.req/res - 访问原生 Node.js 对象readBody(event) - 解析请求体(自动处理 JSON/表单)getHeader(event, 'xxx') - 获取请求头setResponseHeader(event, 'xxx', 'yyy') - 设置响应头event.node.req/res?MCP SDK 需要原生的 Node.js 请求/响应对象:
typescript展开代码await transport.handleRequest(req, res, body)
这是因为 SDK 要直接操作:
Content-Type)res.write())Nitro 的抽象层无法满足这些底层需求,所以必须"穿透"到 Node.js 层。
SSE 是 HTML5 标准,允许服务端向客户端推送数据:
HTTP 响应示例:
http展开代码HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive data: {"message": "hello"} data: {"message": "world"} : this is a comment data: multi-line data: message
关键特性:
EventSource API 接收每条消息由多行组成,以空行分隔:
展开代码field: value\n field: value\n \n
标准字段:
data: - 消息内容(必须)id: - 消息 ID(可选,用于重连时续传)event: - 事件类型(可选,默认 "message"): 开头 - 注释(可用于心跳)示例:
展开代码id: 1 event: news data: {"title": "Breaking news"} : heartbeat id: 2 data: {"title": "Another news"} data: {"description": "Details here"}
在 Streamable HTTP 模式下:
typescript展开代码// POST 请求收到后
res.status(202).end()  // 202 Accepted
HTTP 202 Accepted 的含义:
"我已经收到你的请求,正在处理,但现在不给你结果"
这完美符合 Streamable HTTP 的语义:
客户端代码示例:
javascript展开代码// 建立 SSE 连接
const eventSource = new EventSource('http://107.173.199.53:9044/api/mcp')
// 监听消息
eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data)
  console.log('Received:', data)
}
// 错误处理
eventSource.onerror = (error) => {
  console.error('SSE error:', error)
  eventSource.close()
}
// 发送请求(需要另外的 fetch)
fetch('http://107.173.199.53:9044/api/mcp', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'tools/list'
  })
})
// 响应会通过上面的 eventSource.onmessage 收到
单次请求-响应:
bash展开代码curl -X POST http://107.173.199.53:9044/api/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
  }'
预期响应:
json展开代码{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "get_hotest_latest_news",
        "description": "...",
        "inputSchema": {...}
      }
    ]
  }
}
终端 1:建立 SSE 连接
bash展开代码curl -N http://107.173.199.53:9044/api/mcp
# 连接保持打开,等待事件...
终端 2:发送 POST 请求
bash展开代码curl -X POST http://107.173.199.53:9044/api/mcp \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list"
  }'
预期:
202 Accepted展开代码data: {"jsonrpc":"2.0","id":1,"result":{...}}
查看 HTTP 详细信息:
bash展开代码curl -v http://107.173.199.53:9044/api/mcp
持续监听 SSE(带时间戳):
bash展开代码curl -N http://107.173.199.53:9044/api/mcp | \
  while IFS= read -r line; do
    echo "[$(date +%H:%M:%S)] $line"
  done
模拟 Inspector 的完整流程:
bash展开代码# 1. 在后台建立 SSE
curl -N http://107.173.199.53:9044/api/mcp > sse_output.txt &
SSE_PID=$!
# 2. 等待连接建立
sleep 1
# 3. 发送 initialize
curl -X POST http://107.173.199.53:9044/api/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
# 4. 查看 SSE 输出
sleep 1
cat sse_output.txt
# 5. 清理
kill $SSE_PID
设计理念:协议层与传输层解耦
展开代码┌─────────────────────────────────────────┐ │ MCP 协议层 │ │ (initialize, tools/list, tools/call) │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ StreamableHTTPServerTransport │ │ (智能判断使用 HTTP 还是 SSE) │ └─────────────────────────────────────────┘ ↓ ┌───────────┴───────────┐ ↓ ↓ ┌─────────┐ ┌─────────┐ │ HTTP │ │ SSE │ │ 模式 │ │ 模式 │ └─────────┘ └─────────┘
好处:
传统 REST:
展开代码POST /api/news/zhihu?count=10 → 返回新闻列表 不同的功能需要不同的端点: POST /api/news POST /api/weather POST /api/translate
MCP 方式:
展开代码POST /api/mcp {"method": "get_hotest_latest_news", "params": {"id": "zhihu"}} POST /api/mcp {"method": "get_weather", "params": {...}} POST /api/mcp {"method": "translate", "params": {...}} 所有功能共用一个端点,通过 method 区分
优势:
HTTP 模式的开销:
Streamable HTTP 模式的优势:
在 NewsNow 中的选择:
MCP SDK 会:
res.on('close') 事件)代码体现:
typescript展开代码res.on("close", () => {
  transport.close()  // 清理 SSE 状态
  server.close()     // 断开 MCP 连接
})
可以,每个 SSE 连接对应一个独立的会话:
typescript展开代码const transport = new StreamableHTTPServerTransport({ 
  sessionIdGenerator: undefined  // 自动生成唯一 session ID
})
多个客户端连接时:
WebSocket vs SSE:
| 特性 | SSE | WebSocket | 
|---|---|---|
| 双向通信 | ❌ 只能服务端→客户端 | ✅ 双向 | 
| 协议 | HTTP | 需要升级到 ws:// | 
| 防火墙穿透 | ✅ 标准 HTTP | ⚠️ 可能被拦截 | 
| 实现复杂度 | 简单 | 较复杂 | 
| 自动重连 | ✅ 浏览器内置 | ❌ 需要手动实现 | 
MCP 选择 SSE 的原因:
反向代理配置(Nginx):
nginx展开代码location /api/mcp { proxy_pass http://backend:4444; # 关键:SSE 需要禁用缓冲 proxy_buffering off; proxy_cache off; # 保持长连接 proxy_read_timeout 3600s; proxy_connect_timeout 75s; # 传递必要的头 proxy_set_header Connection ''; proxy_http_version 1.1; chunked_transfer_encoding off; }
Docker 部署:
yaml展开代码# docker-compose.yml
services:
  newsnow:
    build: .
    ports:
      - "9044:4444"
    environment:
      - NODE_ENV=production
      # 确保不设置请求超时
      - NITRO_TIMEOUT=0
MCP 是协议,不是传输
StreamableHTTPServerTransport 的智能
一套代码,两种模式
Accept 头的兼容性处理
展开代码┌─────────────────────────────────────────┐ │ Cursor / Claude Desktop / Inspector │ ← 客户端 └─────────────────────────────────────────┘ ↓ HTTP / Streamable HTTP ↓ ┌─────────────────────────────────────────┐ │ Nitro (defineEventHandler) │ ← Web 框架 │ - mcp.get.ts → GET /api/mcp │ │ - mcp.post.ts → POST /api/mcp │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ @modelcontextprotocol/sdk │ ← MCP 实现 │ - StreamableHTTPServerTransport │ │ - McpServer │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ 业务逻辑 (getServer()) │ ← 你的代码 │ - server.tool("get_hotest_latest_news")│ │ - 调用 /api/s?id=xxx │ └─────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────┐ │ 数据源 (server/sources/*.ts) │ ← 爬虫逻辑 │ - zhihu.ts, weibo.ts, ... │ └─────────────────────────────────────────┘
如果你想深入理解这套技术栈:
基础:
req/res 对象进阶:
实战:
@modelcontextprotocol/sdk 源码这种"一个接口多种模式"的设计在其他场景也很有用:
核心思想都是:用最合适的协议完成特定任务,而不是强行统一。
希望这篇文章帮你理解了"一个接口同时支持 HTTP 和 Streamable HTTP"的魔法!
如有问题或想深入讨论某个话题,欢迎交流! 🚀


本文作者:Dong
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC。本作品采用《知识共享署名-非商业性使用 4.0 国际许可协议》进行许可。您可以在非商业用途下自由转载和修改,但必须注明出处并提供原作者链接。 许可协议。转载请注明出处!