Theme

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, 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 and the CLI take, and it has full access — keep it in a secret manager rather than a committed file.

What it can do

AreaTools
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
Guardslist_guards · save_guard · toggle_guard
Settingsget_settings · save_settings — tunables, send windows, the footer's mailing address (sending.mailing_address) and the unsubscribe page (sending.unsubscribe_page_url — a custom URL to send people on to after unsubscribing — and the sending.unsubscribe_page.* text it shows); 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 · unsubscribe_person · resubscribe_person · add_person_note · record_event · list_events · list_event_types · list_queue
Reportingreport — 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 for the full rule.

Internal notes are not events

add_person_note attaches a plain-text internal note to a person's timeline — bookkeeping about the person (a support exchange, a manual correction, why something was done), where record_event is for things the person did. It takes person (email or id), note (required, up to 10,000 characters), optional structured metadata, and an optional idempotency_key that makes retries safe — a repeated key answers with the already-written note instead of a duplicate.

Notes never trigger automation. They can't start a flow, open a gate, or send mail, and their reserved name never enters the trigger vocabulary — stronger than historical: true, which a flow can opt back into. A note also never creates a person: an unknown email or id is a tool error. It's the same feature as POST /api/v1/people/:id/notes on the HTTP API.

lookup_person also carries the person's money: lifetime_spend_cents and their subscriptions, in the same shape the people API returns — 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_flowdry_run_flowsave_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:

ValueWhat it means afterwards
scratchA 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.

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 — 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 ![alt](url) 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.

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

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 · API overview · Flow schema