# MCP server

Add Mimeo to Claude Code, Cursor or any MCP client, and your agent can operate your whole Mimeo — author flows, write emails, define segments, look people up, read reports, push definitions.

The server runs **on your Mimeo**, at `/mcp`. There's nothing to install locally and nothing to keep updated: the tools an agent sees always match the version your Mimeo is actually running.

## Connecting

**If you use the [manager repo](/content/docs/manager-repo/index.html), this is** **already done.** The repo ships a committed `.mcp.json` that `mimeo init` points at your Mimeo; any agent opening the repo picks it up. The one thing it needs from you is the token in your environment — the committed file deliberately says `${MIMEO_TOKEN}` instead of a credential:

```
export MIMEO_TOKEN="$(cat .mimeo/token)"   # or from your secret manager
```

To use the server somewhere _other_ than the manager repo — a different project, a global agent config — create a token under **Settings → Agents & API** and register it directly:

```
claude mcp add --transport http mimeo https://your-mimeo.com/mcp \
  --header "Authorization: Bearer mm_your_token"
```

Don't do both in the same repo — two registrations of the same server just confuse the client. The token is the same one the [HTTP API](/content/docs/api/index.html) and the [CLI](/content/docs/cli/index.html) take, and it has full access — keep it in a secret manager rather than a committed file.

## What it can do

| Area | Tools |
| --- | --- |
| Reference | `describe_schema` — every node type, condition type and operator, plus the custom fields, tags, products, segments, event names and named durations _this_ install has. Start here before writing anything. |
| Flows | `list_flows` (narrow to a group with `label`) · `get_flow` · `save_flow` (writes `labels` too) · `validate_flow` · `dry_run_flow` · `activate_flow` · `pause_flow` |
| Emails | `list_emails` · `get_email` · `save_email` · `preview_email` · `send_test_email` · `archive_email` · `delete_email` · `fork_email` |
| Media | `list_media` · `get_media` · `update_media` · `delete_media` · `get_media_storage` · `check_media_storage` |
| Templates | `list_templates` · `get_template` · `save_template` (a layout's `theme` too) · `make_default_layout` · `archive_template` · `delete_template` |
| Themes | `list_themes` (with the whole variable vocabulary) · `get_theme` · `save_theme` (partial by default; `replace: true` sends the whole set) · `make_default_theme` · `delete_theme` |
| Sequences | `list_sequences` · `get_sequence` · `save_sequence` · `add_sequence_step` · `update_sequence_step` · `replace_sequence_step_email` · `remove_sequence_step` · `toggle_sequence` |
| Segments | `list_segments` · `get_segment` · `save_segment` · `preview_segment` |
| Guards | `list_guards` · `save_guard` · `toggle_guard` |
| Settings | `get_settings` · `save_settings` — tunables, [send windows](/content/docs/api/settings.html#send-windows/index.html), and the footer's mailing address (`sending.mailing_address`); how mail _looks_ is a theme's job, not a setting's. Shape-checked, credentials never |
| Broadcasts | `list_broadcasts` · `create_broadcast` · `schedule_broadcast` · `unschedule_broadcast` · `cancel_broadcast` |
| People & events | `lookup_person` · `record_event` · `list_events` · `list_event_types` · `list_queue` |
| Reporting | `report` — for an email, sequence, flow or broadcast, over any period |
| Definitions | `pull_definitions` · `diff_definitions` · `apply_definitions` |

## Labels group flows

Flows carry freeform labels — `onboarding`, `revenue`, `win-back` — that change nothing about how a flow runs and everything about finding it again. `list_flows` returns each flow's labels plus every label in use; pass `label` to list only one group.

`save_flow` takes `labels` as the whole set — send every label the flow should carry, or `[]` to clear them. Leave the field out and the existing labels are untouched, so a definition rewrite never quietly drops them. Read the existing vocabulary before coining a new label: two words for one idea make the grouping useless.

## The event stream is readable

`record_event` writes the stream; two tools read it back. `list_events` is the feed, newest first — everything on the instance, whether your systems sent it, the engine emitted it (`tag_added`, `flow_entered` and friends), or an import recorded it — filterable by `name`, `source` (`api`, `system`, `import`), `email` and `since`, up to 200 at a time (default 50).

**Set `historical: true` on anything you're** **backfilling.** Recording an event normally wakes the automation engine, which is right for something that just happened and wrong for history — replaying a few thousand old opt-ins would start a few thousand journeys and mail all of them. A historical event is recorded in full (timeline, money tables, `event_occurred` conditions) but starts no flow and opens no gate. `list_events` takes the same flag as a filter, so you can check what a backfill actually landed.

`record_event`'s `person` object also carries `subscribed_on`, the date someone joined the list. Only an earlier date replaces a stored one, and the event's `occurred_at` never sets it — so backfilling old history through this tool can't rewrite your list's join dates. `lookup_person` returns it. See [the people API](/content/docs/api/people.html) for the full rule.

`lookup_person` also carries the person's money: `lifetime_spend_cents` and their `subscriptions`, in [the same shape the people API](/content/docs/api/people.html) — including `cancels_at` when a cancellation is scheduled but the subscription is still live.

`list_event_types` is the registry: every event name this install has seen, with occurrence counts, sources and first/last seen. Names register themselves on first ingest, so this is the live trigger vocabulary for flows, gates and segments — the same names `describe_schema` returns as `event_names`. Pass `name` and one name comes back in full detail instead: the merged payload shape (every key ever sent, its JSON types, how many occurrences carried it, and one real example) plus everything listening for it — flows by trigger, gate or fire, segments by `event_occurred`. A name that has never been seen is a tool error, not an empty object.

Read the registry before writing a trigger: it's the difference between guessing what an event is called and what it carries, and knowing.

## Two things it deliberately won't do

### Flows are whole documents

There is no `insert_step`, no `move_step`, no `delete_step`. Those exist in the admin UI, where a human clicking **+** is placing exactly one step. An agent's flow edit nearly always spans several steps at once, and a per-step tool is a way to leave a flow half-restructured between two validated states — with real people running through it in between.

So the loop is: `get_flow` → rewrite the definition → `validate_flow` → `dry_run_flow` → `save_flow`. Ids are preserved exactly as given and never renumbered; leave the `id` off a genuinely new step and Mimeo mints one above every id that flow has ever used.

### Attaching an email is a decision, and it gets asked

Every tool that attaches an email takes `email_source`, and it is **required with no default**:

| Value | What it means afterwards |
| --- | --- |
| `scratch` | A blank new row in the library. Nothing else points at it. |
| `shared` | The library row named by `email_id` itself. A later edit reaches **every send that uses it**, and all their opens and clicks count toward the one email. |
| `copy` | An independent copy, subject kept. Edits stay here, its stats start at zero, and everyone who already received the original counts as not having received this one. |

An agent that omits it is guessing on the operator's behalf about whether a later edit reaches one send or twenty — so the server refuses rather than picking. And a **broadcast never shares**: `create_broadcast` doesn't offer `shared` in its schema, and the server refuses it even if asked. What one broadcast sent should stay what it sent.

`fork_email` is the repair when something is shared that should have been a copy: it splits one holder off onto a row of its own and leaves every other holder on the original.

## Reading back what you wrote

`preview_email` returns the email as it would land: components expanded, buttons rendered, markdown converted, the layout wrapped around it and Liquid resolved against one real person. Tracking is the one thing it leaves out, because a preview is never a send.

Pass `body_markdown` (and optionally `template_id`) to render a draft that is never saved. That's the check before the write — whether a component expands, whether a conditional resolves the way it was meant to — answered without the email having to carry the draft first. Pass `person_id` for someone specific, or `segment_id` to render for somebody who would actually receive it, whose merge fields are the real ones.

`send_test_email` is the other half: the compiled email actually delivered to one address, which is the only way to exercise the provider, the sending domain and whatever the receiving client does with the HTML. It's marked as a test, so nobody's sent-status or place in a sequence moves — but it puts a real message in a real inbox, and the address becomes a person if it isn't one already. Confirm the address with the operator rather than picking one, and read the response as _accepted_ rather than _delivered_: those are different facts, and only the first one is known yet.

## Sorting out a library that has filled up

Every broadcast mints its own email row, so the library accumulates rows that existed for one send. `list_emails` takes the same filter the admin listing does:

```
list_emails  used_in: ["broadcasts", "sequences", "flows", "flows_one_off"]
             used_mode: "any" | "all" | "none"
```

`used_mode: "none"` answers "what does nothing point at any more". `flows` means reached by a flow at all — including through a sequence it starts — while `flows_one_off` means only a flow's own `send_email` step. Every row also carries `where_used`, so you can see what editing it would reach before you edit it.

Naming those rows is the other half of the tidying. `save_email` takes a `title` — what the email is called in the library and in a sequence's steps, never seen by a recipient — separate from `subject`, which is the subject line itself. Reading, `title` always answers "what is this called" and falls back to the subject; `title_raw` is `null` when nobody has named it. So an agent can label a row of one-off broadcast emails without rewriting a single subject line. See [Title and subject](/content/docs/api/emails.html#title/index.html).

The follow-through, once you've found what nothing points at:
`archive_email` retires a row while keeping its history intact — the default retirement, and the only option for anything that has been sent — and `delete_email` permanently removes a row with no send history and no holders at all. Deleting is refused, with the holders named, for anything else.

The media tools work the [media library](/content/docs/usage/media.html) — the uploaded images, PDFs and zips email content references by hosted URL. `list_media` and `get_media` read it, `update_media` keeps an image's alt text current, and `delete_media` removes a file nothing should reference any more (already-sent emails keep the reference and lose the file behind it). An image inserts into `body_markdown` as `` with its stored alt text; a PDF or zip as `[filename](url)`. Uploading new files is the one media ability that stays on the HTTP surface — `POST /api/v1/media`, multipart — see the [Media API](/content/docs/api/media.html).

`get_media_storage` answers where uploads will serve from and whether uploads are possible at all — worth reading before uploading anything. `check_media_storage` goes further and proves it: it uploads a small probe file, fetches it back over the public host, then deletes it. Neither exposes storage credentials; those are entered in Settings and never cross the MCP or HTTP surface. Running the check before a send is cheap insurance, because a media URL that 404s is frozen into every email referencing it and already-sent mail can't be repointed.

The template tools work the layouts and components an email is dressed in. `save_template` both creates and updates: omit `id` and say which `kind` to make a new one, and a layout created without markup or a stylesheet starts from a working default of both rather than a refusal — a plain, readable email you can edit down. A layout must carry the `{{ content }}` slot the body is injected at — save one without it and the error says so, naming the slot it found instead, which is usually the one another tool's export brought along. `make_default_layout` moves the default (there is exactly one, and the previous holder steps down), `archive_template` retires or restores one, and `delete_template` removes it outright — refused for the default layout until another one holds that job. Saving markup recompiles every email built on it. See [the Templates API](/content/docs/api/templates.html) for the same abilities over HTTP.

A layout's stylesheet isn't written in hex codes and pixel values — it's written against a **theme**, as tags: `{{ theme.body_text_color }}` for the light value, `{{ theme.dark.body_text_color }}` for the dark one, and `{{ theme.brand-b.link_color }}` to reach across to another theme by key. They resolve at compile time, before Liquid runs, and a tag naming a variable that doesn't exist is refused when the layout is saved, with the bad tag named. Which theme a layout reads is `save_template`'s `theme` field — a theme's key, or `null` to follow whichever theme is the default — and the theme tools above are how you change what those tags resolve to. Editing a theme recompiles every email on it, so a brand colour moves everywhere at once, drafts included. [The Themes API](/content/docs/api/themes.html#variables/index.html) lists the variables.

## Files or MCP?

Not a capability split — the tool list above is mostly write tools, and both surfaces go through the same API and the same validation, so they can't disagree. It's a paper-trail split: prefer the [CLI and a manager repo](/content/docs/cli/index.html) when a change should be reviewed as a diff and live in git history — building a flow, rewriting a sequence, editing copy — and prefer MCP for questions and for changes that don't need a commit: looking someone up, counting a segment, firing a test event, pausing a flow. The longer version, with the rest of the map: [How it fits together](/content/docs/how-it-fits/index.html).

## Protocol notes

Streamable HTTP, protocol version `2025-06-18`, negotiating down to `2025-03-26` and `2024-11-05`. `POST /mcp` answers with a single JSON object; `GET` and `DELETE` return `405`, because the server has nothing to push between calls and an SSE stream that never speaks is worse than none. The `Origin` header is validated on every request.

A tool that fails answers with `isError` and a message explaining what to do instead — not a transport error, because the model is meant to read it and try something else.

See also: [CLI](/content/docs/cli/index.html) · [API overview](/content/docs/api/index.html) · [Flow schema](/content/docs/reference/flow-schema.html)

Next: [The API →](/content/docs/api/index.html)
