把 MCP 接进生产环境之前,先回答这五个问题
SOIT Team

MCP 把「Agent 怎么接工具」这件无聊的部分标准化了:一个 streamable HTTP 端点,一次 list_tools,工具就进了模型的可调用列表。我们内部第一次接的时候,从零到跑通只用了一个下午——这部分确实已经解决了。但把它放进生产环境的那天,安全同学问了五个问题,一个都答不上来。这篇文章讲的就是这五个问题,以及我们在 SOIT(一个把治理放在调用链中间的开源 Agent 运行时)里是怎么答的。文中每个论断都能在仓库里翻到对应文件,毕竟这类文章最容易写成 PPT 而不是软件。
第一个问题是谁能调这个工具。MCP 协议本身不管这件事:list_tools 返回什么,模型就能调什么,工具的可见性等于可调用性。在多租户、多工作区的环境里这显然不够,所以 SOIT 把 MCP server 当作 Plugin 制品来安装,而不是当作一个配置项。工具引用统一是带命名空间的 mcp_tool 加服务器名与工具名,解析时带上包含租户、工作区、用户的请求上下文。这带来两个后果:从 Agent 的视角看,来自 plugin 的工具、来自 MCP server 的工具、内置适配器的工具长得完全一样,绑定有类型也有版本;而权限检查、密钥注入、出网限制、审计、成本归集、trace 与 replay 这一整套对 MCP 工具自动生效,不需要为 MCP 再写一遍。Agent 的每个版本还带一份能力 allowlist,覆盖模型、知识库、工作流、工具、插件和 MCP server——也就是说,「这个 Agent 的 v3 能调哪些 MCP 工具」是可以 diff、可以回滚的东西,而不是运行时的一个开关。
第二个问题是凭据放在哪。绝大多数 MCP 接入示例的做法是把 bearer token 明文写进配置文件,于是它进了 git、进了日志、进了某人导到笔记本上的配置备份。SOIT 直接拒绝这种写法:构造认证头时会检查配置里有没有字面的 token 或 value 字段,有就抛错,提示凭据必须使用 secret id。只接受密钥引用,调用时经由 secrets port 解析;API key 也只支持放在 header 里,绝不放进 query string,因为 query string 会经日志和 referrer 泄漏。真实值只在这次调用期间存在于内存中,别处都没有;落进数据库、审计记录和 trace 的是一份脱敏副本,在解析密钥的同一趟里构造出来,只保留 secret id 和签名策略引用。支持三种认证类型,OAuth 走 2.1 的形态,带授权服务器发现与资源绑定令牌,使用 client_credentials 授权。有一条限制需要挑明:基于浏览器的 authorization_code 流程没有实现,因为 SOIT 是以自己的身份调用 MCP server,而不是代表一个正坐在浏览器前的用户。
第三个问题最该让人担心:它能连到哪里去。MCP server 是一个替你发起网络请求的东西——把 URL 放进工具参数,它就会去取。这类攻击的经典形态,就是让它去访问持有临时凭据的云元数据地址。SOIT 的出网策略是 deny-by-default,分三层。第一层是域名策略,把目标域名同时匹配租户级和工作区级的 allowlist 与 blocklist,blocklist 优先;allowlist 默认为空,也就是在你明确添加之前什么都不放行,而且如果策略查询本身抛异常,答案是拒绝而不是放行。fail-closed 不是一句口号,它就是你在每个 except 分支里实际写了什么。第二层是解析后的逐地址校验,因为 DNS rebinding 能让一个在 allowlist 里的域名解析到回环或内网地址:守卫会真的去解析域名,并要求返回的每一个地址都是公网可路由的,只要有一个不是就拒绝整个请求;同一趟还关掉了非 http/https 协议、带 userinfo 的 URL,并把 DNS 失败当作拒绝而不是重试。第三层是逐跳授权,因为一个通过了前两层的 URL 仍然可以返回一个指向你内网的 302。所以授权挂在出站 HTTP 客户端的请求事件钩子上,真正发出去的每一个请求都要过检查,重定向那几跳也在内,并且默认不跟随重定向。MCP 适配器用的就是这个客户端来建会话,因此初始化、list_tools 和每一次 call_tool 都落在这些约束之内。
第四个问题是事后能不能查清发生了什么。工具调用是 Agent 唯一真正产生副作用的地方:模型说错话可以再问一次,而一个改了生产库某一行的工具,是真的改了。SOIT 把每次工具调用作为一次 run 的一个 step 持久化,每次调用写两份证据。网关审计在请求侧记录工具引用、脱敏后的参数和出网判定,在响应侧记录成功与否、结果类型、元数据和错误;失败路径同样会写,而且 except 分支做的第一件事就是把这条审计记录发出去——这恰恰是最容易被漏掉、也最需要的那一条。与之并行的 step 指标记录延迟、成功标志、摘要化的参数与结果、错误码与错误详情。同一次调用还会写一条成本记录,带上供应商与来源端口,于是「这个 Agent 的 MCP 工具这个月花了多少」是一条可以按 Agent、工作流、工具和来源类型下钻的查询。再往上是一条 OpenTelemetry span,带租户、工作区、run 和 step 的 id,接进你现有的 APM 即可。
第五个问题是能不能重放。Agent 调试最让人恼火的性质是它不复现:同样的输入,不一样的推理。但在工具这一层,至少可以做到确定性。每次工具调用都带一个幂等键,默认由 run id 与 tool call id 组成,并去认领一条带租约的执行记录;如果认领时发现这条记录已经完成,就直接返回缓存结果,外部工具不会被再调一次。重试策略随之改变——只要带了幂等键,重试次数就降到一次;代码里的注释比一整段解释都清楚:耐久 Agent 调用在这个边界上是 at-most-once,因为你没法假设下游每个适配器都尊重你的幂等键。少调一次好过多调一次,对写操作来说这个取舍其实不是选择题。限流与日配额也跟着一起来,按工具引用加租户、工作区、用户来计,这样一个失控的 Agent 不会烧掉整个租户的第三方 API 预算。
坦白说说它做不到什么:传输只支持 streamable HTTP,对齐 MCP SDK v1 线,2026-07-28 那版无状态协议修订还不支持;OAuth 只有 client_credentials;一键安装 MCP 工具的市场还在路线图上,今天要手动装 plugin 制品;出网 allowlist 默认为空,意味着你接的第一个 MCP server 一定会被拒绝,直到你显式加上它的域名——这是有意为之,但确实给 quickstart 多加了一步。另一个反复被问到的问题是:这和 Agent 框架是什么关系?它们不在同一层。框架回答的是「这次调用怎么编排」,运行时回答的是「这次调用以谁的身份跑的、用了谁的凭据、能碰到什么、留下了什么证据、能不能重放」。前者是你写代码时的关切,后者是代码上线之后、别人来问时的关切。你当然可以把权限检查写进框架,但那样每接一个新工具就要把治理逻辑再实现一遍;把它下沉到运行时的 port 层,MCP 工具、plugin 工具和内置工具才会走同一条路径。SOIT 是 Apache-2.0,代码都在 github.com/soit-ai/soit,quickstart 和一份二十分钟的治理演示脚本都在 docs 目录里。如果你正在把 MCP 往生产环境推,五个问题里最难的那个通常不是技术问题,而是:谁有权决定 allowlist 上放什么。