# MCP server

ReelMoney runs a Model Context Protocol server at `https://reel.money/mcp`. Point Claude, Cursor, or any MCP client at it and the model gets 18 tools that do the same work as the REST API, calling the same code underneath, so the two surfaces can never drift.

| | |
| --- | --- |
| Endpoint | `https://reel.money/mcp` |
| Transport | Streamable HTTP, stateless. One self contained exchange per POST. |
| Auth | `Authorization: Bearer rm_your_key`, the same key the REST API takes. |
| Protocol | JSON-RPC 2.0: `initialize`, `tools/list`, `tools/call`. |

There is no OAuth flow yet. The key goes in the header, which every client below supports.

## Connect a client

Claude Code, one line in a terminal:

```bash
claude mcp add reelmoney --transport http https://reel.money/mcp --header "Authorization: Bearer rm_your_key"
```

Claude Desktop, Cursor and anything else that reads a JSON config:

```json
{
  "mcpServers": {
    "reelmoney": {
      "type": "http",
      "url": "https://reel.money/mcp",
      "headers": {
        "Authorization": "Bearer rm_your_key"
      }
    }
  }
}
```

A raw check with curl, useful when a client is quiet about why it failed:

```bash
curl -X POST https://reel.money/mcp \
  -H "Authorization: Bearer rm_your_key" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

## The tools

| Tool | What it does | Kind | Scope |
| --- | --- | --- | --- |
| `get_account` | Read the account behind this API key: masked email, plan name and capabilities (slideshow quota used and remaining), credits when the plan shows them, brands and key scopes. | read only | `brand:read` |
| `get_brand` | Read the brand this key belongs to: name, description, website, language, categories, slideshow settings (default context, slide overrides, append slots) and which social platforms are connected. | read only | `brand:read` |
| `list_media` | Browse media: source own (this brand's uploads), packs (media pack contents) or stock (the shared catalog, searched semantically when you pass a query). | read only | `media:read` |
| `upload_media` | Upload a file in two steps. | writes | `media:write` |
| `create_slideshow` | Start an AI slideshow generation for this brand and get back a job. | writes | `content:write` |
| `update_slideshow` | Edit a generated slideshow: title, caption, tags, aspect ratio, gradient settings or the slides themselves (text overlays are normalized server side). | writes | `content:write` |
| `create_video` | Start a hook and demo (UGC) video: an avatar clip speaks the hook_text, then your product demo clips play. | writes | `content:write` |
| `render_video` | Render a generated slideshow into an MP4 video. | writes | `content:write` |
| `get_job` | Poll a generation or render job by the job_id a create tool returned. | read only | `brand:read` |
| `list_shorts` | List this brand's shorts (slideshows and videos), newest first. | read only | `brand:read` |
| `get_short` | Read one short in full: status, URLs, type_data (slides and overlays for a slideshow) and its shares (posts) so far. | read only | `brand:read` |
| `list_connections` | List the social accounts connected to this brand with token status, posting rate limit headroom and a ready_to_post verdict per account. | read only | `brand:read` |
| `create_connection_link` | Build the OAuth authorize URL a person opens once in a browser to connect a social account to this brand. | read only | `brand:read` |
| `validate_post` | Dry run a post before you commit to it: same arguments as schedule_post, returns a per-account verdict (postable short, fresh tokens, caption and title lengths, platform rate limits, schedule sanity). | read only | `posts:read` |
| `schedule_post` | Publish a short now, or schedule it, to connected accounts. | destructive, ask a person first | `posts:write` |
| `cancel_post` | Cancel a scheduled post (a share id from schedule_post results or get_short) before it goes out. | destructive, ask a person first | `posts:write` |
| `get_analytics` | Read post analytics. | read only | `analytics:read` |
| `explain_error` | Look up any error code another tool or the REST API returned and get its cause, whether a retry can help, and the concrete fix. | read only | any key |

Every tool carries a title and the standard annotations, so a client can tell a read from a write before it calls anything.

## Rules a client should know

- **A missing scope does not hide a tool.** Every key sees all 18 tools. A tool the key lacks the scope for answers at call time with a structured `missing_scope` error that names the scope and how to add it. Predictable beats surprising.
- **`brand_id` picks the brand, when the key allows it.** Every brand facing tool takes an optional `brand_id` argument. Omit it and the tool works on the key's own brand. A key with the `brands:all` scope may pass any brand id the account owns (`get_account` lists them); without the scope a foreign `brand_id` answers a structured `missing_scope` error, and an id the account does not own answers `not_found`.
- **Two tools are marked destructive.** `schedule_post` and `cancel_post` carry `destructiveHint` and `_meta: {"anthropic/requiresUserInteraction": true}`. Publishing to an audience and pulling a post back are decisions a person should confirm. Treat the prompt as the point of the annotation, not as a formality.
- **GET /mcp answers 405.** The server is stateless, so there is no session to attach a server sent event stream to. Use POST. `DELETE /mcp` answers 200 and does nothing, since there is no session to end.
- **Bad key, 401 at the transport.** Authentication happens before any protocol handling, so an invalid key answers HTTP 401 with `WWW-Authenticate: Bearer realm="ReelMoney", error="invalid_token"` and the usual error envelope, not a JSON-RPC error.
- **Rate limits are shared with REST.** The same per key ceilings apply. See [authentication](/api-docs/authentication).

## Result size and pagination

Tool results are token budgeted, around 25k tokens in Claude Code, and a blown budget wastes a whole turn. So:

- Lists come back capped at 20 items with a note. Pass `page` to walk further instead of asking for everything.
- Prefer a list tool with a filter over a list tool without one. `list_shorts` with `short_type` and `status` beats reading the whole library.
- `get_short` returns full detail including slides and overlays. Call it for one record, not in a loop over a list.
- `get_analytics` without arguments returns account summaries; add `social_data_id` only when you need post by post numbers.

## A working loop

1. `get_account`. Read the plan and how much quota is left. Stop here if there is not enough.
2. `get_brand` and, when you need a specific look or a media pack, `list_media`.
3. `create_slideshow` or `create_video`. Both answer with a `job_id`.
4. `get_job`, no faster than `poll_after_seconds`, until it reads `succeeded` or `failed`.
5. `render_video` when a caller wants an MP4 of a slideshow. Never create a second slideshow for that.
6. `list_connections`, then `validate_post`, then `schedule_post`. Read every verdict before you post, and every result after.
7. `get_analytics` a day or two later to see what it did.
8. `explain_error` for any code you do not recognise, instead of guessing from the message.
