1. 为什么我要把 MCP Server 从 SSE 迁到 Streamable HTTP
如果你最近在折腾 MCP Server,大概率踩过 SSE 的坑:本地调试好好的,一放到远程服务器上,客户端一多就开始掉线;网络抖一下,整个会话就废了,得重新握手。SSE 这套传输方式在 MCP 早期确实够用,但它的设计前提是「服务端必须一直挂着一条长连接」,这就把很多本来可以很轻的场景硬生生拖成了有状态服务。
Streamable HTTP 是 MCP 新规范里用来替代 SSE 的传输方式,核心变化一句话:服务端可以自己决定是有状态还是无状态。无状态意味着每个请求自带上下文,服务端不用为每个客户端维护一条常驻连接,断线重连、多客户端并发、水平扩容这些事一下子简单了很多。对于要跑在远程、要给多个客户端同时用的 MCP Server 来说,这个差别是质变。
这篇文章我按「能跟着做」的思路来写:先讲清楚 SSE 和 Streamable HTTP 在连接保持、断线重连、并发上的实际差异,再给一个可复制的最小 Streamable HTTP MCP Server 配置和启动命令,然后把 endpoint 改到 TaoToken 的统一 Key/API 通道,最后用一次工具调用验证连通和流式返回。全程用 Node.js 生态,命令和配置都能直接抄。
适合谁看:已经写过或跑过 SSE 版 MCP Server、想迁移到 Streamable HTTP 的开发者;以及准备第一次写远程 MCP Server、不想一上来就背 SSE 长连接包袱的人。你不需要很深的网络编程背景,但至少要能跑npm命令、看得懂 JSON 配置。
先说结论,省得你往下翻:SSE 是「一条长连接撑到底」,Streamable HTTP 是「按需请求、可选流式」,后者在远程部署和多客户端场景下省心太多。下面我把差异拆开讲,再动手。
2. SSE 与 Streamable HTTP 的差异对比及迁移前准备
2.1 连接保持:长连接 vs 按需请求
SSE 的工作方式是客户端先发一个 GET 请求,服务端返回text/event-stream,然后这条连接就一直开着,服务端通过它往下推消息。MCP 协议要求在整个 connection 生命周期里,服务端必须保持这条 SSE 连接。问题在于:这条连接一旦断了,会话就没了,客户端得重新建立连接、重新初始化。
Streamable HTTP 不一样。客户端用普通的 HTTP POST 发请求,服务端可以返回单个 JSON 响应,也可以返回一个流(stream)来分块推送。关键在于:服务端不需要为每个客户端维持一条常驻连接。请求来了就处理,处理完连接就可以关。这就是「无状态」的底气。
我实测下来,最直观的感受是:SSE 版的服务在本地跑没问题,一旦放到有负载均衡的远程环境,长连接会被各种中间层掐断,排查起来很烦;Streamable HTTP 因为走的是标准请求-响应模型,中间层基本不会给你添乱。
2.2 断线重连:会话恢复的代价
SSE 断线后,客户端要重新走一遍初始化流程。如果服务端是有状态的,还得想办法把之前的会话状态找回来,否则工具调用上下文就丢了。很多 SSE 版 MCP Server 干脆不支持恢复,断了就重来。
Streamable HTTP 把「状态」变成了可选。无状态模式下,每个请求自带完整信息,断线重连就是重新发一个请求,服务端不需要记住你是谁。有状态模式下,服务端可以通过会话 ID 之类的机制关联请求,但这是可选的,不是强制的。
这个差异对远程 MCP Server 特别重要:你不需要为了保证会话不丢而去做复杂的连接保活和状态同步。
2.3 多客户端并发:负载压力的来源
SSE 每个客户端占一条长连接,并发上去了,服务端的连接数、内存、文件描述符都跟着涨。高并发下,SSE 服务端的负载是线性增长的,而且长连接本身会占用资源。
Streamable HTTP 在无状态模式下,每个请求独立处理,服务端可以用普通的无状态服务方式扩容——加实例、上负载均衡都行。并发压力被摊到一个个短请求上,而不是一堆常驻连接上。
下面这张表是我自己整理的核心差异对照,方便你快速判断要不要迁:
| 维度 | SSE | Streamable HTTP |
|---|---|---|
| 连接模型 | 长连接常驻 | 按需请求,可选流式 |
| 状态要求 | 必须 Stateful | 可选 Stateless / Stateful |
| 断线重连 | 会话易丢失,需重新初始化 | 无状态下直接重发请求 |
| 多客户端并发 | 每客户端一条长连接,负载线性增长 | 请求独立,易水平扩容 |
| 远程部署友好度 | 较低,长连接易被中间层掐断 | 较高,标准 HTTP 语义 |
| 适用场景 | 本地、单客户端、快速原型 | 远程、多客户端、生产 |
2.4 迁移前你需要准备什么
动手前确认三件事。第一,Node.js 装好,建议 LTS 版本,命令行能跑node -v和npm -v。第二,有一个能编辑 JSON 的编辑器,VS Code 就行。第三,如果你打算把 MCP Server 接到统一通道上,先去 TaoToken 拿一个 API Key,后面配置要用。
注意:迁移不是把 SSE 代码删掉重写,而是换传输层。你的工具逻辑(tool 的实现)基本不用动,改的是服务端怎么暴露这些工具、客户端怎么连上来。
3. 可复制的 Streamable HTTP MCP Server 最小配置与启动
3.1 用脚手架生成项目
最省事的办法是用 Yeoman 的 MCP 生成器。先全局装脚手架:
npm install -g yo generator-mcp@latest然后创建项目,名字随便起,我这里用 Weather MCP Server 举例:
yo mcp -n 'Weather MCP Server'生成出来的项目里,核心逻辑在src/streamableHttp.ts。这个文件默认就能跑,先不用改,我们要的是先把它启动起来,确认 Streamable HTTP 这条链路是通的。
3.2 启动命令与端口确认
构建并启动 Streamable HTTP 版本:
npm run build npm run start:streamableHttp启动后,服务端会监听一个本地端口(生成器默认配置里能看到,通常是 3000 或类似值,以你项目里的输出为准)。看到类似Streamable HTTP server listening on ...的日志,就说明服务起来了。
这一步的意义是:你有了一个标准的 Streamable HTTP MCP Server,它的 endpoint 就是后面要接到统一通道的地址。
3.3 客户端侧的最小配置片段
在 VS Code 里,MCP 客户端配置一般放在.vscode/mcp.json。生成器会给你一个模板,把 Streamable HTTP 那一项取消注释即可。一个典型的最小配置长这样:
{ "servers": { "weather-mcp-server-streamable-http": { "type": "streamable-http", "url": "http://localhost:3000/mcp" } } }这里的type是关键,写streamable-http而不是sse。url指向你服务端暴露的 MCP endpoint。保存后,客户端就能通过这个地址连上你的 MCP Server。
如果你用的是别的客户端(比如 Cline、Claude Code 之类),配置字段名可能略有不同,但三件套是一样的:传输类型、endpoint URL、以及需要鉴权时的 Key。记住这个三件套,换客户端只是改字段名。
3.4 把 endpoint 接到 TaoToken 统一通道
到这一步,你的 MCP Server 是本地的。如果你希望工具调用走统一的 Key/API 通道,方便管理和切换模型,就把请求的 Base URL 指向 TaoToken 的 API 地址,Key 用你在控制台生成的。
配置里体现为:
{ "servers": { "weather-mcp-server-streamable-http": { "type": "streamable-http", "url": "https://taotoken.net/api", "headers": { "Authorization": "Bearer 你的_TaoToken_API_Key" } } } }三件套对照一下:Base URL 是https://taotoken.net/api,Key 是你在 TaoToken 控制台生成的 API Key,Model ID 按你实际要调用的模型填。这三样凑齐,通道就通了。
提示:API Key 不要硬编码进会提交到仓库的文件里,用环境变量或本地不纳入版本管理的配置文件。
4. 验证请求:一次工具调用确认连通与流式返回
4.1 在客户端里发起调用
配置保存后,在支持 MCP 的客户端里打开 Agent 模式,找到你注册的weather-mcp-server-streamable-http,让它调用一个工具。比如问一句「查一下北京今天的天气」,客户端会通过 Streamable HTTP 把工具调用请求发到你的 endpoint。
如果一切正常,你会看到工具被调用、返回结果,整个过程是流式的——结果分块回来,而不是等全部算完才一次性返回。这就是 Streamable HTTP 的流式能力在起作用。
4.2 用 curl 直接验证 endpoint
想更底层地确认,可以直接用 curl 打你的 endpoint:
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": "tools/list", "params": {} }'这个请求会列出你 MCP Server 注册的所有工具。如果返回了工具列表的 JSON,说明服务端和传输层都没问题。注意Accept头里同时带了application/json和text/event-stream,这是 Streamable HTTP 允许服务端自己决定返回单响应还是流式响应的体现。
4.3 确认流式返回
要确认流式,可以调用一个会分块返回的工具,观察响应是不是分多次到达。在 curl 里加-N关闭缓冲:
curl -N -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": "你的工具名", "arguments": {} } }'如果看到数据是一段段出来的,而不是卡很久然后一次性吐出来,流式就通了。这一步验证通过,说明从客户端到 MCP Server 再到统一通道的整条链路都工作正常。
5. 迁移与接入常见报错排查
5.1 401 Unauthorized
最常见的就是 401。原因基本是 Key 没带对或者没带上。检查你的配置里Authorization头是不是Bearer开头,Key 有没有多余空格,以及这个 Key 是不是在 TaoToken 控制台里还有效。如果你把 endpoint 指向了 TaoToken 的 API 地址,但 Key 用的是别处的,也会 401。
5.2 local proxy failed
这个报错通常出现在客户端侧,意思是客户端尝试连本地代理或本地 endpoint 失败了。先确认你的 MCP Server 进程还在跑,端口没被占用。然后确认配置里的url写的是http://localhost:端口/mcp还是别的地址,端口对不对。如果服务重启过,端口可能变了,配置要同步改。
5.3 reading choices 相关报错
这类报错一般出现在模型返回解析阶段,提示读取choices字段失败。多半是请求发出去后返回的不是预期的模型响应格式,可能是 endpoint 配错了、或者请求被中间层拦截返回了错误页。检查 Base URL 是不是https://taotoken.net/api,以及请求头里的 Content-Type 是否正确。
5.4 OAuth 相关报错
有些客户端在连远程 MCP Server 时会尝试走 OAuth 流程。如果你没配 OAuth,却看到 OAuth 相关的报错,说明客户端以为这个 endpoint 需要 OAuth。解决办法是在配置里明确用 API Key 鉴权(Authorization头),或者确认客户端的传输类型写的是streamable-http而不是别的会触发 OAuth 的类型。
5.5 工具列表为空
服务起来了、请求也通了,但tools/list返回空。检查你的工具注册代码有没有被执行到,src/streamableHttp.ts里注册工具的路径对不对。生成器默认是能列出示例工具的,如果你改过代码,确认没把注册逻辑注释掉。
5.6 排障顺序建议
遇到问题按这个顺序查:先确认服务进程活着、端口对;再确认客户端配置的传输类型和 URL;然后确认 Key 和 Base URL;最后看具体报错信息定位。大部分问题出在前两步,别一上来就怀疑代码逻辑。
6. 把 Streamable HTTP MCP Server 用起来的下一步
到这里,你已经有了一个能跑的 Streamable HTTP MCP Server,也验证了工具调用和流式返回。接下来可以做的事:把工具逻辑换成你真正需要的(查数据库、调内部 API、跑代码都行),把服务部署到远程,让多个客户端同时连。
如果你想让工具调用走统一的 Key 和 API 通道,方便管理额度和切换模型,去 TaoToken 控制台生成 API Key,把 Base URL 指向https://taotoken.net/api,配置里带上Authorization头就行。需要长期跑编码类 Agent 的,可以看看 Coding Plan;只是想先验证模型对话的,用模型对话入口试一下最快。
我踩过的一个坑:迁移时只改了客户端配置的type,忘了服务端还是按 SSE 暴露的,结果客户端连上去一直等流,实际服务端返回的是普通响应。后来把服务端也切到 Streamable HTTP 的启动方式,两边对齐才通。所以迁移是两端的事,别只改一边。
最后留一个实用技巧:调试阶段把服务端日志级别调高,把每个请求的方法名和响应状态打出来,出问题时一眼就能看出是请求没到、还是到了但处理失败。这比对着客户端报错猜要快得多。