把 Claude 托管智能体包装成 MCP 工具:cma-mcp 服务端完整实战指南
导读
cma-mcp 是 managed_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,必须携带agent与environment_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.ts 的 registerTools(server) 把九个 server.tool(...) 统一注册到任意一个 McpServer 实例上,stdio 与 HTTP 两个入口都复用它,保证同一套 schema 与描述。值得注意的 schema 细节:
list_agents的limit被约束为int().min(1).max(200),默认 50;send_message的text描述明确写着 "The user's message, verbatim",用于引导 Claude 中继时逐字转发;wait_for_idle的timeout_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.json 与 tsconfig.json 支持 Bun 运行时(strict 模式 + ESNext + bundler moduleResolution),而注释表明同一文件理论上也能跑在 Node 18+ / Deno,甚至经过 process.env → env 与 Bun.serve → export 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.ts,ENV PORT=3000。部署时把 ANTHROPIC_API_KEY、CLAUDE_ENVIRONMENT_ID、CMA_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
cmaMCP tools. On the first user turn:list_agents(if needed) →create_session→send_message(user text verbatim)→wait_for_idle→ return thereplyverbatim. On subsequent turns:send_message→wait_for_idle. Do not answer from your own knowledge; do not paraphrase the backend's reply. Ifwait_for_idlereturnsstatus: "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.delete、environments.* |
破坏性基础设施操作 |
vaults.*、credentials.* |
涉及机密凭据 |
sessions.resources.add |
需要聊天用户拿不到的 token / file ID |
不过 skill.md 也注明:若确有需要,在 src/cma.ts 与 src/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 形态即可。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00