|
| 1 | +# Setup tips & tricks — CMA as an MCP server |
| 2 | + |
| 3 | +Things that aren't obvious from the docs and tend to cost debugging time. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## Mental model |
| 8 | + |
| 9 | +### Claude Desktop is the frontend, CMA is the backend |
| 10 | + |
| 11 | +The Claude you're typing to in Desktop is a **relay**. It calls `send_message` with your words, calls `wait_for_idle` to block until the CMA agent finishes, then shows you the reply. The actual work — tool use, code execution, repo edits — happens in the CMA session, not in Desktop. |
| 12 | + |
| 13 | +### Eight 1:1 tools, one shim |
| 14 | + |
| 15 | +`list_agents`, `get_agent`, `create_session`, `send_message`, `interrupt`, `get_session`, `list_events`, `archive_session` are straight endpoint wrappers. `wait_for_idle` is the only editorial: MCP is request/response and CMA completion is SSE, so something has to stream-to-idle inside a tool call. |
| 16 | + |
| 17 | +### `session_id` is the only state, and Claude holds it |
| 18 | + |
| 19 | +`create_session` returns it; Claude passes it to every subsequent call. The MCP server itself is stateless. |
| 20 | + |
| 21 | +### Two transports, one tool set |
| 22 | + |
| 23 | +`src/tools.ts` registers the nine tools. `src/server.ts` wraps it in stdio (Claude Desktop spawns it as a subprocess); `src/server-http.ts` wraps it in Streamable HTTP (claude.ai web reaches it over the network). Pick one. |
| 24 | + |
| 25 | +--- |
| 26 | + |
| 27 | +## Setup — Claude Desktop (stdio, local) |
| 28 | + |
| 29 | +1. **Environment** (one-time — sessions need an `environment_id`): |
| 30 | + ```bash |
| 31 | + ant beta:environments create --name cma-mcp \ |
| 32 | + --config '{type: cloud, networking: {type: unrestricted}}' --transform id -r |
| 33 | + ``` |
| 34 | +2. **Agents**: this server doesn't create agents — it drives the ones already in your workspace. Create/update them via the `ant` CLI or the Console. |
| 35 | +3. **`.env.local`**: |
| 36 | + ``` |
| 37 | + ANTHROPIC_API_KEY=sk-ant-... |
| 38 | + CLAUDE_ENVIRONMENT_ID=env_... |
| 39 | + ``` |
| 40 | +4. **Register with Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) and restart Desktop: |
| 41 | + ```json |
| 42 | + { |
| 43 | + "mcpServers": { |
| 44 | + "cma": { |
| 45 | + "command": "bun", |
| 46 | + "args": ["run", "/absolute/path/to/managed_agents/cma-mcp/src/server.ts"], |
| 47 | + "env": { |
| 48 | + "ANTHROPIC_API_KEY": "sk-ant-...", |
| 49 | + "CLAUDE_ENVIRONMENT_ID": "env_..." |
| 50 | + } |
| 51 | + } |
| 52 | + } |
| 53 | + } |
| 54 | + ``` |
| 55 | +5. **Test**: in a new Desktop chat, ask *"list my managed agents, start a session with the first one, and relay this message to it: hello."* |
| 56 | + |
| 57 | +--- |
| 58 | + |
| 59 | +## Setup — claude.ai web (Streamable HTTP, remote) |
| 60 | + |
| 61 | +Same tools, but the server runs at a public URL and claude.ai connects to it as a custom Connector. |
| 62 | + |
| 63 | +1. **Token** — generate and keep it; anyone with this token can drive your agents: |
| 64 | + ```bash |
| 65 | + export CMA_MCP_TOKEN=$(openssl rand -hex 32) |
| 66 | + ``` |
| 67 | +2. **Run locally first** (ngrok/cloudflared for a public URL while testing): |
| 68 | + ```bash |
| 69 | + bun run http # → :3000/mcp |
| 70 | + ``` |
| 71 | +3. **Deploy** — the `Dockerfile` targets Fly / Railway / Render; set `ANTHROPIC_API_KEY`, `CLAUDE_ENVIRONMENT_ID`, `CMA_MCP_TOKEN` as secrets. (Cloudflare Workers also works — `WebStandardStreamableHTTPServerTransport` is fetch-native; swap `process.env` → `env` and `Bun.serve` → `export default { fetch }`.) |
| 72 | +4. **claude.ai → Settings → Connectors → Add custom connector**: |
| 73 | + |
| 74 | + | Field | Value | |
| 75 | + |---|---| |
| 76 | + | Name | `CMA` | |
| 77 | + | URL | `https://<your-deploy>/mcp` | |
| 78 | + | Authentication | Bearer token → your `CMA_MCP_TOKEN` | |
| 79 | + |
| 80 | + > **Team / Enterprise orgs:** adding a connector by URL is typically **org-admin only** — individual users see a curated directory, not a URL field. An admin adds the URL + token once in org settings; it then appears in every member's Connectors list to enable. (This is the shape you want for rolling out to non-technical users anyway.) |
| 81 | +
|
| 82 | +5. **Test**: new chat → enable the `CMA` connector → same prompt as above. |
| 83 | + |
| 84 | +--- |
| 85 | + |
| 86 | +## Relay mode (recommended Project instructions) |
| 87 | + |
| 88 | +Without guidance, Desktop Claude will try to answer the user itself instead of relaying. Put this in a Project's custom instructions: |
| 89 | + |
| 90 | +> 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_session` → `send_message(user text verbatim)` → `wait_for_idle` → return the `reply` verbatim. On subsequent turns: `send_message` → `wait_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. |
| 91 | +
|
| 92 | +--- |
| 93 | + |
| 94 | +## Gotchas |
| 95 | + |
| 96 | +### stdio = Desktop only; HTTP = the internet |
| 97 | + |
| 98 | +Browser claude.ai can't spawn a local process, so stdio only works with Claude Desktop / Claude Code. The HTTP path gets you claude.ai web, but now the URL is public — the **bearer token is the only gate** in front of your `ANTHROPIC_API_KEY`'s CMA quota. Don't deploy without it, don't log it, rotate it like any API key. |
| 99 | + |
| 100 | +### Long turns vs tool timeout |
| 101 | + |
| 102 | +`wait_for_idle` blocks. If the CMA agent runs for minutes (big repo clone, many tool calls), the MCP client may time out the tool call. Mitigations: pass a smaller `timeout_sec` and loop (`wait_for_idle` returns `status: "timeout"` + `last_event_id`; call again), or have Claude poll `get_session` + `list_events(after_id=...)` instead. |
| 103 | + |
| 104 | +### Billing |
| 105 | + |
| 106 | +All CMA usage bills to the `ANTHROPIC_API_KEY` baked into the server config — not to the Desktop user's account. That's the point (non-technical users, no keys), but size the key's workspace limits accordingly. |
| 107 | + |
| 108 | +### Deliberately not exposed |
| 109 | + |
| 110 | +| Endpoint | Why not | |
| 111 | +|---|---| |
| 112 | +| `agents.archive` | Permanent, no undo — one bad tool call bricks a prod agent | |
| 113 | +| `agents.create` / `update` | Authoring belongs in the `ant` CLI / Console, not a chat turn | |
| 114 | +| `sessions.delete`, `environments.*` | Destructive infra ops | |
| 115 | +| `vaults.*`, `credentials.*` | Secrets | |
| 116 | +| `sessions.resources.add` | Needs tokens/file IDs the chat user won't have | |
| 117 | + |
| 118 | +Adding any of these is a few lines in `src/cma.ts` + `src/server.ts` if your use case needs them. |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## Debugging |
| 123 | + |
| 124 | +| Symptom | Check | |
| 125 | +|---|---| |
| 126 | +| Desktop shows no `cma` tools | Config path wrong, or `bun` not on PATH for the Desktop process (use an absolute path to `bun`). Check Desktop's MCP logs. | |
| 127 | +| `CLAUDE_ENVIRONMENT_ID is required` | Env block missing from the Desktop config — stdio servers don't read your shell's env. | |
| 128 | +| `wait_for_idle` returns empty `reply` | Agent went idle without emitting text (e.g. only tool calls). `list_events` shows the full log. | |
| 129 | +| Every turn starts a new session | Claude isn't threading `session_id`. Tighten the Project instructions. | |
| 130 | +| claude.ai Connector shows "couldn't connect" | URL wrong, server not reachable, or token mismatch (check server logs for 401). | |
| 131 | +| HTTP server returns 401 locally | `CMA_MCP_TOKEN` not exported in the shell you ran `bun run http` from. | |
0 commit comments