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