再见,SSE!你好,Streamable HTTP!用 TaoToken 轻松开发 Streamable HTTP MCP Server

发布时间:2026/10/3 16:39:06
再见,SSE!你好,Streamable HTTP!用 TaoToken 轻松开发 Streamable HTTP MCP Server 1. 为什么 SSE 撑不住远程 MCP Server 了如果你最近在折腾 MCP Server大概率已经踩过 SSE 的坑。SSE 全称 Server-Sent Events是 MCP 早期规范里唯一支持远程连接的传输方式。它的工作模式很直接客户端发起一个 GET 请求服务端保持这条连接不关闭然后通过这条长连接不断往客户端推送事件。客户端要发消息就另外开一个 POST 请求服务端再把响应通过那条长连接推回来。听起来没问题但真正部署到远程环境就会发现问题。SSE 要求服务端在整个连接生命周期内保持状态也就是说每个客户端连接都要占用一个持续存在的会话。连接数一多服务端的内存和连接管理压力就上来了。更麻烦的是很多云函数、Serverless 平台、反向代理层对长连接的支持并不友好超时断连、连接复用、负载均衡后的会话粘性问题一个接一个。MCP 在 3 月 26 日发布的新规范里用 Streamable HTTP 取代了 SSE。核心变化是Streamable HTTP 允许 MCP Server 自己决定是有状态还是无状态。无状态模式下每个请求都是独立的 HTTP 请求服务端不需要维护长连接也不需要会话粘性。这对远程部署来说意味着可以直接跑在普通的 HTTP 基础设施上扩容、负载均衡、冷启动都变得简单很多。这篇文章要解决的问题很具体你手上有一个基于 SSE 的 MCP Server或者你正准备新建一个想直接上 Streamable HTTP。我会用可复制的配置和命令带你从零跑通一个 Streamable HTTP MCP Server并且用 curl 实际验证 SSE 端点和 Streamable HTTP 端点的行为差异。整个过程不需要你理解 MCP 协议的全部细节跟着操作就能看到结果。适合谁看需要自建 MCP Server 的后端开发者、正在把本地 stdio 工具改造成远程服务的工程师、以及想搞清楚 Streamable HTTP 到底怎么落地的技术负责人。如果你只是想在 IDE 里用现成的 MCP 插件这篇文章的部分内容可能偏底层但验证环节对你排查连接问题同样有用。先说清楚一个概念Streamable HTTP 不是要你完全抛弃 SSE。在 Streamable HTTP 的规范里服务端仍然可以在需要流式返回时使用 SSE 格式来推送数据但连接的管理方式变了。客户端发一个 POST 请求服务端可以返回一个普通的 JSON 响应也可以返回一个 SSE 流。关键在于这个连接不需要在整个会话期间一直挂着请求结束连接就可以释放。这就是它比传统 SSE 灵活的地方。我试过把一个原本跑 SSE 的天气查询服务改成 Streamable HTTP最直观的感受是部署脚本简单了一半。原来要配置连接超时、心跳保活、会话粘性现在这些都不需要了。下面进入具体操作。2. TaoToken 前置准备与 MCP Server 项目初始化在开始写代码之前先把两件事准备好一个是模型调用侧的凭证一个是 MCP Server 的项目骨架。TaoToken 在这里的角色是提供模型调用的统一入口。你的 MCP Server 如果内部需要调用大模型能力比如让模型解析用户意图、生成工具调用参数就需要一个稳定的 API 端点。TaoToken 的 API 地址是 https://taotoken.net/api兼容常见的模型调用格式。你需要先去控制台创建一个 API Key这个 Key 后面会用在环境变量里。创建 Key 的入口在控制台的 API Keys 页面路径是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite。进去之后点新建复制生成的 Key存到一个安全的地方。注意这个 Key 只显示一次丢了就得重新建。接下来初始化 MCP Server 项目。官方推荐的方式是用 Yeoman 生成器命令很直接npm install -g yo generator-mcplatest安装完成后创建一个新项目yo mcp -n Weather MCP Server这个命令会生成一个完整的项目结构里面已经包含了 stdio 和 Streamable HTTP 两种入口文件。生成器会问你几个问题比如项目名称、描述、是否包含示例工具。对于第一次跑通流程建议保留默认的示例工具这样启动后马上就能验证。项目生成后目录结构大致是这样的weather-mcp-server/ ├── src/ │ ├── stdio.ts │ └── streamableHttp.ts ├── package.json ├── tsconfig.json └── .vscode/ └── mcp.json核心逻辑在src/streamableHttp.ts里。这个文件默认已经实现了一个 Streamable HTTP 的 MCP Server包含一个示例工具。你可以先不改代码直接构建和启动确认环境没问题。构建命令npm run build启动 Streamable HTTP 服务npm run start:streamableHttp默认情况下服务会监听一个本地端口通常是 3000 或者生成器指定的端口。启动成功后终端会输出类似Streamable HTTP MCP Server listening on port 3000的信息。这时候服务已经在跑了但还没有客户端连上来。在配置模型调用之前先确认你的 Node.js 版本。生成器要求 Node.js 18 以上推荐用 LTS 版本。可以用node -v检查。如果版本太低去 Node.js 官网下载 LTS 安装包覆盖安装即可。现在把 TaoToken 的 API Key 配到环境变量里。在项目根目录创建一个.env文件TAOTOKEN_API_KEY你的_API_Key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在src/streamableHttp.ts里读取这两个变量。如果你用的是生成器默认代码可能已经预留了模型调用的位置找到对应的初始化代码把 base URL 和 API Key 传进去。具体写法取决于你用的 SDK但核心就是两个参数baseURL指向https://taotoken.net/apiapiKey从环境变量读取。这里有一个容易忽略的点Streamable HTTP 的 MCP Server 本身不强制要求你调用模型。它只是一个工具服务端对外暴露工具接口。模型调用发生在 MCP Client 那一侧比如 IDE 里的 Agent。所以如果你只是想让 MCP Server 跑起来并被客户端调用TaoToken 的 Key 不是必须的。但如果你要在 Server 内部做模型推理比如工具本身需要调用模型那就需要配好。为了后续验证方便建议在 Server 里加一个最简单的工具比如返回当前时间的get_current_time或者返回固定天气数据的get_weather。生成器默认会带一个示例工具你可以直接用。项目初始化完成后先别急着改代码。下一步我们用 curl 直接请求端点看看 Streamable HTTP 和 SSE 在 HTTP 层面的差异。这是理解协议切换最直观的方式。3. 可复制的 Streamable HTTP 配置与启动参数这一节给出完整的配置片段和启动命令你可以直接复制到项目里用。重点是把 Streamable HTTP 的端点路径、请求方法、请求头写清楚后面验证的时候对照着看。先看 MCP Server 端的配置。在src/streamableHttp.ts里创建 Server 实例的部分通常长这样import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; import { StreamableHTTPServerTransport } from modelcontextprotocol/sdk/server/streamableHttp.js; import express from express; const app express(); app.use(express.json()); const server new McpServer({ name: weather-mcp-server, version: 1.0.0, }); // 注册工具 server.tool( get_weather, 获取指定城市的天气, { city: { type: string, description: 城市名称 }, }, async ({ city }) { return { content: [ { type: text, text: ${city} 今天晴气温 22 摄氏度, }, ], }; } ); // Streamable HTTP 端点 app.post(/mcp, async (req, res) { const transport new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // 无状态模式 }); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); app.listen(3000, () { console.log(Streamable HTTP MCP Server listening on port 3000); });这段代码的关键点有三个。第一端点路径是/mcp用 POST 方法。第二sessionIdGenerator设为undefined表示无状态模式每个请求独立处理不维护会话。第三transport.handleRequest接收请求、响应和已经解析好的 body。如果你需要流式返回比如工具执行时间较长可以改成有状态模式并让服务端返回 SSE 流。配置上把sessionIdGenerator换成一个生成唯一 ID 的函数import { randomUUID } from crypto; const transport new StreamableHTTPServerTransport({ sessionIdGenerator: () randomUUID(), });有状态模式下服务端会在响应头里返回Mcp-Session-Id客户端后续请求需要带上这个头。无状态模式则不需要。再看客户端的配置。如果你在 VS Code Insiders 里用 MCP.vscode/mcp.json的配置大概是这样{ servers: { weather-mcp-server-streamable-http: { type: streamable-http, url: http://localhost:3000/mcp } } }注意type字段写的是streamable-http不是sse。URL 直接指向/mcp端点不需要像 SSE 那样先请求一个/sse端点拿 session ID。如果你用的是 Cline 或者 Claude Code 这类工具配置方式类似核心就是 Base URL、Key、Model ID 三件套。以 Cline 的 MCP 配置为例{ mcpServers: { weather: { type: streamable-http, url: http://localhost:3000/mcp, headers: { Authorization: Bearer 你的_TaoToken_Key } } } }这里的 Authorization 头是可选的取决于你的 MCP Server 是否做了鉴权。如果 Server 内部要调用 TaoToken 的模型接口那 Key 是配在 Server 的环境变量里不是配在客户端。客户端到 Server 的鉴权是另一层可以用自定义的 token。启动命令汇总一下# 安装依赖 npm install # 构建 npm run build # 启动 Streamable HTTP 服务 npm run start:streamableHttp如果你的package.json里没有start:streamableHttp脚本手动加一条{ scripts: { build: tsc, start:streamableHttp: node dist/streamableHttp.js } }启动后服务监听在http://localhost:3000/mcp。接下来用 curl 验证。4. 用 curl 验证 SSE 与 Streamable HTTP 端点差异这一节是全文最实操的部分。我们直接用 curl 发请求对比两种端点的行为。你需要先确保 MCP Server 已经启动监听在 3000 端口。先看 Streamable HTTP 的请求。MCP 协议里客户端和服务端之间用 JSON-RPC 格式通信。一个初始化请求长这样curl -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: 2024-11-05, capabilities: {}, clientInfo: { name: curl-test, version: 1.0.0 } } }注意Accept头里同时写了application/json和text/event-stream。这是 Streamable HTTP 的一个特点客户端告诉服务端两种响应格式我都能接受你根据情况返回。如果服务端选择普通 JSON 响应返回的就是一个 JSON 对象如果选择流式返回的就是 SSE 格式的事件流。执行这条命令你会看到类似这样的响应{ jsonrpc: 2.0, id: 1, result: { protocolVersion: 2024-11-05, capabilities: { tools: {} }, serverInfo: { name: weather-mcp-server, version: 1.0.0 } } }这是一个完整的 JSON 响应请求结束后连接就关闭了。没有长连接挂着也没有 session ID 需要后续携带。现在对比 SSE 的行为。假设你有一个 SSE 版本的 MCP Server监听在 3001 端口。SSE 的初始化分两步。第一步客户端先发一个 GET 请求建立 SSE 连接curl -N http://localhost:3001/sse-N参数表示禁用缓冲让 curl 实时输出。执行后你会看到服务端返回的第一条事件event: endpoint data: /messages?sessionIdabc123这条事件告诉客户端你后续发消息要发到/messages?sessionIdabc123这个地址。注意这个 sessionId它是服务端生成的整个会话期间不变。第二步客户端用 POST 往这个地址发消息curl -X POST http://localhost:3001/messages?sessionIdabc123 \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 1, method: initialize, params: { protocolVersion: 2024-11-05, capabilities: {}, clientInfo: { name: curl-test, version: 1.0.0 } } }这个 POST 请求本身可能返回一个简单的确认真正的响应会通过第一步建立的那条 SSE 长连接推回来。也就是说你必须在另一个终端里保持curl -N那条命令不中断才能看到 initialize 的结果。这就是 SSE 和 Streamable HTTP 最核心的差异。SSE 需要两条连接配合一条长连接收事件一条短连接发消息。Streamable HTTP 只需要一条 POST 请求响应直接返回连接用完即关。再验证一下工具调用。Streamable HTTP 下调用get_weather工具curl -X POST http://localhost:3000/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d { jsonrpc: 2.0, id: 2, method: tools/call, params: { name: get_weather, arguments: { city: 北京 } } }响应会直接返回工具执行结果{ jsonrpc: 2.0, id: 2, result: { content: [ { type: text, text: 北京 今天晴气温 22 摄氏度 } ] } }整个过程一次请求完成不需要提前建立任何长连接。如果你在有状态模式下测试服务端会在响应头里返回Mcp-Session-Id。你可以用-i参数查看响应头curl -i -X POST http://localhost:3000/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -d {...}响应头里会出现Mcp-Session-Id: 550e8400-e29b-41d4-a716-446655440000后续请求需要带上这个头curl -X POST http://localhost:3000/mcp \ -H Content-Type: application/json \ -H Accept: application/json, text/event-stream \ -H Mcp-Session-Id: 550e8400-e29b-41d4-a716-446655440000 \ -d {...}无状态模式下没有这个头每个请求都是独立的。这就是为什么说 Streamable HTTP 对远程部署更友好你可以把服务跑在任意一个 HTTP 实例后面不需要会话粘性负载均衡随便配。验证到这里你应该已经能清楚看到两种协议在 HTTP 层面的区别了。SSE 是长连接加短连接的双通道模式Streamable HTTP 是单次 POST 请求模式。接下来看常见报错。5. 迁移过程中的常见报错与排查从 SSE 切到 Streamable HTTP最容易遇到的报错集中在几个地方。这一节按报错信息来排查你遇到哪个就查哪个。第一个常见报错是401 Unauthorized。这个通常出现在客户端连 MCP Server 的时候或者 Server 内部调用模型接口的时候。如果是客户端连 Server 报 401检查你的 MCP 配置里有没有带正确的 Authorization 头。如果是 Server 内部调用 TaoToken 接口报 401检查环境变量里的 API Key 是否复制完整有没有多余的空格。TaoToken 的 Key 在控制台创建后只显示一次如果怀疑 Key 有问题直接去 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 重新生成一个。第二个报错是local proxy failed或者连接被拒绝。这个多半是端口没对上。Streamable HTTP 默认监听 3000但你的客户端配置里可能写的是别的端口。用curl http://localhost:3000/mcp先确认服务本身能通。如果 curl 也连不上检查服务是否真的启动了终端有没有报错。有时候npm run build失败但没注意dist目录里还是旧代码启动的其实是旧版本。第三个报错是reading choices或者类似的字段读取错误。这个通常发生在 Server 内部调用模型接口的时候返回格式和预期不一致。检查你的 base URL 是不是https://taotoken.net/api注意结尾不要多加/v1或者别的路径除非你的 SDK 明确要求。另外检查请求体里的 model 字段是否写对模型 ID 要和 TaoToken 支持的列表一致。第四个报错是 OAuth 相关的比如OAuth callback failed或者invalid token。如果你在 MCP 客户端里配置了 OAuth 鉴权但 Server 端没有对应的处理逻辑就会报这个。Streamable HTTP 本身不强制 OAuth你可以先用简单的 Bearer Token 或者干脆不加鉴权跑通之后再补。如果确实需要 OAuth确认回调地址和 Client ID 配置正确。第五个报错是Session not found或者Invalid session ID。这个出现在有状态模式下客户端带了错误的Mcp-Session-Id或者服务端重启后 session 丢失。解决办法有两个一是改用无状态模式把sessionIdGenerator设为undefined二是确保客户端在每次 initialize 之后正确保存并携带 session ID。第六个报错是Cannot find module或者 TypeScript 编译错误。这个一般是依赖没装全或者 Node.js 版本太低。先跑npm install再确认node -v是 18 以上。如果用的是生成器默认代码检查tsconfig.json里的module和target配置推荐module设为NodeNexttarget设为ES2022。第七个报错是 curl 请求返回415 Unsupported Media Type。检查Content-Type头是不是application/json以及Accept头有没有包含application/json和text/event-stream。Streamable HTTP 对这两个头比较敏感缺一个都可能被拒。排查的时候有一个通用方法先用 curl 直接请求端点排除客户端配置的干扰。如果 curl 能通说明 Server 没问题问题在客户端配置如果 curl 也不通说明 Server 端有问题看终端日志。终端日志里通常会有具体的错误堆栈比客户端看到的报错信息详细得多。还有一个容易忽略的点Streamable HTTP 的端点路径。生成器默认用的是/mcp但有些示例代码用的是/或者/streamable-http。你的客户端配置里的 URL 必须和 Server 实际监听的路径完全一致。用curl -v可以看到请求的实际路径和响应状态码对照着排查。6. 把 MCP Server 接到实际工作流里跑通验证之后下一步是把这个 Streamable HTTP MCP Server 接到你日常用的工具里。这里给几个实际场景的接入方式。如果你用 VS Code Insiders 的 Agent Mode打开.vscode/mcp.json把之前注释掉的weather-mcp-server-streamable-http配置取消注释点击 start 按钮。Agent Mode 会自动连接这个 Server你可以在对话里直接让 Agent 调用get_weather工具。整个过程不需要手动填 URL配置文件里已经写好了。如果你用 Cline 或者类似的编码助手在 MCP 配置里加上 Server 的 URL 和类型。Cline 支持streamable-http类型配置格式和前面给的 JSON 片段一致。配好之后重启 Cline在工具列表里应该能看到get_weather。如果你要把这个 Server 部署到远程比如跑在一台云主机上把监听地址从localhost改成0.0.0.0然后通过反向代理暴露出去。Streamable HTTP 对反向代理很友好普通的 Nginx 配置就行不需要 WebSocket 升级或者特殊的超时设置。这一点比 SSE 省心很多。对于需要长期跑编码任务或者 Agent 工作流的场景建议把模型调用统一走 TaoToken 的 Coding Plan。Coding Plan 的入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite适合需要稳定模型调用的开发场景。你的 MCP Server 内部如果需要频繁调用模型用这个入口比每次单独配 Key 更省事。如果你只是想先验证模型对话能力可以用模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 快速试一下。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有完整的 API 说明和示例代码。最后说一个实际部署时的技巧。Streamable HTTP 的无状态模式虽然简单但如果你需要工具执行过程中流式返回中间结果还是要用有状态模式加 SSE 流。配置方式是在StreamableHTTPServerTransport里设置sessionIdGenerator然后在工具处理函数里用transport.send推送事件。客户端侧需要在Accept头里保留text/event-stream并且用支持流式解析的 HTTP 客户端。整个迁移过程的核心就一句话把长连接依赖去掉改成按请求处理。你的 MCP Server 代码逻辑基本不用大改主要改的是传输层的初始化和端点配置。验证的时候用 curl 直接打端点比在客户端里调试快得多。遇到报错先看终端日志再看客户端配置大部分问题都能定位到。