MCP 工具平台:server 放在哪、谁来管每一次调用

比较 Embedded stdio Server、Remote Stateless Server、Gateway-managed Server 与 Per-tool Authorised Server 四种 MCP 拓扑:server 跑在哪、服务几个客户端、认证和按工具的授权在哪个节点做、状态归谁,以及一条被注入或被劫持的调用能碰到多远、怎么收住。

MCP 把消息格式、传输方式、工具原语和 HTTP 场景下的 OAuth 授权模型都定死了,但没有定 server 跑在哪、几个客户端共用它、认证和按工具的授权在哪个节点做、一条坏调用能碰到多远。这四问就是 MCP 工具平台的架构题。

想亲手走一遍四种拓扑、注入故障再恢复,打开互动 Lab:/system-design-lab/mcp-tool-platform-architectures

有约束的设计问题

一家公司先后要接四类工具:一个开发者给自己的 IDE 加本机工具,只服务他一个人;一个面向成千上万客户端的公共工具服务,要能水平扩展,数据在后端;企业里已经在网关后面治理好的 API,要让 Copilot 和内部 Agent 当工具用并能在目录里发现;一个同时有查询和删除的 server,不同用户权限不同,删除前必须有人看过参数。每一类该用哪种拓扑?

四种 topology signature

架构谁运行 server服务几个客户端认证和授权在哪State owner适合主要代价
Embedded stdio Server用户的机器,宿主拉起一个宿主自己的权限;安装时同意server 碰到的本机资源开发者工具、个人自动化、IDE 集成以用户权限运行;恶意启动命令即任意代码;没有集中可见性
Remote Stateless Server你的服务,负载均衡后很多每个请求按本 server 的受众验 bearer token工具后端公共或多租户工具服务没有 session 就没有服务端上下文;每个请求重新认证;后端必须是唯一真相
Gateway-managed Server现有 server 或 REST API,网关在前很多网关策略(JWT、限流、IP)按 server网关的 policy store企业复用已治理的 API策略按 server 不按工具;缓冲会打断流;多一跳
Per-tool Authorised Server你的 server 作为 OAuth 资源服务器很多,各看到不同工具带 scope 的 token、条件注册、按调用检查、人确认同意与权限记录用户权限不同、有破坏性工具活动部件最多;同意页和 scope 要自己设计;注解不可信

1. Embedded stdio Server:宿主起一个子进程

规范写得很直接:「客户端把 MCP server 作为子进程启动」,消息是以换行分隔的 JSON-RPC,「server 不得向 stdout 写任何不是合法 MCP 消息的内容」,「客户端应尽可能支持 stdio」。没有网络,只有一个客户端,server 继承宿主的全部权限。

安全最佳实践把这条路的坑列得很清楚:攻击者可以「在客户端配置里放一条恶意的启动命令」,或者「把恶意载荷放进 server 本身」;支持一键配置的客户端「必须在执行命令前实现适当的同意机制」,显示「将要执行的完整命令,不截断」,并且应该「在最小默认权限的沙箱环境里执行 MCP server 命令」;打算本地运行的 server 应该「用 stdio 传输把访问限制在 MCP 客户端」,用 HTTP 时只绑 localhost 并校验 Origin,否则「攻击者可以用 DNS 重绑定从远程网站和本地 MCP server 交互」。团队共享的一份配置被改过,某个 server 的启动命令后面接了一段外传 ~/.ssh 的脚本,用户只看到「MCP server 已安装」——这就是这个架构的典型事故。

2. Remote Stateless Server:无状态副本,每个请求自带 token

Streamable HTTP 下「server 作为独立进程运行,可以处理多个客户端连接」,提供「一个同时支持 POST 和 GET 的 HTTP 端点」,可以用 SSE 流式返回,「可以在初始化时分配一个 session ID」——注意是「可以」。授权规范把 server 定义为 OAuth 资源服务器:客户端发 resource 参数把 token 绑定到这个 server,server「必须验证 access token 确实是签发给它的」,并且「不得把从 MCP 客户端收到的 token 透传」给上游 API。

无状态的价值在安全最佳实践的会话劫持一节里最清楚。「当你有多个处理 MCP 请求的有状态 HTTP server」,攻击者猜到 session id 后可以往副本 B 塞一条恶意事件,副本 A 从共享队列取出来当作服务端消息交给真客户端——如果那是一条 tools/list_changed,客户端「可能得到它并不知道已被启用的工具」;或者直接拿 session id 冒充客户端。缓解措施是「MCP server 不得用 session 做认证」,session id「必须安全、不可预测」,并「应该把 session ID 绑定到用户特定信息」,例如 <user_id>:<session_id>。做成无状态,根本没有那个共享队列;副本随便加,前提是后端撑得住,因为后端是唯一真相。另一个坑是 token 透传:副本为了省一次换 token 把收到的 Authorization 头直接带给后端,后端的日志里全是「MCP server」在调用,被盗的 token 借 server 就能读后端数据。

3. Gateway-managed Server:网关在现有 server 前加认证、限流和日志

Azure API Management 提供两种来源:「把 API Management 里管理的任何 REST API 暴露成 MCP server」,「API 操作变成 MCP 工具」;或者透传「一个 MCP 兼容的 server(例如 LangChain、LangServe、Azure Logic Apps、Azure Functions)」。策略可以配「限流和配额」、用 JWT 做「认证和授权」、「IP 过滤」和缓存,server 登记到 API Center 供发现。但文档也写明:「当前,策略应用于 MCP server 里暴露成工具的所有 API 操作」——粒度是 server,不是工具。

透传指南列了三条硬约束:外部 server 必须符合 MCP 2025-06-18 或更新版本,用 Streamable HTTP 或 SSE;支持工具和资源,不支持 prompts;以及一条醒目的警告:「不要在 MCP server 策略里通过 context.Response.Body 访问响应体。这样做会触发响应缓冲,干扰 MCP server 所需的流式行为」,全局范围的响应体日志字节数要设为 0。有人在网关的全局策略里打开了响应体日志想看看工具返回了什么,SSE 流被整体缓冲,客户端直到超时收不到任何事件,所有经过这个 server 的工具一起挂。另一个坑是粒度:同一个 server 既有查询工具也有删除工具,任何持有 JWT 的客户端都能调删除,网关的每一条策略都放行了;按工具的授权要放进 server,或者把读工具和写工具拆成不同的 server。

4. Per-tool Authorised Server:按用户注册工具,危险动作先问人

工具规范的信任模型是:「出于信任、安全和安全性的考虑,回路里应该始终有一个人能拒绝工具调用」,应用应该「展示哪些工具暴露给了模型」并「为操作向用户展示确认提示」;「客户端必须把工具注解视为不可信的,除非它们来自可信的 server」;server 必须「校验所有工具输入」「实现适当的访问控制」「对工具调用限流」「清理工具输出」。安全最佳实践补上 scope 最小化:一上来就拿到 files:*db:* 这种宽 scope 的 token「会带来横向数据访问、权限链接和难以撤销」,应该「最小的初始 scope 集合」加「通过针对性的 WWW-Authenticate scope="..." 挑战逐步提升」。Cloudflare 的文档给出了两个执行点:「在 handler 里检查会给 LLM 返回一条错误消息」,而「条件注册工具意味着 LLM 永远看不到用户无法访问的工具」,并且「不要记录或返回原始 access token」。Claude 的 MCP connector 在客户端一侧提供了补充:按工具的 allowlist 和 denylist,「构建只读助手或者想在状态变更前加一步人工确认时,推荐把写入或破坏性工具列入 denylist」。

代价是部件最多,而且注解不能信。一个第三方 server 把「删除全部记录」标成 readOnlyHint: true,客户端按注解把它归为免确认工具,模型被注入后一次调用就清空了数据;注解只当提示,由策略而不是注解决定哪些工具要人确认。另一个是 confused deputy:server 作为代理用一个静态 client_id 接第三方授权服务器,用户批准过一次后第三方设了同意 cookie,攻击者动态注册一个 redirect 指向自己的 client 并发给用户一个链接,第三方看到 cookie 跳过了同意页,授权码被重定向到攻击者。安全最佳实践要求代理 server「必须实现按客户端的同意」:维护每个用户批准过的 client_id 清单,转到第三方之前先显示自己的同意页,redirect_uri 精确匹配,state 单次有效且在同意之后才设置。OWASP LLM01 提醒的是为什么这一切必要:通过「网站或文件这类外部来源」的间接注入可以导致「在连接的系统里执行任意命令」,缓解手段是最小权限、「对特权操作实施人在回路控制」、隔离不可信内容。

四种拓扑共同的底线

  • 工具结果回到 prompt 时是不可信内容。 里面的「忽略之前的指令」只是文字,但没有人在回路里模型会照做。
  • 注解只是提示。 来自不可信 server 的一律按需要确认处理。
  • session 不是身份。 每个请求验 token 的受众,绝不用 session id 认人。
  • 到后端的 token 永远不是客户端发来的那张。 server 用自己申请的上游 token。
  • 流式必须原样穿过每一跳。 MCP 路径上不缓冲、不记录响应体。
  • 每次调用记审计:client、用户、工具、脱敏参数、scope、判定、后端;永远不记原始 token。

故障与恢复

架构故障用户看到什么恢复
Embedded stdio共享配置里的恶意启动命令「已安装」,密钥已外传完整显示命令并同意;最小权限沙箱;高亮危险模式
Embedded stdio本地 HTTP 绑 0.0.0.0 被 DNS 重绑定打到网页读走本机文件stdio;或只绑 localhost、校验 Origin、要求 token
Remote Stateless有状态变体:猜到的 session id 跨副本注入模型看到攻击者的工具无状态;或 session id 安全随机并绑定 user_id,每个请求仍验 token
Remote Stateless副本把客户端 token 原样转发给后端后端分不清是谁只接受受众是自己的 token;用自己的上游 token;后端拒绝错误受众
Remote Stateless没校验 Origin浏览器页面直接调用校验 Origin;每个连接都要认证
Gateway-managed读响应体的策略把流缓冲住工具全部超时MCP 路径不碰响应体;全局响应体日志 0 字节
Gateway-managed按 server 的策略挡不住破坏性工具只读助手删了数据按工具的授权放进 server;读写工具拆成不同 server
Per-tool Authorised客户端信了不可信 server 的 readOnlyHint没人看到的删除注解当提示;策略决定确认;只读助手 denylist 写工具
Per-tool Authorised动态注册的恶意 client 借同意 cookie 拿 token攻击者以用户身份调用自己的同意页和 client_id 清单;redirect_uri 精确匹配;state 单次

怎么选

  • 一个用户一台机器:Embedded stdio Server,显示命令、同意、沙箱,不绑 0.0.0.0。
  • 很多客户端的工具服务:Remote Stateless Server,每个请求验受众,不用 session,用自己的上游 token,后端是唯一真相。
  • 企业复用已治理的 API:Gateway-managed Server,JWT、限流、目录在网关,流式原样透传,按工具的检查放进 server。
  • 用户权限不同或有破坏性工具:Per-tool Authorised Server,最小 scope 逐步升级,条件注册,人确认,审计每次调用。
  • 无论哪种,工具结果不可信、注解不可信、session 不是身份、到后端的 token 是自己的。

回到开头的四类工具:IDE 本机工具走 Embedded stdio;公共工具服务走 Remote Stateless;企业 API 走 Gateway-managed;有删除的多用户 server 走 Per-tool Authorised。

面试时这样回答

  1. 先复述约束:几个客户端、在哪台机器、要不要复用已有 API、用户权限是否不同、有没有不可逆的工具。
  2. 说调用路径:点名图上的边,例如「宿主按配置拉起子进程,stdio 传消息」「副本按受众验 token,用自己的上游 token 调后端」「网关按 server 验 JWT 后原样转发」「按用户过滤 tools/list,破坏性工具先问人」。
  3. 说状态和规则归谁:本机文件、工具后端、策略仓库,还是同意与权限记录。
  4. 说代价与一个故障:例如客户端信了注解跳过确认会让删除无人看见,修法是注解当提示、由策略决定确认、只读助手把写工具列入 denylist。

一手证据