ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MCP 传输层选型:Streamable HTTP 相比 SSE 的优势与配置实践指南

2026/9/25 10:02:45 拓冰建站 浏览量
MCP 传输层选型:Streamable HTTP 相比 SSE 的优势与配置实践指南 1. 为什么 MCP 传输层选型会卡住你如果你正在把 MCP Server 接进 AI 工具链大概率会遇到一个绕不开的问题传输层到底选 SSE 还是 Streamable HTTP。MCP 全称 Model Context Protocol是让 AI 客户端比如 Claude Code、各类 Agent 框架和外部工具、数据源对话的协议。它本身不关心你用什么网络方式传消息但传输层选错了后面连接复用、断线重连、双向通信全都会变成坑。我最早用 SSE 跑 MCP Server 时本地测试一切正常一放到有反向代理的环境里就开始出问题连接数被限制、代理超时断开、重连后会话丢失。后来 MCP 在 2025 年 3 月引入了 Streamable HTTP 传输机制专门解决这些工程问题。它保留了 SSE 服务端推送的能力同时把通信模型简化成单一端点支持 POST 和 GET还能跑无状态模式。这篇面向需要把 MCP Server 接入 AI 工具链的开发者重点讲三件事Streamable HTTP 相比 SSE 在连接复用、断线重连、双向通信上的实际差异一份可复制的 MCP Server 传输配置骨架以及用 TaoToken 统一 Key/API 通道接入后怎么用 curl 验证连接和重连。适合已经写过或准备写 MCP Server、被 SSE 连接问题折磨过的人。2. TaoToken 前置统一 Key 与 API 通道在写传输层代码之前先把模型调用这条链路理顺。MCP Server 本身负责工具暴露但工具背后往往要调模型如果每个工具各自配一套 Key管理起来很乱。TaoToken 提供统一的 Key 和 API 通道把模型调用收敛到一个入口MCP Server 里只需要读环境变量即可。你需要先拿到一个 API Key。进入控制台创建 Key 的入口在这里控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite创建好 Key 之后把它写进环境变量不要硬编码进代码。API 基础地址是https://taotoken.net/api注意这个地址不带 UTM 参数直接用于程序请求。export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你只是想先验证模型通道是否通可以直接用模型对话页面测一下模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite这一步的意义在于MCP Server 的传输层和模型调用层是两件事但经常一起调试。先把 Key 通道确认可用后面排查传输问题时就能排除掉模型侧的干扰。接入文档在这里遇到参数问题可以对照接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite3. Streamable HTTP 与 SSE 的核心差异先把概念对齐。SSE 是 Server-Sent Events一种基于 HTTP 的单向推送技术服务器可以持续往客户端发消息但客户端发消息要走另一个 POST 端点。传统 MCP 的 HTTPSSE 模式就是两个端点一个 GET 建 SSE 流一个 POST 发消息。Streamable HTTP 把这两件事合并到一个端点/mcp上POST 用来发请求和拿响应GET 可选地建立 SSE 流接收服务端推送。连接复用上的差异最明显。SSE 模式下每个客户端要维持一条长期连接浏览器对同域名并发连接有上限企业代理也容易掐断长连接。Streamable HTTP 在有状态模式下用mcp-session-id头复用会话无状态模式下干脆不维护会话每个请求独立处理服务器只在请求期间分配资源。这对容器化和自动扩缩容友好得多。断线重连方面SSE 自带自动重连但重连后会话状态容易丢需要额外逻辑恢复。Streamable HTTP 把会话 ID 放在 HTTP 头里传递客户端在会话有效期内可以随时重连配合last-event-id还能做断点续传把断线期间错过的事件补回来。双向通信上SSE 本质是单向的客户端要发消息得另开 POST两个端点之间要同步 sessionId出错点更多。Streamable HTTP 单一端点同时处理请求和推送协议设计更紧凑错误恢复也更直观。维度SSEHTTPSSEStreamable HTTP端点数两个GET 建流 POST 发消息一个/mcp连接复用每客户端一条长连接会话头复用或无状态断线重连自动重连但会话易丢会话头重连 断点续传双向通信单向推送发消息走另一端点单端点双向无状态支持不支持支持企业代理兼容长连接易被掐断标准 HTTP兼容更好4. 可复制的 MCP Server 传输配置骨架下面这份骨架用 TypeScript SDK 写核心是单一端点同时处理 POST 和 GET并区分「复用会话」「新建会话」「无状态」三种情况。先装依赖npm init -y npm install express modelcontextprotocol/sdk npm install -D typescript ts-node types/express types/node然后写服务器。注意sessionIdGenerator返回randomUUID()是有状态模式返回undefined就是无状态模式按你的部署环境二选一。import express, { Request, Response } from express; import { Server } from modelcontextprotocol/sdk/server/index.js; import { StreamableHTTPServerTransport } from modelcontextprotocol/sdk/server/streamableHttp.js; import { randomUUID } from crypto; const server new Server( { name: streamable-http-demo, version: 1.0.0 }, { capabilities: { tools: {}, logging: {} } } ); const app express(); app.use(express.json()); const transports: Recordstring, StreamableHTTPServerTransport {}; const MCP_ENDPOINT /mcp; app.post(MCP_ENDPOINT, async (req: Request, res: Response) { const sessionId req.headers[mcp-session-id] as string | undefined; try { if (sessionId transports[sessionId]) { await transports[sessionId].handleRequest(req, res, req.body); return; } if (!sessionId req.body?.method initialize) { const transport new StreamableHTTPServerTransport({ sessionIdGenerator: () randomUUID(), // 无状态模式改成: sessionIdGenerator: () undefined }); await server.connect(transport); await transport.handleRequest(req, res, req.body); const newId transport.sessionId; if (newId) transports[newId] transport; return; } res.status(400).json({ jsonrpc: 2.0, error: { code: -32000, message: 无效会话或请求 }, id: null }); } catch (err) { console.error(处理请求出错:, err); if (!res.headersSent) { res.status(500).json({ jsonrpc: 2.0, error: { code: -32000, message: 内部错误 }, id: null }); } } }); app.get(MCP_ENDPOINT, async (req: Request, res: Response) { const sessionId req.headers[mcp-session-id] as string | undefined; if (!sessionId || !transports[sessionId]) { res.status(400).json({ jsonrpc: 2.0, error: { code: -32000, message: 无效会话ID }, id: null }); return; } await transports[sessionId].handleRequest(req, res); }); const PORT process.env.PORT || 3000; app.listen(PORT, () console.log(MCP Server 运行在 http://localhost:${PORT}${MCP_ENDPOINT}));会话超时清理是生产环境必须加的否则内存会慢慢涨。给每个会话挂一个定时器活动时重置const sessionTimeouts: Recordstring, NodeJS.Timeout {}; function setupSessionTimeout(sessionId: string, timeoutMs 30 * 60 * 1000) { sessionTimeouts[sessionId] setTimeout(() { if (transports[sessionId]) { delete transports[sessionId]; delete sessionTimeouts[sessionId]; console.log(会话 ${sessionId} 超时清理); } }, timeoutMs); } function resetSessionTimeout(sessionId: string) { if (sessionTimeouts[sessionId]) { clearTimeout(sessionTimeouts[sessionId]); setupSessionTimeout(sessionId); } }客户端侧用StreamableHTTPClientTransport连接并带上重试逻辑。指数退避比固定间隔更稳移动网络下尤其明显import { Client } from modelcontextprotocol/sdk/client/index.js; import { StreamableHTTPClientTransport } from modelcontextprotocol/sdk/client/streamableHttp.js; async function connectWithRetry(url: string, maxRetries 5, delayMs 1000) { const client new Client({ name: demo-client, version: 1.0.0 }); let retries 0; while (retries maxRetries) { try { const transport new StreamableHTTPClientTransport(new URL(url)); await client.connect(transport); console.log(连接成功); return { client, transport }; } catch (err) { retries; console.log(第 ${retries} 次重试...); await new Promise((r) setTimeout(r, delayMs)); delayMs * 1.5; } } throw new Error(达到最大重试次数); }5. 用 curl 验证连接与重连代码写完了先用 curl 确认端点行为比直接上客户端更容易定位问题。第一步发 initialize 请求注意Accept头要同时包含application/json和text/event-stream这是 Streamable HTTP 的约定curl -i -X POST http://localhost:3000/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d {jsonrpc:2.0,id:1,method:initialize,params:{protocolVersion:2025-03-26,capabilities:{},clientInfo:{name:curl-test,version:1.0.0}}}成功的话响应头里会带mcp-session-id把它记下来。响应体可能是 JSON也可能是 SSE 格式的event: message加data:行取决于服务端实现。拿到 session id 后用它发后续请求验证会话复用curl -i -X POST http://localhost:3000/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -H mcp-session-id: 你上一步拿到的ID \ -d {jsonrpc:2.0,id:2,method:tools/list,params:{}}验证重连用同一个 session id 再发一次请求如果服务端返回正常结果而不是 400说明会话在有效期内被正确复用。再模拟断线故意用一个不存在的 session id 发请求应该收到 400 和「无效会话」错误这证明服务端的会话校验生效了。curl -i -X POST http://localhost:3000/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -H mcp-session-id: 不存在的ID \ -d {jsonrpc:2.0,id:3,method:tools/list,params:{}}如果你要验证 GET 建立的 SSE 流用-N关闭 curl 缓冲能实时看到服务端推送curl -N http://localhost:3000/mcp \ -H Accept: text/event-stream \ -H mcp-session-id: 你的会话ID6. 本篇常见错排查报 406 Not Acceptable最常见的原因是Accept头没带全。Streamable HTTP 要求客户端同时接受application/json和text/event-stream只写一个会被拒。检查你的客户端或 curl 命令。initialize 之后拿不到 session id确认sessionIdGenerator返回的不是undefined。如果你开了无状态模式本来就不会有 session id这是预期行为后续请求不需要带mcp-session-id。重连后报无效会话会话可能已经超时被清理或者服务重启导致内存里的 transports 丢失。生产环境要么把会话状态外置到 Redis要么用无状态模式。另外确认 session id 是通过 HTTP 头传的不是查询参数。GET 请求一直挂起没有数据SSE 流本来就是长连接没有推送时保持打开是正常的。如果代理层有超时需要在代理配置里放宽读超时或者改用无状态模式减少长连接依赖。代理环境下连接被掐断这是 SSE 时代的老问题。Streamable HTTP 用标准 POST/GET兼容性更好但如果代理仍有限制优先考虑无状态模式让每个请求独立完成不依赖长连接。模型调用报 401检查TAOTOKEN_API_KEY环境变量是否被正确读取以及请求头里的 Authorization 格式。Key 相关操作在 API Keys 页面管理接入细节对照接入文档。7. 把传输层和模型通道接起来传输层跑通之后下一步是让 MCP Server 里的工具真正能调模型。推荐把模型调用统一走 TaoToken 的 API 通道MCP Server 只读环境变量不散落 Key。如果你要长期跑编码类 Agent用 Coding Plan 更省心Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteClaude Code 相关的接入配置在这里ClaudeCodeAnthropichttps://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewrite实际调试时我的习惯是先用 curl 把/mcp端点的 initialize、tools/list、重连三种情况各跑一遍确认传输层没问题再去接客户端和模型。传输层的坑基本都在会话管理和 Accept 头上把这两块盯住Streamable HTTP 比 SSE 省心很多。