上一篇聊可观测性,结尾留了个钩子:部署形态。这篇把它补上。
先摆一个很多教程不会明说的事实:写一个能跑的 MCP Server 很快,但"跑在哪"这个决定,会影响你后面一半的架构。同一个 todo-server,以 stdio 子进程跑和以远程托管服务跑,代码里业务逻辑部分几乎不变,但传输层、身份层、运维方式完全是两套东西。
常见的三种形态:
127.0.0.1(或内网地址),客户端用 Streamable HTTP 连接;这篇文章讲我自己的选型判断:每种形态适合什么、会引入什么新问题、以及从一种迁到另一种时真正的成本在哪。代码仍基于 TypeScript 官方 SDK。
stdio 是最容易被低估的形态。很多人觉得它是"玩具模式",做点正经东西就该上 HTTP——这个判断大部分时候是反的。
stdio 的工作方式:客户端(Claude Desktop、Cursor 等)在配置里声明一条命令,连接时把你的 server 作为子进程拉起,协议消息走 stdin/stdout,日志走 stderr。官方 SDK 里就是一行:
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "todo-server", version: "1.0.0" },
{ capabilities: { tools: {} } },
);
await server.connect(new StdioServerTransport());它有几个被低估的性质:
所以我的判断标准很简单:操作的资源在本机、使用者就是这台机器的用户——用 stdio,别犹豫。文件系统 server、git server、本地数据库客户端,全是这一类。
它的真实边界也很清楚:一个进程服务一个客户端连接,没法被团队共享;宿主机得有你的运行时(Node/Python)和依赖;跨机器的资源(公司内网数据库、云服务 API)虽然也能从本地发起访问,但每个用户都要自己配一份凭证,凭证就散落在了每台笔记本上。
当 server 变成"团队要用同一个"的时候,stdio 就不够了。典型场景:
这时把 server 跑成一个常驻进程,监听本机或内网地址,换上 Streamable HTTP transport:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
const app = express();
app.post("/mcp", async (req, res) => {
// 生产实现需要按 sessionId 管理 transport 实例,这里从简
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式
});
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, "127.0.0.1");注意 listen 的第一个参数写 "127.0.0.1" 而不是 "0.0.0.0"——只服务本机就别监听所有网卡,这是本地 HTTP 最常见也最不该犯的错。供应链那篇说过,MCP server 的工具描述会进模型的上下文;一个监听 0.0.0.0 的内网 server,等于把"调用我的工具"这件事开放给了同网段所有机器。
形态一换,三个新问题跟着来了:
server.connect——生产实现要按 session 管理连接,工具内部的共享状态(连接池、缓存)要考虑并发安全;这三件事都不难,但每一件都是"stdio 时代不存在"的工作量。所以别为了架构图好看提前上 HTTP。
第三种形态是部署到云上,面向你无法控制的客户端。判断标准一句话:用户不在你的内网里。
一旦决定上远程,问题清单会跳一个数量级,核心是身份层彻底变了:
这条路线有两种走法:自己拿一个 HTTP 框架加 OAuth 资源服务器整套搭,或者用托管平台(Cloudflare Workers、各类 MCP 托管服务)把传输和认证外包出去。经验法则是:server 数量少、身份体系想自己攥着——自建;工具想快速上线给外部用户试——托管。
三种形态听起来像三个项目,实际迁移成本比想象中低,前提是你一开始就守住了分层。官方 SDK 把 transport 做成了可替换组件:
// stdio 形态
await server.connect(new StdioServerTransport());
// HTTP 形态——业务逻辑一行不改
await server.connect(new StreamableHTTPServerTransport({ ... }));registerTool 注册的工具 handler、工具的 schema 和描述、业务逻辑,全部不动。动的是两端:入口的 transport 和出口的身份。
从 stdio 迁到 HTTP,真实成本排序:
反过来的教训也在这里:如果 stdio 时代就在工具 handler 里写死了"读 process.env.HOME 下的文件"这类本机假设,迁移时这些散在业务逻辑里的假设才是最贵的。写工具时把"资源从哪来"做成注入的依赖,而不是在 handler 里直接摸文件系统,三种形态就能共用同一份业务代码。
把判断压成三行:
形态 | 适合 | 别用的情形 |
|---|---|---|
stdio | 个人工具、资源在本机、单用户 | 团队共享同一份凭证;重运行时环境 |
本地 HTTP | 团队内部共享、连内网资源、集中运维 | 只有你自己用(纯增负担) |
远程托管 | 面向外部用户、公开服务 | 内部工具(身份层成本完全白付) |
三个容易踩的坑,单独说:
127.0.0.1,监听 0.0.0.0 加防火墙不算防护,算侥幸;清单版总结:
127.0.0.1,补上身份、会话管理、进程守护;到这里,MCP Server 工程化这条线(安全四篇 + 测试 + 可观测性 + 部署)算是完整了。下一篇可能换个方向,聊聊 Agent 侧的话题:会话跨天之后,记忆系统怎么设计——上下文窗口装不下一个长期项目,什么该记住、什么该忘掉。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。