详细对比 MCP stdio(子进程继承凭证,适合本地)和 Streamable HTTP(OAuth 认证,适合托管)两种传输方式的使用场景和决策指南。
Model Context Protocol 最有趣也最让人困惑的特性之一,就是服务端代码完全不关心客户端以何种方式连接它。服务端暴露工具、资源和提示词;传输层只是底层的一个薄适配器。团队常在这里卡住,因为两种常用传输方式在运行层面几乎毫无共同点:stdio 是一个继承凭证的子进程,而 Streamable HTTP 是一个需要 OAuth 和可用率保证的托管服务。
这篇文章就是我们内部同时用两种方式部署 MCP 服务器之前,迫切需要的那份决策指南。
新服务器应实现 stdio 用于本地使用,Streamable HTTP 用于托管场景。旧版 HTTP+SSE 传输存在于旧教程中;把它当作迁移目标,而非新项目首选。
通过 stdio,MCP 客户端将你的服务器作为子进程启动,通过标准流以 JSON-RPC 协议通信。无端口、无 TLS、无登录提示:
{
"mcpServers": {
"orders": {
"command": "npx",
"args": ["-y", "@acme/orders-mcp"],
"env": {
"ORDERS_DB_URL": "postgres://localhost:5432/orders",
"NODE_ENV": "development"
}
}
}
}
这种模式天然带来的属性:
认证是环境式的。 进程继承用户的 shell 环境、CLI 令牌和钥匙串。无需 OAuth,无需客户端注册。
隔离粒度是每用户。 两个开发者各自运行两个进程,连接两个本地数据库;不存在可被破坏的共享状态。
密钥永不离开本机。 stdio 服务器读取本地文件或本地数据库时,除工具本身的操作外没有网络攻击面。
生命周期极简。 关闭客户端即终止服务器;无需部署,无需监控。
代价同样直接。没有人能共用你的服务器。无法从 CI、基于浏览器的 Agent、手机或队友的机器上调用。笔记本休眠时长时间运行的工作会中断。每个用户都需要安装运行时环境(Node、Python、JVM),除非你交付的是二进制可执行文件。
同一套工具实现挂载到 HTTP 传输上就变成了托管服务。客户端配置简化为一个 URL:
{
"mcpServers": {
"orders": {
"url": "https://mcp.example.com/orders/mcp",
"headers": {}
}
}
}
首次未认证调用返回元数据,客户端在浏览器中执行 OAuth 2.1 流程,后续 JSON-RPC 请求携带 bearer 令牌。(完整流程参见 MCP 认证:远程 MCP 服务器的 OAuth 2.1。)
托管能买到 stdio 结构性上无法提供的能力:
共享访问。 整个团队、CI 流水线、基于浏览器的 Agent 都指向同一个 URL。
数据集中化。 服务器可以访问客户端无法企及的生产数据库或内部网络。
版本控制与发布。 一次性升级工具;所有调用方立即获得新行为。
可观测性。 日志、指标、限速、审计追踪都集中在一处。
同时也带来了所有服务都有的义务:TLS、令牌生命周期、权限范围、可用率、容量规划,以及工具被允许代表哪个用户做什么事。
大多数 TypeScript MCP SDK 支持将一个服务器挂载两次,这就消除了代码库分叉的冲动:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { createServer } from "node:http";
const server = new Server(
{ name: "orders", version: "1.4.0" },
{ capabilities: { tools: {} } },
);
server.setRequestHandler(ListToolsRequestSchema, listTools);
server.setRequestHandler(CallToolRequestSchema, callTool);
if (process.env.MCP_TRANSPORT === "http") {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: randomUUID });
const http = createServer(async (req, res) => {
if (req.url?.startsWith("/mcp")) await transport.handleRequest(req, res);
else res.writeHead(404).end();
});
http.listen(8080);
} else {
await server.connect(new StdioServerTransport());
}
工具处理器中不包含任何传输特定代码。认证中间件包裹 HTTP 路径;stdio 路径没有中间件,因为它不需要。
按顺序回答这些问题;第一个"是"即为结论。
工具是否读取本地文件、git 仓库或仅存在于用户本机的本地数据库? 是则选 stdio。托管需要上传数据,而这往往是用户拒绝做的事情。
调用方是否为队友、CI 流水线、定时任务或基于浏览器的 Agent? 是则选远程。子进程无法共享。
工具是否调用私有网络内部的系统? 是则选远程,并在该网络内部托管,这样笔记本无需 VPN 就能连接十个数据库。
工具集是否稳定且团队共享,有负责人可以值班? 是则选远程。如果天天变且只有你一个人用,选 stdio。
你在原型阶段? 始终先用 stdio。这是最快获得可用 tools/list 的路径,代码后续无需重写即可迁移到 HTTP。
一个成熟的常见架构两者同时运行:工程师在开发时通过 stdio 服务器连接本地代码,而同一服务器的托管构建版本为设计评审和 CI 提供 staging 数据。生成工具所依据的 OpenAPI 规范在两种场景下完全相同。
stdio 服务器可以将所有内容保存在内存中;请求通过单个管道串行化,进程是单租户的。不要把这个假设带入 HTTP 传输:
HTTP 服务器是多租户的。 每个会话的状态必须以 MCP session ID(以及安全边界内的已认证用户)为键进行索引,绝不能以模块级变量存储。
长时间工具调用通过 SSE 在 HTTP 响应上流式推送进度,而非简单地阻塞管道。设计工具时,对于耗时超过几秒的操作应返回作业引用。
客户端会重连。 状态变更工具必须是幂等的,因为连接断开后的重试是正常现象,而非异常。
从 OpenAPI 文档生成 MCP 服务器使传输问题成本更低,因为服务器是构建产物而非手工维护的粘合代码。在本地优先的 API 工作区,你通过 stdio 对 mock 和本地服务运行它,无需凭证、无需部署。当同一规范发布到托管环境时,构建目标切换为 Streamable HTTP,OAuth 放在边缘,团队获得一个共享端点。
从规范生成服务器的细节见分步教程《将 OpenAPI 规范转为 MCP 服务器》,而用一份文档同时服务人类和 Agent 的模式在《一份规范,两种受众》中有描述。如果你希望不安装任何东西就能看到 stdio 端的表现,本地构建完全在浏览器 demo 中运行。