把 MCP 工具调用关进笼子:deny-by-default 出网、密钥不落盘、调用可重放

简介: MCP 把 Agent 接工具的成本降到一个下午,但推上生产环境前要先回答五个问题:谁能调这个工具、凭据放在哪、它能连到哪里去、出了事查不查得到、能不能重放。本文以开源 Agent 运行时 SOIT 为例逐条作答:带命名空间的工具引用与按版本能力 allowlist、拒绝明文 token 只接受 secret_id、deny-by-default 三层出网策略(域名策略、解析后逐地址 is_global 校验、逐跳授权)、成功与失败都落库的网关审计,以及以 tool:{run_id}:{tool_call_id} 为幂等键的 at-most-once 调用。每条论断均附仓库文件路径。

MCP(Model Context Protocol)把「Agent 怎么接工具」这件事标准化了。一个
streamable HTTP 端点,一次 list_tools,工具就进了模型的可调用列表。接入成本
低到什么程度?低到我们内部第一次接的时候,从零到跑通只用了一个下午。

但把它放进生产环境的那天,安全同学问了五个问题,一个都答不上来。

这篇文章讲的就是这五个问题,以及我们在 SOIT(一个开源的 Agent 运行时与治理
平台)里是怎么答的。代码都在 GitHub 上,文中每个论断我都附了文件路径,可以
直接去翻——毕竟这类文章最容易写成 PPT。

问题一:谁能调这个工具?

MCP 协议本身不管这件事。list_tools 返回什么,模型就能调什么。工具的可见性
等于可调用性。

在多租户、多工作区的环境里这显然不够。SOIT 的做法是把 MCP server 当作
Plugin 制品安装,而不是当作一个配置项:工具引用统一是
mcp_tool:{server}:{tool} 这种带命名空间的形式(server/app/adapters/tools/mcp.py
里的 parse_mcp_tool_ref),解析时带上 RequestContext——里面有 tenant_id、
workspace_id、user_id。

这带来两个后果:

  1. 从 Agent 的视角看,来自 plugin 的工具、来自 MCP server 的工具、内置适配器的
    工具,长得完全一样。绑定是有类型、有版本的。
  2. 权限检查、密钥注入、出网限制、审计、成本归集、trace、replay 这一整套,对
    MCP 工具自动生效,不需要为 MCP 单独写一遍。

Agent 的每个版本还带一份能力 allowlist——模型、知识库、工作流、工具、插件、
MCP server 都在里面。换句话说,「这个 Agent 的 v3 版本能调哪些 MCP 工具」是一个
可以 diff、可以回滚的东西,而不是运行时的一个开关。

问题二:凭据放在哪?

绝大多数 MCP 接入示例是这样写的:

{
   
  "auth": {
    "type": "bearer", "token": "sk-xxxxxxxx" }
}

配置文件里一个明文 token。它会进 git,会进日志,会进你导出的那份配置备份。

SOIT 在这里做了一个硬拒绝。_build_auth_headers 里有这么一行判断:如果
auth 配置里出现了 token 或 value 字段,直接抛错——

MCP credentials must use secret_id

只接受 secret_id,运行时再通过 SecretsPort 解析成真值。API key 同理,而且
只支持放在 header 里(放 query string 会被日志和 referrer 带走,不给这个选项)。

真值只在调用的那一瞬间存在于内存里。落库、落审计、落 trace 的是脱敏后的
副本
:ToolPolicyGateway._resolve_secrets 在解析密钥的同时生成一份 redacted
payload,只保留 secret_id 和签名策略引用,后续所有写盘路径用的都是它
(server/app/kernel/ports/tools/policy.py)。

支持三种认证类型:bearer、api_key、oauth2。OAuth 走的是 2.1 规范,带
授权服务器发现(RFC 9728、RFC 8414 / OIDC)和资源绑定令牌(RFC 8707),
用 client_credentials 授权。

这里要说清楚一个限制:我们没实现浏览器端的 authorization_code 流程。因为
SOIT 是以自己的身份去调 MCP server 的,不是代表某个正在浏览器前面的用户。
如果你的场景需要「以终端用户身份调用受保护的 MCP server」,现在这套不覆盖。

问题三:它能连到哪里去?

这是五个问题里最要命的一个。

MCP server 是你部署的,但它是一个会代你发起网络请求的东西。工具参数里塞
一个 URL,它就去请求。经典的 SSRF 场景:让它去请求 http://169.254.169.254/,
云厂商的元数据服务就在那儿,里面有临时凭据。

SOIT 的出网策略是 deny-by-default,并且分了三层:

第一层,域名策略。check_egress_policy 检查目标域名,支持租户级和工作区级
的 allowlist / blocklist,blocklist 优先于 allowlist。默认配置
enable_egress_policy: bool = True,egress_allowlist: list[str] = []——空
allowlist 意味着默认什么都不放行,你得显式加。还有一个细节:策略查询本身如果
抛异常,结果是拒绝,不是放行:

except Exception as exc:
    raise ForbiddenError(
        "Egress policy lookup failed; request denied",
        {
   "resource_ref": resource_ref},
    ) from exc

fail-closed 不是口号,是要在每个 except 分支里写对的东西。

第二层,解析后逐地址校验。光过域名不够——DNS rebinding 可以让一个通过了
allowlist 的域名解析到 127.0.0.1 或 10.0.0.x。所以 GovernedEgressGuard
在放行域名之后,会真的去解析主机名,然后对解析出的每一个地址判断
ipaddress.ip_address(address).is_global,只要有一个不是公网地址就整体拒绝
(server/app/kernel/security/egress.py)。

顺带堵掉的还有:非 http/https 协议默认拒绝;URL 里带 userinfo(
https://user:pass@host/)拒绝;DNS 解析失败拒绝而不是重试。

第三层,逐跳授权。一个通过了前两层的 URL,返回 302 跳到内网怎么办?
所以出网的 httpx 客户端是这么构造的(server/app/adapters/http/governed_client.py):

async def authorize_request(request: httpx.Request) -> None:
    await guard.authorize(ctx, resource_ref, str(request.url))

event_hooks["request"] = [authorize_request, *request_hooks]
kwargs.setdefault("follow_redirects", False)

授权挂在 httpx 的 request 事件钩子上——每一个真实发出的请求都过一遍,包括
重定向的每一跳,而不是只在调用入口检查一次。而且默认不跟随重定向。

MCP 适配器的会话就是用这个客户端建的,所以 MCP 的整条链路——初始化、
list_tools、每一次 call_tool——都在这套约束里面。

问题四:出了事查得到吗?

工具调用是 Agent 唯一真正产生副作用的地方。模型说错话可以重问,工具把生产库的
一行数据改了就是改了。

SOIT 把工具调用当作 run 的一个 step 落库,每次调用写两条证据:

  • 网关审计:log_gateway_request 记 gateway_type="tool",请求侧记
    tool_ref、脱敏参数、出网决策(allow / deny + 目标 URL),响应侧记成功与否、
    结果类型、metadata、错误。失败路径同样写——except 分支里第一件事就是补
    一条 audit,这条经常被忘掉,但恰恰是排查时最需要的那条。
  • step 指标:延迟、成功标志、工具参数与结果摘要、错误码与错误详情。

同一次调用还会写一条成本记录,billing_basis="requests"、带 provider 和
source_port="tools"——所以「这个月哪个 Agent 的哪个 MCP 工具花了多少」是可以
按 agent / workflow / tool / 来源(source_kind=plugin | mcp | builtin)下钻的。

再加一层 OpenTelemetry span soit.tool.invoke,属性里带 tenant / workspace /
run / step 的 id,接你自己的 APM。

问题五:能重放吗?

Agent 排障最烦的一点是不可复现:同样的输入,模型这次这么想,下次那么想。

工具这一层至少可以做到确定性。SOIT 的每次工具调用都带一个幂等键,默认是
tool:{run_id}:{tool_call_id},通过一个带租约的执行记录来 claim。如果这次
claim 命中的是一条已经完成的记录,直接返回缓存的响应,不会真的再调一次
外部工具。

重试策略也跟着变。看这段注释就懂了:

max_retries=1 if kwargs.get("idempotency_key") else self.max_retries,

Durable Agent calls are at-most-once at this boundary.
Not every downstream adapter can honor an idempotency key.

在这个边界上是 at-most-once,因为不能假设下游 MCP server 认得你的幂等键。宁可
少调一次,不能多调一次——对写操作来说这个取舍没得选。

配套还有速率限制和日配额,key 按 tool_ref + tenant + workspace(+ user)分组,
所以一个 Agent 跑飞了不会把整个租户的第三方 API 额度打爆。

坦白局

按惯例说一下这套东西不能做什么:

  • MCP 传输只支持 streamable HTTP,适配的是 MCP SDK v1 线;2026-07-28 那版
    stateless 协议修订还没支持。
  • OAuth 只有 client_credentials,没有 authorization_code(原因见问题二)。
  • 一键安装 MCP 工具的市场还在 roadmap 上,现在是手动装 Plugin 制品。
  • 出网策略的默认 allowlist 是空的——这意味着你第一次接 MCP server 一定会被拒,
    必须显式加域名。这是刻意的,但确实会让 quickstart 多一步。

为什么这些东西不应该在 Agent 框架里做

顺便回答一个经常被混淆的问题:这跟 LangChain 之类的框架是什么关系?

不是一层东西。框架解决的是「怎么把这次调用编排出来」,运行时解决的是「这次
调用在谁的身份下、用谁的密钥、能连到哪、留下什么证据、能不能重放」。前者是
写代码时的关注点,后者是代码上线之后别人问你的关注点。

框架里当然也能塞权限检查,但那样每接一个新工具就要重写一遍治理逻辑。把它下沉
到运行时的端口层(port)之后,MCP 工具、插件工具、内置工具走的是同一条路径——
这也是为什么前面说「不需要为 MCP 单独写一遍」。

来试试

SOIT 是 Apache-2.0 的,代码全在 GitHub:

  • 仓库:github.com/soit-ai/soit
  • 快速开始:仓库里的 docs/quickstart.md(有中文版)
  • 治理演示:docs/governance-demo.md——一个 20 分钟的本地跑通脚本,把权限、
    密钥、调用审计、成本归集、重放、回归这六件事挨个演示一遍,跑完输出一份机器
    可读的报告

如果你正在把 MCP 往生产环境推,欢迎来 issue 区聊聊你被卡在哪一步。上面五个
问题里,最难的那个通常不是技术问题,是「谁来定这份 allowlist」。


利益相关:我是 SOIT 的维护者。

目录
相关文章
|
12月前
|
数据采集 机器学习/深度学习 运维
量化合约系统开发架构入门
量化合约系统核心在于数据、策略、风控与执行四大模块的协同,构建从数据到决策再到执行的闭环工作流。强调可追溯、可复现与可观测性,避免常见误区如重回测轻验证、忽视数据质量或滞后风控。初学者应以MVP为起点,结合回测框架与实时风控实践,逐步迭代。详见相关入门与实战资料。
|
24天前
|
人工智能 JSON 自然语言处理
全网爆火Jev模型完整解析:实战测评+保姆级落地教程
最近有一款特殊AI模型在开发者圈子刷屏,它摒弃传统大模型擅长的对话聊天能力,专注做高速结构化决策,它就是TypeSafe AI推出的Jev模型。该模型由ChatGPT共同发明人Diogo Almeida主导研发,定位为**System One Model(系统一模型)**,对标人类大脑快速直觉判断的思维模式,在响应延迟、调用成本、结构化输出稳定性上相比传统生成式大模型有着巨大差异。本文会完整拆解Jev底层原理、三大核心原语能力、适用业务场景,同时提供可直接运行的curl、Python代码示例,并且结合多组实测数据,客观分析模型优势与能力边界,帮助普通开发者和AI应用从业者快速上手落地。
719 0
|
2月前
|
弹性计算 JSON 自然语言处理
阿里云 Workbench CLI 上架 Qoder 插件市场,不用 SSH 也能操作 ECS
阿里云 Workbench CLI 上线 Qoder IDE,让 AI Agent 用自然语言直接操作 ECS:查实例、执行命令、传文件、看日志、部署服务——无需公网 IP、免 SSH 配置、不记复杂命令,安全高效,开箱即用。
325 0
|
2月前
|
机器学习/深度学习 人工智能 缓存
DeepSeek Harness 最新邪修曝光!被吹成核弹的极简模式,真的夯吗?
DeepSeek Harness 极简模式实战测评!用 3D 射击游戏和 AI 宠物领养系统实测速度与成品质量,看看性能真的能暴增吗?无论你用 DeepSeek Harness 还是关注 DeepSeek V4 Pro,都能选对模式。
1265 1
|
2月前
|
存储 人工智能 缓存
DeepSeek Harness 全景可观测实践
日志能记下发生了什么,却说不清时间和 Token 花在哪。基于开源 LoongSuite,DeepSeek Harness 有了两条 OpenTelemetry 路线:插件两分钟直发 OTLP,Pilot 统一观测多个 Agent。
|
2月前
|
数据可视化 关系型数据库 API
支持 DeepSeek Harness 接入:AgentScope Service 开源智能体服务控制面解读
AgentScope Service 开源组件提供构建智能体应用所需的可视化调试、编排与分布式协调服务,支持业界主流智能体框架方式接入。
|
2月前
|
人工智能 弹性计算 安全
阿里云轻量应用服务器2核2G价格:38元、68元、79元、459元是怎么回事?
本文介绍阿里云轻量应用服务器2核2G配置价格差异现象,从产品定位、阶梯定价策略到高性价比替代方案进行了系统梳理。文章首先剖析了38元、68元、79元、459元四个价格分别对应新用户限量抢购价、新用户专属价、产品新用户专属价及老用户价,明确了不同用户身份的选购门槛。接着指出,轻量应用服务器以其一体化集成、丰富应用镜像及开箱即用等优势,成为个人开发者和轻量级应用的理想起点,其中38元抢购款日均成本仅约0.1元,且可一键部署OpenClaw等AI应用镜像。
|
算法 Java 微服务
【SpringCloud(1)】初识微服务架构:创建一个简单的微服务;java与Spring与微服务;初入RestTemplate
微服务架构是What?? 微服务架构是一种架构模式,它提出将单一应用程序划分为一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。 每个服务允许在其独立的进程中,服务于服务间采用轻量级的通信机制互相协作(通常是Http协议的RESTful API或RPC协议)。 每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据上下文,选择合适的语言、工具对其进行构建
845 126
|
3月前
|
数据采集 存储 人工智能
【洛神公开课】第5期:AI网络白皮书-01数据采集篇
AI训练常卡在“数据没到位”——PB级语料散落多源,搬运慢则每小时损失数十万元。本文详解阿里云AI数据采集网络:从高速专线、多云互联到OSS/CPFS分层存储,再到eRDMA直通GPU,构建稳定、高效、低成本的数据输送起跑线。(239字)
411 1
|
4月前
|
人工智能 Kubernetes 应用服务中间件
🚀【技术直播 · 重磅】Nginx Ingress 退役后,如何选对生产级网关?6月24日14:00-15:30,请锁定开发者社区直播间
"Nginx Ingress 不更新了,我该咋办?" 这场直播就是答案。 阿里云专家在线拆解迁移全流程,手把手教你不踩坑、不停服、不背锅。
431 2