MCP 工具开始算钱了:给每个工具标注成本,给每次对话设预算


月底对账的时候你才发现:某个 MCP 工具被 agent 调了四十几次,每次都真金白银地扣额度。回看对话记录,它当时的表现和调用一个免费的读文件工具没什么区别——同样的 JSON 输入、同样的一行“完成”,你根本无从判断哪一次是在花钱。

这就是 MCP 生态现在最尴尬的地方:图片、视频、3D 生成、付费 API 都被包成了 MCP 服务器,而 agent 看待它们的方式和看待 read_file 一模一样。PR #142470(2026 年 10 月 21 日合并)给这个问题补了两块拼图:声明成本,以及每次对话的预算上限。

三个新配置项

在 config.yaml 的 MCP 服务器条目里:

mcp_servers:
  renders:
    command: "render-mcp"
    trust: untrusted
    tool_costs:
      create_image: 20                                    # 每次大约 20 额度
      animate: {credits: [5, 20], note: "per animation"}   # 区间 + 一句说明
      get_status: 0                                        # 免费,与不写等价
    spend_limit: 100     # 每次对话的上限;0 或省略 = 不限制
    cost_unit: credits   # 弹窗里显示的单位,默认就是 credits

tool_costs 的值支持三种形态:

  • 一个非负数,表示固定成本;
  • 一个 [最小值, 最大值] 区间,适合“按分辨率/时长计价”的工具;
  • 一个对象,用 credits 字段放数字或区间,再用 note 补一句人话说明。

spend_limit 是每次对话的额度上限,写 0 或者不写就是不限制。cost_unit 只影响显示文案,默认 credits——你的服务按“次”“点”“token”计价,把它改成对应的词即可。

审批弹窗变成了账单

没声明成本之前,一个 trust: untrusted 的服务器在写入类调用前会弹一句“允许执行 create_image 吗”。声明之后,同一句话会变成:

Costs about 20 credits.

区间成本会显示成 Costs 5-20 credits (per animation).;而这个对话已经花过钱之后,还会多一行:

This chat has used about 40 credits on 'renders' (limit 100).

这两句话的价值在于把“要不要批准”从一个安全问题变成了一个预算问题。你批准的不再是一个动作,而是“再花 20 额度”。

累计到底记在谁头上

这块规则比配置项重要,因为它决定你会不会算错账:

  • 按对话 × 按服务器记账,两个维度都是独立的。同一个服务器在你另一个聊天里的花费不算进来。
  • 只统计真正发出去的调用。被拒绝的、被拦下的调用不计数。
  • 按声明的估算值计,区间取上限。也就是说它宁可让你早点被问,也不会让你事后才发现超支。
  • 一个调用在服务器还没收到就挂掉的情况,仍然计数——这是有意的,往“多问一次”的方向偏。
  • 累计只存在内存里,它是“这次对话的预算”,不是厂商的账单。重启 Hermes 会清零。
  • 上下文压缩、/branch 换会话 id 之后,累计跟着对话走;CLI 里 /new 会开新账,桌面版新开聊天同理;但消息平台上账挂在聊天本身,/new 之后仍然延续。每个 cron 运行有自己独立的一份。

超过预算会发生什么

这是整套机制里最值得记住的一节:

  1. 在 trust: full 的服务器上也会问。 平时这种服务器上花钱工具是直接跑的,一旦会越过 spend_limit,它会先停下来问你。
  2. 每次都要问一次。 选“本会话内允许”也压不住它——后续每一次带成本的调用仍然会弹。
  3. 无人值守时直接拒绝,且不执行。 cron 任务、hermes chat -q、没有审批人的网关,都属于这一类。模型收到的是一个明确说明“已花多少、上限多少、不要再重试”的工具错误,而不是静默失败。
  4. 没超预算时一切照旧。 trust: full 照样不问;带 readOnlyHint: true 标注的只读工具不会被这条规则叫醒,除非预算要求。

换句话说,spend_limit 不是限速器,而是“越过这条线就必须有人点头”的闸门。

插件包里也能声明

可移植插件包可以在 plugin.json 的 extensions 里给它的服务器带同一张表:

"extensions": {
  "com.nousresearch.hermes": {
    "servers": {
      "meshy": {
        "trust": "untrusted",
        "costs": { "meshy_remesh": 5, "meshy_animate": { "credits": [5, 20], "note": "per animation" } },
        "spend_limit": 100,
        "cost_unit": "credits"
      }
    }
  }
}

这解决了一个现实问题:官方或第三方插件的作者最清楚自己每个工具花多少钱,而用户通常不清楚。包里的 costs / spend_limit / cost_unit 会自动翻译成原生配置项。

五个容易踩的坑

  1. 写错的成本表会让整个服务器被跳过,不是静默忽略。三个键里任何一个格式不对,那个服务器都不会加载,并报出具体是哪个键——这是有意为之:你不能因为写错一个缩进就把预算丢了。
  2. tool_costs 的键必须是服务器自己的工具名,不要写 Hermes 展示层加的前缀 mcp_<server>_。写错不会报错,只在连接时给一条警告,很容易漏看。
  3. 同名覆盖时的继承规则:你自己写一个同名的 mcp_servers 条目(比如为了把它升到 trust: full),包里的成本配置仍然生效,除非你在自己的条目里也写了这几个键。想彻底关掉包里的上限,写 spend_limit: 0;想丢掉它的成本表,写 tool_costs: {}。
  4. 它是预算,不是账单。厂商侧的实际计费可能和声明不一致,重启之后累计也会清零。别拿它当财务报表。
  5. hermes plugins validate 会列出 server costs: <name>,装机前用它确认成本表被正确读到了,比事后查对话记录便宜。

现在能用吗

这个特性目前只在 main 上,不在已经发布的 v0.22.4 里(我对比了 v0.22.3 到 v0.22.4 的 14 个提交,没有它)。所以生产环境要等下一个版本;如果你跑的是源码安装并跟主干,现在就可以试。

来源上,这个设计的灵感来自社区项目 Adolanium/hermes-tripo——它用原生的 pre_tool_call 钩子实现了同样的“每次额度都要问一声、每次对话有个上限”。可移植包不能带钩子,所以这次做成了核心特性,靠声明式数据驱动,官方插件(比如 Meshy 那个)也能直接用上。

如果你还没决定哪些服务器该给 trust: full,可以先读我们之前的 MCP 目录安全指南,成本声明和信任分级解决的是两个不同的问题,但搭配起来才完整。