首页
/ 把 Claude 托管智能体包装成 MCP 工具:cma-mcp 服务端完整实战指南

把 Claude 托管智能体包装成 MCP 工具:cma-mcp 服务端完整实战指南

2026-09-07 14:43:20作者:何将鹤

导读

cma-mcpmanaged_agents/cma-mcp 目录下的一个轻量 MCP(Model Context Protocol)服务器,用一层很薄的封装把 Claude 托管智能体(Claude Managed Agents,简称 CMA)的 Sessions API 暴露成九个 MCP 工具。这样一来,无论是 Claude Desktop / Claude Code,还是 claude.ai 网页版,都可以像调用普通工具一样,启动并与组织里已经部署好的托管 Agent 会话对话。读完本文,你将掌握:两种传输方式(stdio 与 Streamable HTTP)的选型逻辑、send_message → wait_for_idle 的核心交互循环、桌面端与网页端的完整接入步骤,以及该项目刻意不暴露某些危险端点的安全设计考量。

整体架构:Claude 是前端,CMA 是后端

先建立正确的心理模型。你在 Claude Desktop 里输入文字时,桌面端 Claude 本质是一个中继(relay):它通过 send_message 把你的原话送进 CMA 会话,再调用 wait_for_idle 阻塞等待托管 Agent 跑完当前回合,最后把回复原样展示给你。真正的工具调用、代码执行、仓库编辑都在 CMA 会话里完成,而不是在 Desktop 进程里完成。

项目 README 给出的调用链可以画成如下结构:

User ─▶ Claude (Desktop or claude.ai) ─▶ MCP: send_message + wait_for_idle ─▶ CMA session
  ▲                                                                              │
  └──────────────────────── agent's reply ◀─── stream-to-idle ◀──────────────────┘

这一设计的一个关键推论记录在 skill.md 中:session_id 是唯一的会话状态,而持有它的是 Claude 本身create_session 返回 session_id,Claude 在后续每次调用中都把它原样传回;MCP 服务器本身是无状态的。

另一个重要推论是同一套工具集对应两种传输tools.ts 注册了九个工具,server.ts 用 stdio 传输包装它(供 Claude Desktop 作为子进程拉起),server-http.ts 则用 Streamable HTTP 传输包装它(供 claude.ai 网页版经网络访问)。两者二选一,按目标客户端决定:

客户端 传输方式 入口文件
Claude Desktop / Claude Code stdio(本地子进程) src/server.ts
claude.ai 网页版(自定义 Connector) Streamable HTTP(部署 URL + Bearer Token) src/server-http.ts

九个工具:八个与 CMA 端点一一对应,一个 SSE 桥

九个工具的职责划分非常清晰。其中八个是 CMA 会话类端点的直通包装,唯一的"加工品"是 wait_for_idle——因为 MCP 是请求/响应模型,而 CMA 的完成事件是 SSE 流式推送的,必须有人在一个工具调用内部完成"流式等到 idle"的转换:

工具 对应 CMA 端点
list_agents / get_agent GET /v1/agents[/{id}]
create_session POST /v1/sessions
send_message / interrupt POST /v1/sessions/{id}/events
get_session GET /v1/sessions/{id}
list_events GET /v1/sessions/{id}/events
archive_session POST /v1/sessions/{id}/archive
wait_for_idle 流式读取 …/events/stream 直到 idle,返回回复文本

源码级拆解:核心实现是如何组织的

src/cma.ts 是共享的 Anthropic SDK 调用层,代码内以注释把工具分成了 Tier 1(1:1 端点包装)Tier 1.5(唯一一个便利动词) 两组,具体见 src/cma.ts

  • listAgents:流式遍历 anthropic.beta.agents.list(),支持 limit(默认 50)与大小写不敏感的 name_contains 过滤,只向 MCP 返回精简字段(id/name/description/model)。
  • getAgent:直接 agents.retrieve(agent_id) 返回完整配置。
  • createSession:调用 sessions.create,必须携带 agentenvironment_id 两个参数,可选 title;只返回 { session_id, status }
  • sendMessage:把用户文本封装成 { type: "user.message", content: [{ type: "text", text }] } 事件推送进会话,立刻返回 { queued: true }
  • interrupt:发送 { type: "user.interrupt" } 事件,让 Agent 在下一个安全点停下。
  • getSession:取回会话状态、标题、Agent、时间戳与 usage。
  • listEvents:支持 after_id 增量拉取,每个事件经 summarizeEvent 精简后返回(agent.message 只保留拼接的文本、工具调用保留 name/input、结果保留 is_error、idle 状态保留 stop_reason)。
  • archiveSession:归档会话,返回 { archived: true }
  • waitForIdle:整个项目里唯一对原始 API 做了加工的函数。它打开 sessions.events.stream(session_id) 的 SSE 流,边读边把 agent.message 里的 text block 拼进 reply,把 agent.tool_use / agent.mcp_tool_use 记入 tool_activity;遇到 session.status_idle(并进一步根据 stop_reason.type === "requires_action" 区分状态)或 session.status_terminated 即跳出,超过 deadline(由 timeout_sec 决定,默认 120 秒)则以 timeout 收尾;最后在 finally 中 abort 掉 SSE 连接,避免悬挂。返回结构为 { status, reply, tool_activity, last_event_id }

所有函数都依赖两个环境变量:ANTHROPIC_API_KEY(SDK 默认从它读取)与 CLAUDE_ENVIRONMENT_ID——src/cma.ts 在模块加载时就会校验后者,缺失直接抛 CLAUDE_ENVIRONMENT_ID is required

工具注册层:zod schema 是参数说明书

src/tools.tsregisterTools(server) 把九个 server.tool(...) 统一注册到任意一个 McpServer 实例上,stdio 与 HTTP 两个入口都复用它,保证同一套 schema 与描述。值得注意的 schema 细节:

  • list_agentslimit 被约束为 int().min(1).max(200),默认 50;
  • send_messagetext 描述明确写着 "The user's message, verbatim",用于引导 Claude 中继时逐字转发;
  • wait_for_idletimeout_sec 被约束在 min(5).max(600),默认 120;
  • 每个工具的 description 都刻意写明了它与前后动作的衔接(如 create_session 的返回值要传给后续每次调用),这本质上是对前端 Claude 的轻量提示词工程。
  • 工具的返回统一用 json() 包装成 MCP 的 text content(JSON.stringify 并缩进 2)。

两个入口文件详解

stdio 入口(Claude Desktop 本地路径)

src/server.ts 全文只有约十行:创建名为 cma-mcp、版本 0.1.0 的 McpServer,注册工具,然后 server.connect(new StdioServerTransport())。Claude Desktop 在本地把它作为子进程拉起,通过标准输入/输出交换 JSON-RPC 消息。

Streamable HTTP 入口(claude.ai 网页路径)

src/server-http.ts 约 40 行,但包含了远程暴露必须的安全细节:

  • 启动即校验 CMA_MCP_TOKEN,缺失直接抛错;端口取 process.env.PORT 或默认 3000;
  • 采用无状态模式:由于工具本身无状态、session_id 由 Claude 持有,每个 HTTP 请求都新建一个 McpServer + WebStandardStreamableHTTPServerTransport(注释里明确说明因此不需要 sessionIdGenerator);
  • 提供 /health 的 GET 探活接口;
  • 只有 /mcp 路径被路由到 transport;
  • 鉴权用 timingSafeEqual 做恒定时间比较,逐字节校验 Authorization: Bearer <token>,避免时序侧信道;代码注释把这一层称为"互联网与你 ANTHROPIC_API_KEY 的 CMA 配额之间唯一的屏障。不要移除"。

由于底层是 Web 标准的 Request/Response,package.jsontsconfig.json 支持 Bun 运行时(strict 模式 + ESNext + bundler moduleResolution),而注释表明同一文件理论上也能跑在 Node 18+ / Deno,甚至经过 process.envenvBun.serveexport default { fetch } 的适配后运行在 Cloudflare Workers 上。

快速开始

项目依赖 Bun 运行时,依赖声明在 package.json@anthropic-ai/sdk ≥ 0.95.1(README 明确要求这个最低版本)、@modelcontextprotocol/sdk ^1.22.0 与 zod ^3.25.0。启动步骤:

cd managed_agents/cma-mcp
bun install
claude

然后直接在 Claude 里问一句:"walk me through setting this up." Claude 会读取同目录的 skill.md,询问你目标是哪个客户端,再带你走对应路径的检查清单。skill.md 本身提供了三条命令对应三种常见操作:bun run stdio(桌面端)、bun run http(claude.ai 网页)、bun run typecheck(tsc 类型检查)。若要手动扩展或调试,CLAUDE.md 给出的工作流是:先通过 /claude-api 技能加载完整的 Managed Agents API 参考作为字段名的唯一事实来源,再读 skill.md,最后按客户端路径执行。

接入一:Claude Desktop(stdio,本地)

完整步骤如下,其中环境创建是一次性的:

1. 创建环境(CMA 会话需要 environment_id)——用 ant CLI:

ant beta:environments create --name cma-mcp \
  --config '{type: cloud, networking: {type: unrestricted}}' --transform id -r

2. 准备 Agent:这个 MCP 服务器不负责创建 Agent,它只驱动工作区里已经存在的托管 Agent;Agent 的创建/更新请走 ant CLI 或 Console。

3. 配置本地环境变量 .env.local

ANTHROPIC_API_KEY=sk-ant-...
CLAUDE_ENVIRONMENT_ID=env_...

4. 注册进 Claude Desktop——把以下内容合并进 macOS 的 ~/Library/Application Support/Claude/claude_desktop_config.json,然后重启 Desktop:

{
  "mcpServers": {
    "cma": {
      "command": "bun",
      "args": ["run", "/absolute/path/to/managed_agents/cma-mcp/src/server.ts"],
      "env": {
        "ANTHROPIC_API_KEY": "sk-ant-...",
        "CLAUDE_ENVIRONMENT_ID": "env_..."
      }
    }
  }
}

注意 args 里的路径必须写绝对路径。stdio 服务器由 Desktop 拉起,读不到你 shell 的环境变量,因此 API Key 与 Environment ID 必须显式放在这个 env 块里。

5. 测试:开一个新会话,问 "list my managed agents, start a session with the first one, and relay this message to it: hello."

接入二:claude.ai 网页(Streamable HTTP,远程)

同一套工具,但服务器跑在一个公网 URL 上,claude.ai 通过自定义 Connector 连过去。

1. 生成并保管 Token——拿到它的人就能驱动你的 Agent:

export CMA_MCP_TOKEN=$(openssl rand -hex 32)

2. 先本地起服务验证(测试期间可用 ngrok / cloudflared 暴露公网 URL):

bun run http   # → :3000/mcp

3. 部署Dockerfile 面向 Fly / Railway / Render 等平台——镜像基于 oven/bun:1-slim,安装生产依赖后执行 bun run src/server-http.tsENV PORT=3000。部署时把 ANTHROPIC_API_KEYCLAUDE_ENVIRONMENT_IDCMA_MCP_TOKEN 三个变量配成密钥(secrets)。

4. 在 claude.ai 添加自定义 Connector:Settings → Connectors → Add custom connector,填写:

字段
Name CMA
URL https://<your-deploy>/mcp
Authentication Bearer token → 你的 CMA_MCP_TOKEN

Team / Enterprise 组织提示:以 URL 方式添加 Connector 通常只有 org-admin 能做——普通成员看到的是官方精选目录而非 URL 输入框。由管理员在组织设置里一次性填入 URL + Token 后,它会出现在每个成员的 Connectors 列表里供启用。(对于要推广给非技术用户的使用形态,这本来也是更合理的方式。)

5. 测试:新会话 → 启用 CMA Connector → 使用与桌面端相同的测试提示词。

Relay 模式:推荐写入 Project 指令

如果不加引导,桌面端 Claude 往往会忍不住自己回答用户,而不是转发给后端 Agent。把下面这段写进某个 Project 的自定义指令里,即可让它专心当中继:

You are a frontend for a backend Managed Agent reached via the cma MCP tools. On the first user turn: list_agents (if needed) → create_sessionsend_message(user text verbatim)wait_for_idle → return the reply verbatim. On subsequent turns: send_messagewait_for_idle. Do not answer from your own knowledge; do not paraphrase the backend's reply. If wait_for_idle returns status: "timeout", tell the user it's still running and offer to keep waiting.

关键约束是:用户原话与后端回复都要 verbatim 直传,禁止用自己的知识作答、禁止改写后端回复;超时则如实告知并询问是否继续等待。

易踩的坑(Gotchas)

1. stdio 只能用于 Desktop,HTTP 等于暴露到公网。 浏览器里的 claude.ai 无法拉起本地进程,stdio 只适用于 Claude Desktop / Claude Code;要走 claude.ai 网页就必须走 HTTP,而 URL 一旦公开,Bearer Token 就是你 ANTHROPIC_API_KEY 背后 CMA 配额唯一的门禁。不带它部署、不打日志、像 API Key 一样定期轮换。

2. 长回合 vs 工具超时。 wait_for_idle 是阻塞的。若 CMA Agent 要跑好几分钟(克隆大仓库、大量工具调用),MCP 客户端可能把这次工具调用判为超时。缓解手段:传较小的 timeout_sec 并循环重试(wait_for_idle 返回 status: "timeout" 时会带上 last_event_id,可据此再调一次),或者让 Claude 改为轮询 get_session + list_events(after_id=...)

3. 计费归属。 所有 CMA 用量都记在服务器配置里那个 ANTHROPIC_API_KEY 名下,而不是 Desktop 用户自己的账号——这正是整套方案的意义(非技术用户不需要自己的 Key),但也意味着要根据使用量给该 Key 配置合适的 workspace 限额。

4. 刻意不暴露的操作。 出于安全考虑,以下端点被故意排除在工具集之外:

端点 不暴露的原因
agents.archive 永久性且不可撤销——一次误调用就可能废掉生产 Agent
agents.create / update Agent 的创作应在 ant CLI / Console 完成,而不是在一次聊天回合里
sessions.deleteenvironments.* 破坏性基础设施操作
vaults.*credentials.* 涉及机密凭据
sessions.resources.add 需要聊天用户拿不到的 token / file ID

不过 skill.md 也注明:若确有需要,在 src/cma.tssrc/server.ts 里各加几行即可暴露上述任一操作。

调试速查表

skill.md 的排错表几乎覆盖了所有常见症状,值得逐条收藏:

症状 排查方向
Desktop 里看不到 cma 工具 配置文件路径写错,或 Desktop 进程的 PATH 里没有 bun(给 bun 用绝对路径);查看 Desktop 的 MCP 日志
CLAUDE_ENVIRONMENT_ID is required Desktop 配置里缺 env 块——stdio 服务器不会读取你 shell 的环境变量
wait_for_idle 返回空的 reply Agent 到达 idle 却没产出文本(比如只做了工具调用);用 list_events 看完整日志
每轮对话都新开会话 Claude 没有回传 session_id;收紧 Project 指令
claude.ai Connector 显示 "couldn't connect" URL 写错、服务器不可达或 token 不匹配(去服务器日志看有没有 401)
HTTP 服务本地就返回 401 启动 bun run http 的那个 shell 里没有导出 CMA_MCP_TOKEN

结语:最小工具面 + 清晰角色分工

cma-mcp 的工程取舍非常克制:九个工具、一层 SSE→请求/响应桥、无状态服务器、token 单点鉴权、只读优先的安全边界。它把"能驱动托管 Agent"这件事变成了任何 MCP 客户端都认识的九个动词,同时把 Agent 的编排、环境的生命周期、用户的计费边界都留给了 CMA 平台自身。如果你需要在自己的 Claude 工作流里把已经部署好的托管 Agent 变成随手可用的"对话式工具",这份代码就是一份很干净的参考实现——先跑通 Desktop 的 stdio 路径,再按需升级到 claude.ai 的 HTTP + Connector 形态即可。

登录后查看全文
热门项目推荐
相关项目推荐