首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >MCP Server 部署形态怎么选:stdio、本地 HTTP 和远程托管的真实分界线

MCP Server 部署形态怎么选:stdio、本地 HTTP 和远程托管的真实分界线

原创
作者头像
用户11136834
发布于 2026-10-14 23:52:56
发布于 2026-10-14 23:52:56
240
举报

前言

上一篇聊可观测性,结尾留了个钩子:部署形态。这篇把它补上。

先摆一个很多教程不会明说的事实:写一个能跑的 MCP Server 很快,但"跑在哪"这个决定,会影响你后面一半的架构。同一个 todo-server,以 stdio 子进程跑和以远程托管服务跑,代码里业务逻辑部分几乎不变,但传输层、身份层、运维方式完全是两套东西。

常见的三种形态:

  • stdio:客户端把你的 server 作为子进程拉起,通过标准输入输出通信;
  • 本地 HTTP:server 作为一个常驻进程监听 127.0.0.1(或内网地址),客户端用 Streamable HTTP 连接;
  • 远程托管:部署在云上,面向不受你控制的客户端开放,走完整的认证授权。

这篇文章讲我自己的选型判断:每种形态适合什么、会引入什么新问题、以及从一种迁到另一种时真正的成本在哪。代码仍基于 TypeScript 官方 SDK。

一、stdio:默认起点,也是多数人该停下的地方

stdio 是最容易被低估的形态。很多人觉得它是"玩具模式",做点正经东西就该上 HTTP——这个判断大部分时候是反的。

stdio 的工作方式:客户端(Claude Desktop、Cursor 等)在配置里声明一条命令,连接时把你的 server 作为子进程拉起,协议消息走 stdin/stdout,日志走 stderr。官方 SDK 里就是一行:

代码语言:typescript
复制
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());

它有几个被低估的性质:

  • 零网络面。没有端口、没有 TLS、没有 CORS,攻击面天然最小。可观测性那篇说的"stdio 只写 stderr/文件",前提就是它没有 HTTP 这条路;
  • 生命周期跟着客户端走。客户端退出,子进程被回收,不需要你写守护逻辑;
  • 权限即用户权限。server 能读用户能读的文件,天然单用户,不存在"别人的会话看到我的数据";
  • 数据本地性。操作本地文件系统、跑本地命令的 server,数据本来就在这台机器上,走 stdio 没有网络往返。

所以我的判断标准很简单:操作的资源在本机、使用者就是这台机器的用户——用 stdio,别犹豫。文件系统 server、git server、本地数据库客户端,全是这一类。

它的真实边界也很清楚:一个进程服务一个客户端连接,没法被团队共享;宿主机得有你的运行时(Node/Python)和依赖;跨机器的资源(公司内网数据库、云服务 API)虽然也能从本地发起访问,但每个用户都要自己配一份凭证,凭证就散落在了每台笔记本上。

二、本地 HTTP:团队共享的第一步

当 server 变成"团队要用同一个"的时候,stdio 就不够了。典型场景:

  • 连的是共享资源——公司内部数据库、内部的构建系统,凭证只应该存在一处;
  • 运行环境重——依赖特定版本的 JVM、Python 环境、模型文件,不可能在每台机器上复刻;
  • 需要集中升级——工具逻辑改了,不想通知二十个人各更新一遍。

这时把 server 跑成一个常驻进程,监听本机或内网地址,换上 Streamable HTTP transport:

代码语言:typescript
复制
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,等于把"调用我的工具"这件事开放给了同网段所有机器。

形态一换,三个新问题跟着来了:

  1. 身份。stdio 天然单用户,HTTP 不是。可观测性篇说的"HTTP/远程部署时身份这列是命根子",就是从这里开始的:你得知道这次调用来自谁,才能谈权限和审计。哪怕都在内网,至少也要一个 API key 级别的标识;
  2. 并发与会话隔离。多个客户端同时连,transport 实例不能像无状态示例那样每个请求新建一个 server.connect——生产实现要按 session 管理连接,工具内部的共享状态(连接池、缓存)要考虑并发安全;
  3. 进程管理。从"客户端拉起、客户端回收"变成你要自己管:systemd/pm2 守护、崩溃重启、滚动升级,还有日志从 stderr 变成了要集中收集。

这三件事都不难,但每一件都是"stdio 时代不存在"的工作量。所以别为了架构图好看提前上 HTTP。

三、远程托管:把它当产品做

第三种形态是部署到云上,面向你无法控制的客户端。判断标准一句话:用户不在你的内网里。

一旦决定上远程,问题清单会跳一个数量级,核心是身份层彻底变了:

  • 认证授权。内网 API key 撑不住公网,要上 OAuth 2.1(MCP 规范对 HTTP transport 的授权就是按 OAuth 2.1 定的):动态客户端注册、token 过期刷新、scope 对应到工具粒度——权限模型篇聊的那套工具级授权,在这里才有完整的施展空间;
  • 多租户。用户 A 的会话绝不能碰到用户 B 的数据。工具实现里所有"当前用户"的语义,都要从 stdio 时代的"就是本机用户"变成从 token 里解析;
  • 限流与成本。每个工具调用背后可能是下游 API 的真金白银,没有限流的远程 MCP server 是一个敞开的计费漏洞;
  • 版本管理。客户端五花八门,你不能假设所有人都升到了最新版,工具 schema 的变更要向后兼容。

这条路线有两种走法:自己拿一个 HTTP 框架加 OAuth 资源服务器整套搭,或者用托管平台(Cloudflare Workers、各类 MCP 托管服务)把传输和认证外包出去。经验法则是:server 数量少、身份体系想自己攥着——自建;工具想快速上线给外部用户试——托管。

四、迁移的真实成本:业务逻辑不变,变的是两端

三种形态听起来像三个项目,实际迁移成本比想象中低,前提是你一开始就守住了分层。官方 SDK 把 transport 做成了可替换组件:

代码语言:typescript
复制
// stdio 形态
await server.connect(new StdioServerTransport());

// HTTP 形态——业务逻辑一行不改
await server.connect(new StreamableHTTPServerTransport({ ... }));

registerTool 注册的工具 handler、工具的 schema 和描述、业务逻辑,全部不动。动的是两端:入口的 transport 和出口的身份。

从 stdio 迁到 HTTP,真实成本排序:

  1. 身份与审计(从"默认可信"到"必须验证")——这是大头;
  2. 进程管理与日志收集——体力活但琐碎;
  3. transport 本身——反而是最便宜的一步。

反过来的教训也在这里:如果 stdio 时代就在工具 handler 里写死了"读 process.env.HOME 下的文件"这类本机假设,迁移时这些散在业务逻辑里的假设才是最贵的。写工具时把"资源从哪来"做成注入的依赖,而不是在 handler 里直接摸文件系统,三种形态就能共用同一份业务代码。

五、一张选型表

把判断压成三行:

形态

适合

别用的情形

stdio

个人工具、资源在本机、单用户

团队共享同一份凭证;重运行时环境

本地 HTTP

团队内部共享、连内网资源、集中运维

只有你自己用(纯增负担)

远程托管

面向外部用户、公开服务

内部工具(身份层成本完全白付)

三个容易踩的坑,单独说:

  • 不要跳级。"以后可能给外部用户用"不构成今天上远程托管的理由,等真的有外部用户再迁,成本主要是身份层,业务代码不动;
  • 本地 HTTP 必须显式绑定 127.0.0.1,监听 0.0.0.0 加防火墙不算防护,算侥幸;
  • 远程托管的限流要按"每个工具"设计,而不是按"每个用户"——一个用户也有可能在一轮对话里触发几十次昂贵调用。

写在最后

清单版总结:

  • 资源在本机、用户就是本机用户:stdio,不要觉得它简陋;
  • 团队要共享:本地 HTTP,绑定 127.0.0.1,补上身份、会话管理、进程守护;
  • 面向外部:远程托管,OAuth 2.1 + 多租户 + 按工具限流,一步到位;
  • 三种形态共用业务逻辑的前提:transport 可替换、资源依赖注入、"当前用户"不写死。

到这里,MCP Server 工程化这条线(安全四篇 + 测试 + 可观测性 + 部署)算是完整了。下一篇可能换个方向,聊聊 Agent 侧的话题:会话跨天之后,记忆系统怎么设计——上下文窗口装不下一个长期项目,什么该记住、什么该忘掉。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 前言
  • 一、stdio:默认起点,也是多数人该停下的地方
  • 二、本地 HTTP:团队共享的第一步
  • 三、远程托管:把它当产品做
  • 四、迁移的真实成本:业务逻辑不变,变的是两端
  • 五、一张选型表
  • 写在最后
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档