Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions managed_agents/cma-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Anthropic
ANTHROPIC_API_KEY=sk-ant-...

# One environment shared by all sessions this server creates.
# Create via: ant beta:environments create --name cma-mcp --config '{type: cloud, networking: {type: unrestricted}}'
CLAUDE_ENVIRONMENT_ID=env_...

# HTTP transport only — bearer token the client must send.
# Generate: openssl rand -hex 32
CMA_MCP_TOKEN=
4 changes: 4 additions & 0 deletions managed_agents/cma-mcp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
.env
.env.local
bun.lock
11 changes: 11 additions & 0 deletions managed_agents/cma-mcp/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# CMA as an MCP server

MCP server that exposes Managed Agents session primitives as tools. Two entrypoints sharing one tool set: `src/server.ts` (stdio → Claude Desktop) and `src/server-http.ts` (Streamable HTTP → claude.ai web Connector).

## When the user asks to set this up, extend it, or debug it

1. **Invoke `/claude-api` first.** That skill loads the full Managed Agents API reference (agents, sessions, events, environments). Use it as the source of truth for any SDK call — don't guess field names.
2. **Read `./skill.md`** and walk the user through it. First ask which client they're targeting (Claude Desktop → stdio path; claude.ai web → HTTP + deploy + Connector), then follow that path's checklist. Both end with the same `send_message → wait_for_idle` loop and relay-mode Project instructions.
3. **Adding a tool?** Map the CMA endpoint 1:1 in `src/cma.ts`, then register it in `src/server.ts` with a zod schema. See the tier table in skill.md for what's deliberately **not** exposed (destructive ops, secrets).

Commands: `bun run stdio` (Desktop), `bun run http` (claude.ai), `bun run typecheck`.
8 changes: 8 additions & 0 deletions managed_agents/cma-mcp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM oven/bun:1-slim
WORKDIR /app
COPY package.json ./
RUN bun install --production
COPY src ./src
ENV PORT=3000
EXPOSE 3000
CMD ["bun", "run", "src/server-http.ts"]
50 changes: 50 additions & 0 deletions managed_agents/cma-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# CMA as an MCP server

A thin [MCP](https://modelcontextprotocol.io) server that wraps the Claude [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) Sessions API — so Claude Desktop **or** claude.ai web can start and chat with your org's hosted agents as if they were tools.

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

Nine tools — eight are 1:1 with CMA endpoints, one (`wait_for_idle`) is the SSE→request/response shim. Same handlers, two transports.

## Quickstart

```bash
cd managed_agents/cma-mcp
bun install
claude
```

Then ask: **"walk me through setting this up."** Claude reads [`skill.md`](./skill.md) and drives whichever path you pick:

| Client | Transport | Entrypoint |
|---|---|---|
| **Claude Desktop / Claude Code** | stdio (local process) | `src/server.ts` |
| **claude.ai web** (custom Connector) | Streamable HTTP (deployed URL + bearer token) | `src/server-http.ts` |

## Tools

| Tool | CMA endpoint |
|---|---|
| `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`** | streams `…/events/stream` until idle, returns reply text |

## Files

| | |
|---|---|
| `src/cma.ts` | Anthropic SDK calls — shared |
| `src/tools.ts` | Nine `server.tool(...)` registrations — shared |
| `src/server.ts` | stdio entrypoint (~10 LOC) |
| `src/server-http.ts` | HTTP entrypoint + bearer auth (~40 LOC) |
| `Dockerfile` | Fly / Railway / Render deploy for the HTTP path |

Requires `@anthropic-ai/sdk` ≥ 0.95.1.
23 changes: 23 additions & 0 deletions managed_agents/cma-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "cma-mcp",
"version": "0.1.0",
"private": true,
"type": "module",
"bin": {
"cma-mcp": "./src/server.ts"
},
"scripts": {
"stdio": "bun run src/server.ts",
"http": "bun run src/server-http.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.95.1",
"@modelcontextprotocol/sdk": "^1.22.0",
"zod": "^3.25.0"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5"
}
}
131 changes: 131 additions & 0 deletions managed_agents/cma-mcp/skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Setup tips & tricks — CMA as an MCP server

Things that aren't obvious from the docs and tend to cost debugging time.

---

## Mental model

### Claude Desktop is the frontend, CMA is the backend

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.

### Eight 1:1 tools, one shim

`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.

### `session_id` is the only state, and Claude holds it

`create_session` returns it; Claude passes it to every subsequent call. The MCP server itself is stateless.

### Two transports, one tool set

`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.

---

## Setup — Claude Desktop (stdio, local)

1. **Environment** (one-time — sessions need an `environment_id`):
```bash
ant beta:environments create --name cma-mcp \
--config '{type: cloud, networking: {type: unrestricted}}' --transform id -r
```
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.
3. **`.env.local`**:
```
ANTHROPIC_API_KEY=sk-ant-...
CLAUDE_ENVIRONMENT_ID=env_...
```
4. **Register with Claude Desktop** — add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) and restart Desktop:
```json
{
"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_..."
}
}
}
}
```
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."*

---

## Setup — claude.ai web (Streamable HTTP, remote)

Same tools, but the server runs at a public URL and claude.ai connects to it as a custom Connector.

1. **Token** — generate and keep it; anyone with this token can drive your agents:
```bash
export CMA_MCP_TOKEN=$(openssl rand -hex 32)
```
2. **Run locally first** (ngrok/cloudflared for a public URL while testing):
```bash
bun run http # → :3000/mcp
```
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 }`.)
4. **claude.ai → Settings → Connectors → Add custom connector**:

| Field | Value |
|---|---|
| Name | `CMA` |
| URL | `https://<your-deploy>/mcp` |
| Authentication | Bearer token → your `CMA_MCP_TOKEN` |

> **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.)

5. **Test**: new chat → enable the `CMA` connector → same prompt as above.

---

## Relay mode (recommended Project instructions)

Without guidance, Desktop Claude will try to answer the user itself instead of relaying. Put this in a Project's custom instructions:

> 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.

---

## Gotchas

### stdio = Desktop only; HTTP = the internet

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.

### Long turns vs tool timeout

`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.

### Billing

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.

### Deliberately not exposed

| Endpoint | Why not |
|---|---|
| `agents.archive` | Permanent, no undo — one bad tool call bricks a prod agent |
| `agents.create` / `update` | Authoring belongs in the `ant` CLI / Console, not a chat turn |
| `sessions.delete`, `environments.*` | Destructive infra ops |
| `vaults.*`, `credentials.*` | Secrets |
| `sessions.resources.add` | Needs tokens/file IDs the chat user won't have |

Adding any of these is a few lines in `src/cma.ts` + `src/server.ts` if your use case needs them.

---

## Debugging

| Symptom | Check |
|---|---|
| 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. |
| `CLAUDE_ENVIRONMENT_ID is required` | Env block missing from the Desktop config — stdio servers don't read your shell's env. |
| `wait_for_idle` returns empty `reply` | Agent went idle without emitting text (e.g. only tool calls). `list_events` shows the full log. |
| Every turn starts a new session | Claude isn't threading `session_id`. Tighten the Project instructions. |
| claude.ai Connector shows "couldn't connect" | URL wrong, server not reachable, or token mismatch (check server logs for 401). |
| HTTP server returns 401 locally | `CMA_MCP_TOKEN` not exported in the shell you ran `bun run http` from. |
Loading
Loading