1. 为什么要把 stdio MCP 托管成 StreamableHTTP
先说清楚这件事到底在解决什么问题。MCP 服务默认最常见的形态是 stdio:客户端启动一个子进程,通过标准输入输出收发 JSON-RPC 消息。这种模式在本地单机场景非常省事,不用端口、不用鉴权、不用考虑网络。但一旦你想让多个客户端共用同一个 MCP 服务,或者想把它部署到一台服务器上给团队用,stdio 就立刻变成瓶颈——每个客户端都得自己 spawn 一份子进程,工具依赖要装好几遍,日志散落各处,进程数量随连接数线性膨胀。
StreamableHTTP 是 MCP 官方后来推出的单端点 HTTP 传输方案,一个/mcp端点同时承载请求与流式响应,比早期的 SSE 双端点(GET /sse + POST /messages)更好做反向代理、更好做鉴权、更好做水平扩展。所以「把 stdio 托管为 SE/StreamableHTTP」的本质是:保留原有 MCP 服务的业务代码不动,在外面套一层桥接网关,由网关负责 spawn 子进程、管理会话、把 stdio 帧翻译成 HTTP 流。
这套方案适合谁?我梳理了三类典型人群。第一类是手里已经有一堆写好的 stdio MCP 服务(比如自己写的 Python 脚本、uvx mcp-server-git这类现成工具),不想改代码就想让它们被远程客户端调用。第二类是要把 MCP 能力接进 AI 客户端(Claude Code、Cline、Codex 等),而这些客户端对远程 MCP 的 transport 支持越来越偏向 StreamableHTTP。第三类是做企业内部工具托管,需要统一入口、统一鉴权、统一日志。
选型上有个关键分叉必须先讲明白:代理网关模式和源码直改 transport 模式。代理网关模式不改原服务,网关 spawn 子进程跑 stdio 服务,对外暴露 SSE/StreamableHTTP,适合存量服务托管,也是本文重点。源码直改则是直接改 MCP 服务代码,去掉StdioTransport,换成 SDK 内置的StreamableHttpServerTransport,不需要子进程桥接,适合你完全掌握源码、且愿意长期维护的情况。两条路没有绝对优劣,存量多就选网关,自研新服务就选直改。
还有一个容易被忽略的前置问题:桥接出来的 HTTP 端点,客户端拿什么去连?如果只是本地127.0.0.1:8000,随便连;但一旦要跨机器、跨网络给 AI 客户端用,就需要一个统一的 API 通道来收敛 Key 和 Base URL,否则每个网关都要单独配一套凭证,管理成本会失控。这也是后面要引入 TaoToken 统一通道的原因——它不替代网关,而是给网关外面再套一层统一的接入层。
2. TaoToken 统一通道前置准备:Base URL 与 auth.json
在动手配网关之前,先把「客户端怎么连」这件事定下来。我的做法是:网关负责把 stdio 转成 StreamableHTTP,TaoToken 负责提供统一的 Base URL 和 Key 管理,客户端只认一套凭证。这样即使你后面换了网关实现(supergateway 换 mcpgate),客户端配置也不用动。
先拿 Key。打开 https://taotoken.net/api 对应的控制台入口,在 API Keys 页面创建一个新 Key,命名建议带上用途,比如mcp-gateway-prod,方便后面按环境区分。创建后立刻复制保存,页面通常只完整显示一次。
Base URL 统一用https://taotoken.net/api,注意这个地址不带任何查询参数,是纯 API 根路径。模型 ID 按你实际要调用的填,MCP 场景下如果网关本身不涉及模型调用,Model ID 可以留到客户端侧再配;但如果你的 MCP 工具内部会回调模型,就要在这里一并确定。
Codex 这类客户端读的是auth.json,路径通常在~/.codex/auth.json(不同版本可能略有差异,以你本地实际为准)。可复制配置如下:
{ "OPENAI_API_KEY": "sk-你的TaoTokenKey", "OPENAI_BASE_URL": "https://taotoken.net/api" }如果你用的是支持自定义 provider 的客户端,配置结构可能是 TOML 或 settings 片段。以常见的 TOML 形式为例:
[model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"对应的环境变量在 shell 里导出:
export TAOTOKEN_API_KEY="sk-你的TaoTokenKey"这里有个坑要提前说:Base URL 和 MCP 网关地址是两个东西。TaoToken 的https://taotoken.net/api是模型/API 通道的根地址,而你的 supergateway 暴露的是http://127.0.0.1:8000/mcp,两者不要混填。客户端配置 MCP server 时填网关地址,配置模型 provider 时填 TaoToken 地址。我见过有人把网关地址填进OPENAI_BASE_URL,结果请求全打到 MCP 端点上,报一堆解析错误。
三件套记牢:Base URL + Key + Model ID。Base URL 是https://taotoken.net/api,Key 是刚创建的sk-开头字符串,Model ID 按实际模型填。这三样在任何客户端里出现,都要成组配置,缺一个就连不通。
3. 可复制配置:supergateway 把 stdio 转成 StreamableHTTP
这一节是全文的操作核心。选 supergateway 作为主实现,理由是它 Node.js 生态、npx一键跑、同时支持 SSE 和 StreamableHTTP 双向转换、自动管理子进程生命周期,社区活跃度在同类里最高。下面给出完整可复制的命令和配置。
最简形态,把本地一个 Python 写的 stdio MCP 服务转成 StreamableHTTP,监听 8000 端口:
npx -y supergateway \ --stdio "python your_mcp_server.py" \ --outputTransport streamableHttp \ --port 8000启动后端点就是POST http://127.0.0.1:8000/mcp。注意--stdio后面跟的是一条完整命令字符串,supergateway 会把它当子进程 spawn。如果你的服务依赖虚拟环境,命令要写全路径,比如--stdio "/opt/venv/bin/python /app/mcp_server.py",否则网关进程的 PATH 可能找不到解释器。
如果你要的是旧版 SSE 模式(部分老客户端只认 SSE),把--outputTransport换成sse:
npx -y supergateway \ --stdio "python your_mcp_server.py" \ --outputTransport sse \ --port 8000SSE 模式下端点是GET /sse建流、POST /messages发消息,和 StreamableHTTP 的单端点/mcp不一样,客户端配置时别填错。
生产环境建议用 Docker 固化,避免宿主机 Node 版本漂移。一个可用的 Dockerfile 片段:
FROM node:20-slim RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY your_mcp_server.py . RUN npm install -g supergateway EXPOSE 8000 CMD ["supergateway", "--stdio", "python3 /app/your_mcp_server.py", "--outputTransport", "streamableHttp", "--port", "8000"]构建运行:
docker build -t mcp-gateway:latest . docker run -d --name mcp-gateway -p 8000:8000 mcp-gateway:latest如果你更倾向 Python 栈,mcp-proxy是成熟替代,安装后用法类似:
pip install mcp-proxy mcp-proxy --stdio "python your_mcp_server.py" --port 8000注意这个项目有两个同名仓库,认准sparfenyuk/mcp-proxy(双向桥接),另一个是客户端侧 SSE→stdio,别装错。Rust 栈想要单二进制无依赖、多后端统一网关,可以看mcpgate,用config.json定义多个 stdio 后端,对外统一暴露 SSE/StreamableHTTP,适合 K8s 部署。
选型对照表如下,方便你按团队技术栈拍板:
| 项目 | 语言 | stdio→SSE | stdio→StreamableHTTP | 打包方式 | 适合场景 |
|---|---|---|---|---|---|
| supergateway | TS/Node.js | 支持 | 支持 | npx / docker | 快速上线、容器托管 |
| mcp-proxy | Python | 支持 | 支持 | pip / docker | Python 技术栈团队 |
| mcpgate | Go | 支持 | 支持 | 单二进制 / docker | 生产 K8s、多后端 |
| mcp-stdio-bridge | Python | 支持 | 部分支持 | docker | 配置热重载、稳定性优先 |
| IBM mcpgateway.translate | Python | 支持 | 支持 | python 模块 | 企业调研、IBM 生态 |
生产托管必须补齐三件事,开源 demo 默认都不带。第一是鉴权,在网关前面加 Bearer Token 或 ApiKey 的 HTTP header 校验,绝对不要裸暴露公网。第二是 Nginx 反向代理,关键配置是关闭缓冲,否则流式响应会被攒着一次性吐出:
proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; chunked_transfer_encoding on;第三是会话与进程策略。supergateway 默认每个客户端连接 spawn 一个子进程,高并发下会进程爆炸,需要做进程池和空闲超时 kill。日志方面要捕获子进程 stderr 并采集,有条件再加 Prometheus 指标。
4. 连通性验证:从 curl 到客户端 SDK 的成功结果
配置写完不算完,必须验证。我习惯分三层验:HTTP 层、协议层、客户端层。
第一层,HTTP 层确认端点活着。StreamableHTTP 是 POST 单端点,先发一个初始化请求:
curl -i -X POST http://127.0.0.1:8000/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"}}}'成功的话你会看到 HTTP 200,响应头里Content-Type可能是text/event-stream,body 里是initialize的结果,包含serverInfo和capabilities。如果返回 404,说明路径不对(SSE 模式是/sse不是/mcp);返回 405 说明方法不对,StreamableHTTP 初始化必须 POST。
第二层,协议层确认工具列表能拉出来。带上初始化拿到的 session(部分实现需要Mcp-Session-Id头),发tools/list:
curl -s -X POST http://127.0.0.1:8000/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'成功结果里result.tools是一个数组,每个元素有name、description、inputSchema。看到你原 stdio 服务里定义的工具名出现在这里,就说明桥接链路通了——子进程被正确 spawn、stdio 帧被正确翻译、HTTP 流被正确回传。
第三层,客户端 SDK 层。用 MCP 官方 TypeScript SDK 的 StreamableHTTP transport 连:
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHttpClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const transport = new StreamableHttpClientTransport( new URL("http://127.0.0.1:8000/mcp") ); const client = new Client({ name: "verify-client", version: "1.0" }, { capabilities: {} }); await client.connect(transport); const tools = await client.listTools(); console.log(tools);connect不抛异常、listTools返回非空数组,就是完整成功。如果客户端还要回调模型,把模型 provider 指向 TaoToken 的https://taotoken.net/api,Key 用前面创建的,这样 MCP 工具调用和模型调用走两条独立通道,互不干扰。
验证时有个细节:StreamableHTTP 的响应可能是流式的,curl 不加-N有时会看起来「卡住」,其实是正常等流结束。加-N关闭缓冲能更直观看到分块返回。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来,每个都给定位思路和修法。
401 Unauthorized。最常见于两层:网关层和 TaoToken 层。如果 curl 网关就 401,说明你在网关前加了鉴权中间件但请求没带Authorization: Bearer xxx头,补上即可。如果网关通了但客户端调模型 401,检查auth.json或环境变量里的 Key 是否完整、是否过期、是否复制时带了空格。特别注意 Base URL 别写成https://taotoken.net/api/带尾斜杠,部分客户端拼接路径时会出双斜杠导致鉴权失败。
local proxy failed。这个报错通常出现在客户端尝试连本地网关时,本质是连接建立失败。排查顺序:先curl确认网关进程活着;再确认端口没被占(lsof -i :8000);再确认客户端填的地址是127.0.0.1而不是localhost(某些环境 localhost 解析到 IPv6 而网关只监听 IPv4);最后看网关日志里子进程是否 spawn 失败——如果--stdio命令里的解释器路径不对,子进程起不来,客户端侧就表现为 proxy failed。
reading choices 相关报错。这类多半是响应体不是预期 JSON,客户端解析choices字段时拿到空或 HTML。原因通常是请求打到了错误端点:把 MCP 网关地址填进了模型 provider 的 Base URL,或者反过来。回到三件套核对——Base URL 是https://taotoken.net/api,MCP 端点是http://127.0.0.1:8000/mcp,两者不能互换。另外检查Content-Type是否为application/json,缺了会被网关当表单处理。
OAuth 相关报错。部分远程 MCP 客户端会尝试走 OAuth 流程,如果你的网关没实现授权服务器,就会卡在授权跳转或 token 交换。MCP 场景下如果只是内部工具,建议直接用静态 Bearer Token 绕过 OAuth;如果确实需要 OAuth,要在网关前单独部署授权服务,别指望开源桥接工具自带。
再补几个我踩过的坑。子进程 stderr 没捕获时,MCP 服务内部报错会静默丢失,客户端只看到超时,所以务必把 stderr 重定向到日志文件。高并发下 supergateway 每连接 spawn 一个子进程,连接数一多内存就飙,要么加进程池,要么在 Nginx 层做连接数限制。还有 SSE 模式下 Nginx 不关proxy_buffering,流式消息会被攒批,客户端表现为「半天没反应然后一次性来一堆」,这个前面配置里已经给了。
排查时建议开两个终端,一个跑网关看实时日志,一个发 curl,报错和日志对得上,定位会快很多。
6. 选型判断清单与后续接入路径
最后给一份可以直接拿去对照的选型判断清单,按你的实际情况逐条打勾,基本就能定下来用哪个方案。
如果你的诉求是「最快跑通、快速原型、容器托管测试」,选 supergateway,npx一行命令,Node 环境即可,SSE 和 StreamableHTTP 都支持。如果团队是 Python 栈、希望用 pip 管理依赖,选 mcp-proxy,注意认准sparfenyuk/mcp-proxy这个双向桥接仓库。如果要上生产 K8s、需要单二进制无运行时依赖、还要一个网关管多个 stdio 后端,选 mcpgate,Rust 编译产物直接扔进镜像。如果追求配置热重载和健壮的子进程管理,看 mcp-stdio-bridge。如果是企业调研阶段、看重协议兼容性和 IBM 生态,试mcpgateway.translate。
判断维度我建议按这五项打分:存量服务数量(多则网关模式,少且自研则考虑源码直改)、团队技术栈(Node/Python/Go 对应不同工具)、部署形态(单机 docker 还是 K8s)、并发预期(高并发必须做进程池和连接限制)、鉴权要求(公网暴露必须加 Bearer Token 中间件)。五项里如果有两项以上指向同一工具,基本就定了。
接入路径上,网关跑通、curl 验证通过之后,下一步是把客户端接进来。模型调用走 TaoToken 统一通道,Base URL 用https://taotoken.net/api,Key 在控制台管理;MCP 工具调用走你本地或部署好的网关端点。两条通道分开配,互不污染。需要长期跑编码 Agent、多客户端共用的场景,可以看 Coding Plan 把配额和 Key 统一管起来;只是验证模型通不通,用模型对话页面发一条请求最快。
我实测下来,最容易翻车的不是网关本身,而是「地址填错层」——把模型 Base URL 和 MCP 端点搞混。所以配置完第一件事永远是 curl 打一发initialize,看到serverInfo再往下走。网关日志和客户端报错对着看,八成问题十分钟内能定位。