跳到正文

Blog

MCP Tool 已经是产品 API,不是 Demo 接口

为什么 Appaloft 从 operation catalog 生成 MCP tools,并让它们回到同一套 CommandBus/QueryBus、authz、audit 和 readback,而不是手写 agent 专用工具。

AppaloftAIMCPAPI部署控制面Agents

MCP 最容易做成 demo。

写几个 tools,给每个 tool 一个名字、一段 description、一份 JSON schema,再把它们接到现有函数上。Agent 能列工具,能调用工具,能拿到返回值。截图很好看。

部署产品的问题是:工具一旦能改状态,它就不再是 demo 接口。它是产品 API。

这篇接着 Skill 不是 MCP 往下说。上一篇讲 skill 和 MCP 的分工:skill 教 agent 怎么判断,MCP 给 agent 稳定调用入口。今天更具体一点:当 MCP tool 真的能部署、回滚、配置资源、管理域名或查询日志时,它必须和 CLI、HTTP/API、Web Console 共享同一份产品语义。

否则工具会先在 demo 里工作,然后在生产里长出第二套控制面。

手写 tool list 很快会漂移

最危险的 MCP 设计通常不是明显错误,而是过于方便。

比如写一个 quick_deploy_create。它接受一个仓库地址、一个 server id、几个环境变量,然后帮 agent 创建 project、resource、deployment。站在 agent 体验上看,这很顺手。站在产品边界上看,它马上绕开了一堆问题:

  • 这次操作到底对应哪个 operation key?
  • CLI 和 Web Console 里有没有同一个动作?
  • 输入 schema 和 HTTP/API 是否一致?
  • 哪些 tenant、role、entitlement 和 capability 规则生效?
  • 这次 mutation 是否幂等?
  • audit event 怎么记录?
  • secret、connection string、log line 怎么脱敏?
  • 部署失败后,用户该看哪个 readback 或 recovery command?

这些问题如果没有答案,MCP tool 就不是“AI 入口”,而是一条产品捷径。

捷径最初通常只是为了让 demo 顺一点。后来会被文档引用,被用户记住,被 agent prompt 学会。再后来,CLI/API/Web 的行为改了,MCP 还在旧语义上跑。团队开始修一个用户看不见、权限边界说不清、测试覆盖也不完整的产品面。

Appaloft 不想让这件事发生。

Operation catalog 是产品合同

Appaloft 的做法是把 operation catalog 当成共享合同,而不是把 MCP 当成新的业务层。

在 public Appaloft 里,packages/application/src/operation-catalog.ts 记录产品操作:operation key、command/query 类型、domain、CLI transport、HTTP/API route、input schema。MCP descriptor 从这份 catalog 生成。deployments.create 映射成 deployments_createdeployments.plan 映射成 deployments_planresources.runtime-logs 映射成 resources_runtime_logssystem.doctor 映射成 system_doctor

这不是命名偏好。它让每个入口都能回答同一个问题:我正在调用哪个产品操作?

如果一个行为不在 operation catalog 里,Appaloft MCP 就不应该偷偷发明一个 tool。正确做法是先判断这个行为是否属于 public Appaloft 的中性能力。如果是,就在 public core 里补 operation、schema、handler、CLI/API surface、测试和文档;Cloud 再通过 authz、entitlement、audit、policy 或 adapter 做私有组合。如果它只是 Cloud 私有治理能力,也要有清楚的 Cloud route 和边界,而不是藏在 agent-only tool 里。

这个规则会让早期开发慢一点,但能避免产品语义分叉。

Tool 不是 wrapper,而是受约束的 transport

一个 MCP tool 看起来像函数调用,实际应该被当成 transport。

在 Appaloft 里,tool descriptor 只负责把 operation 暴露给 agent:名字、描述、input schema、CLI/API metadata、read-only/destructive/idempotent/open-world hints。真正的读写仍然回到应用边界:CommandBus 和 QueryBus。

这意味着 deployments_create 不应该有一段 MCP-only 的部署逻辑。它应该创建同一个 command,走同一套 application handler,触发同样的权限、审计、幂等、redaction、readback 和错误语义。resources_runtime_logs 也不应该直接 SSH 到服务器读日志;它应该走 Appaloft 已经定义的 runtime log query surface。

这条边界对 agent 尤其重要。Agent 很擅长把“能调通”误解成“应该调”。产品不能把是否安全交给模型猜。Tool schema 应该尽量精确,但真正的 guard 必须留在产品层。

Resources 和 prompts 不能变成隐藏命令

MCP 里还有 resources 和 prompts。它们很有用,也很容易被滥用。

Resources 适合放只读上下文:operation catalog snapshot、high-value tools、Appaloft skill、deploy protocol、MCP guide、agent docs。Agent 可以读这些材料来决定下一步。它不能通过 resource 改状态。

Prompts 适合放 workflow starter:first deploy、recover failed deployment、configure resource、observe runtime、publish static artifact。Prompt 可以提醒 agent 先 inspect,再 plan,再 execute,再 observe。它不能拥有业务状态,不能绕过 confirmation,也不能在背后做 mutation。

区别很细,但生产里很现实。如果一个 prompt 自己开始创建 deployment,它就成了没有 operation key 的 command handler。如果一个 resource 会根据读取动作修复 runtime,它就不再是 resource。等出了问题,operator 很难回答:“到底是哪条操作改了系统?”

所以 Appaloft 把 MCP 的三类东西分开:

  • tools 执行 operation catalog 里的操作;
  • resources 提供只读上下文;
  • prompts 编排已有操作。

这听起来普通,但普通正是这里需要的东西。

写操作要有同样的产品保护

Agent 调用写操作时,保护边界不能因为入口是 MCP 就变薄。

部署、回滚、删除、域名、证书、dependency resource、storage、organization、deploy token、system maintenance 这些操作都可能影响真实环境。有些会创建外部资源,有些会改变访问路径,有些会触碰凭据或日志。MCP tool 不能只靠一句 description 说“be careful”。

Appaloft 希望每个写操作都能带着产品层信息运行:

  1. 当前 actor 和 tenant context 是谁。
  2. 这个 actor 是否有 capability。
  3. 这个操作是否需要 plan-first。
  4. 是否需要 exact confirmation field。
  5. 输入里是否包含 secret 或应该引用 secret ref。
  6. 结果里哪些字段必须脱敏。
  7. 是否产生 audit event。
  8. 失败后应该返回哪个结构化错误和 recovery hint。

这些规则属于控制面,不属于 prompt。Agent 可以读提示、按步骤做事,但不能成为权限模型。

结构化错误比漂亮文案更有用

MCP 给 agent 的返回值也不能只是一段“部署失败了,请稍后再试”。

Agent 需要能区分:认证失败、权限不足、缺少 secret、server runtime 未准备好、dependency binding 缺失、provider adapter 不可用、DNS 未传播、health check 超时、deployment 被 supersede、操作需要 confirmation,或者控制面暂时不支持某个 transport。

这些错误对人也有用。CLI 可以打印更好的下一步。Web Console 可以把用户带到正确页面。MCP tool 可以让 agent 选择“先看 logs”还是“请求用户授权”,而不是继续猜命令。

这也是为什么 operation catalog 不能只记录 tool 名字。它要把输入、transport、handler、annotations、readback 和错误语义都拉回同一条产品路径。Agent-friendly 的接口不是“更会说话”的接口,而是更少让 agent 猜的接口。

Auth 不能是 MCP 的旁路

上一篇讲过 AI Agent 登录不该复制 Cookie。这里再补一层:remote MCP gateway 也不应该拥有 MCP-only 权限模型。

本地 stdio MCP 可以复用本机 Appaloft runtime。Remote MCP 需要 bearer handoff、profile、tenant context 和服务端 authz。Cloud hosted MCP 可以增加团队权限、entitlement、audit、policy 和 rate limit。Enterprise 可以部署 private gateway。

这些差异都可以存在,但 tool 最后仍然应该回到同一个 operation。不能因为请求来自 agent,就跳过 Web/CLI 会走的权限和审计。也不能把 hosted gateway 变成一个私有 deploy engine。

MCP 是更适合 agent 的调用方式,不是更宽松的权限方式。

测试要证明没有第二套语义

如果 MCP tools 是产品 API,测试也要按产品 API 来写。

Appaloft 的 public MCP 测试里有一类很朴素的断言:每个 operation catalog entry 都有一个生成出来的 tool descriptor;descriptor 保留 operation key、kind、domain、CLI/API transport metadata;高价值 deployment 和 resource tools 使用 operation-key-derived 名字。

这类测试不会让人兴奋,但很有用。它们证明 MCP 没有手写另一份列表,也没有悄悄把 deployments.create 改成另一个产品动作。

更完整的测试还需要覆盖 handler:read-only tool 不能 mutation,write tool 走 command boundary,destructive operation 需要 confirmation,secret 不出现在输出里,structured result 和 text fallback 都能被旧客户端处理。Skill eval 也要覆盖真实任务,避免 agent 学会绕过 Resource profile、dependency binding、preview selector 或 auth handoff。

测试目标不是“agent 能不能成功调用一次 tool”。目标是“agent 调用 tool 时,产品边界仍然成立”。

我们愿意接受的麻烦

这套做法有成本。

新增一个操作时,不能只给 MCP 加一段 handler。要看它是否属于 operation catalog,要补 schema、CLI/API route、CommandBus/QueryBus handler、权限、audit、redaction、readback、docs、skill reference 和测试。Cloud 里的 hosted gateway 也要确认自己只是在 composition 层增加治理,而不是复制 public MCP server。

但我们愿意接受这个麻烦。

部署控制面的难点从来不只是“让某个调用成功”。难点是半年后还能说清楚:哪个 actor,在什么 tenant,下发了哪个 operation,输入是什么,哪些 secret 被引用而没有泄露,是否经过 plan,是否被授权,改了什么状态,失败时如何恢复。

当 AI agent 进入生产工作流,这些问题不会消失,只会更频繁地出现。

所以 Appaloft 对 MCP 的态度很直接:MCP tool 是产品 API。它应该被生成、校验、授权、审计、测试,也应该和 CLI、HTTP/API、Web Console 回到同一份 operation catalog。Demo 可以手写工具;部署控制面不应该长期这么做。