CLI & MCP
Drive your workspace from the terminal with @mailcrate/cli, or hand it to an AI agent as an MCP server.
@mailcrate/cli is one package with two faces. On a terminal it is a command
line: mailcrate push, mailcrate list-campaigns, mailcrate send-email. To an
AI agent it is an MCP server: the same
operations, exposed as tools, over mailcrate mcp.
Both talk to the same public API over HTTPS with a workspace API key. There is no separate agent API and no server-side setup.
Install
npm install -g @mailcrate/cliOr run it without installing:
npx @mailcrate/cli list-campaignsAuthenticate
mailcrate login --api-key <api_key>That writes ~/.mailcrate/config.json (mode 0600). In CI, set environment
variables instead — they take priority over the file:
export MAILCRATE_API_KEY=<api_key>
export MAILCRATE_API_URL=https://api.mailcrate.ioWhat it can do
Run mailcrate --help for the current list, and
mailcrate <command> --help for one command's flags. Broadly:
| Area | Commands |
|---|---|
| Templates | push, list, get-email-template, create-email-template, update-email-template, delete-email-template, render-template-preview, render-html-preview |
| Contacts | find-contact, upsert-contact, list-contact-deliveries, get-contact-subscriptions, set-contact-subscription |
| Sending | send-email, send-email-batch |
| Campaigns | list-campaigns, get-campaign, create-campaign, create-campaign-follow-up, send-campaign, update-campaign, reopen-campaign, cancel-campaign-schedule, get-ab-test-results, select-ab-winner, delete-campaign |
| Journeys | list-journeys, get-journey, enroll-contact, exit-contact |
| Deliverability | search-deliveries, get-delivery-events, check-suppression, add-suppression, list-suppressions, delete-suppression |
| Segments | list-segments, get-segment-members |
| Subscription groups | list-subscription-groups, get-subscription-group |
| Webhooks | list-webhook-endpoints, get-webhook-endpoint, create-webhook-endpoint, update-webhook-endpoint, delete-webhook-endpoint, regenerate-webhook-secret, list-webhook-deliveries, get-webhook-delivery, get-webhook-delivery-attempts |
| Sending domains | list-sending-domains, create-sending-domain, check-sending-domain-verification, delete-sending-domain, set-tracking-domain, check-tracking-domain-verification |
| Sender identities | list-sender-identities, create-sender-identity, check-sender-identity-verification |
| Hosted pages | list-hosted-page-domains, create-hosted-page-domain, check-hosted-page-domain-verification, delete-hosted-page-domain |
| Workspace | get-workspace, update-workspace |
The MCP server exposes a curated subset of these; the CLI renders all of them. See MCP server below.
Pushing templates
The CLI's original job. Point it at a file or a directory of
React Email templates; each is matched to Mailcrate by
its templateSlug export.
mailcrate push ./emails/
mailcrate push ./emails/welcome.tsx --slug welcome-email
mailcrate push ./emails/ -y # create missing templates without promptingRate limits are handled for you
A push spends two writes per template it has to create, so a large repo can meet
the per-key write budget on the Rate Limits page. The CLI
waits it out instead of failing: it honours Retry-After, paces itself off
X-RateLimit-Remaining before it gets there, and tells you at the end that it
was slowed rather than broken.
Done: 30 created, 0 pushed, 0 unchanged, 0 skipped, 0 failed
Slowed by rate limits: waited 24s (3 retried, 2 paused to stay under the limit). This push was slower, not broken — nothing failed because of it.That line is not a failure and the exit code is still 0; under --json the
same information is a throttled block on the result, present only when
something was throttled. A key that stays over its limit does eventually give
up — after four attempts, or immediately if the API asks for a wait longer than
a minute — and says how long it waited before it did.
Machine-readable output
Every command except login takes --json, which prints the API's raw result
and nothing else:
mailcrate list --json | jq -r '.data[].slug'Failures print { "error": { "message", "statusCode", "body" } } to stderr, with
the API's own response body inside — including the class-validator messages
that name the field it rejected.
Recipes for CI
Stand up a sending domain
Registering a domain returns the DNS records to publish. Registering the same domain twice is safe — you get the existing one back, with its tokens unchanged — so this can live in a provisioning script that runs every deploy.
mailcrate create-sending-domain mail.example.com --json \
| jq -r '.dnsRecords[] | [.type, .name, .value] | @tsv'Publish those records, then poll. check-sending-domain-verification exits
non-zero until the domain verifies, so a shell loop is the whole implementation:
until mailcrate check-sending-domain-verification "$DOMAIN_ID"; do sleep 30; done
mailcrate create-sender-identity --name "Acme" --email hello@mail.example.com \
--sending-domain-id "$DOMAIN_ID"check-hosted-page-domain-verification and
check-tracking-domain-verification follow the same exit-code convention.
Two things to know before scripting this: deleting a domain and re-creating it mints new tokens, so every record you published becomes stale; and the public API cannot delete a sender identity, which also means a domain that has one can never be deleted through the API.
Register a webhook endpoint
The signing secret is returned once, by create and
regenerate-webhook-secret. Every read masks it, so capture it in the same
step:
mailcrate create-webhook-endpoint --json \
--name "CI" --url https://hooks.example.com/mailcrate \
--subscribed-events delivery.bounced \
--subscribed-events delivery.complained \
| jq -r .secret--subscribed-events is repeatable and validated against the known event
names — the API itself accepts any string, so a typo there would subscribe you
to an event that never fires.
Send a batch
Batch sending reads a JSON file, because five hundred messages are not
something a command line can express. The file is a JSON array of messages, or
an object with a messages array; each message takes the same fields as
send-email.
mailcrate send-email-batch ./messages.json -y --json | jq '.failed'The API answers 200 even when individual messages were rejected, so the
HTTP status is not the answer. The command exits non-zero if any message
failed, and the result carries the index of each failure so you know which one
to fix.
Exit codes
0 means the command did what you asked. 1 means it did not — including the
cases where the request itself succeeded: a batch with failures, a domain that
is not verified yet, a campaign whose preflight refused to send. That is what
makes && and until work without parsing output.
MCP server
mailcrate mcp speaks the Model Context Protocol over stdin and stdout, so an
agent can work your workspace directly. For an agent that cannot start a local
process — claude.ai, Managed Agents — use the
hosted MCP server instead.
Register it once:
claude mcp add mailcrate -- mailcrate mcpThe equivalent JSON, for any client that configures MCP servers that way:
{
"mcpServers": {
"mailcrate": {
"command": "mailcrate",
"args": ["mcp"],
"env": {
"MAILCRATE_API_KEY": "<api_key>",
"MAILCRATE_API_URL": "https://api.mailcrate.io"
}
}
}
}Some clients do not pass your shell environment through
An MCP client usually spawns the server with a filtered environment, so
MAILCRATE_API_KEY exported in your shell may not reach it. Either set it in the
client's own env block as above, or run mailcrate login once so the key is
read from ~/.mailcrate/config.json. With no key at all the server exits
immediately and says so, unless you start it with --docs-only, which serves
just the documentation tools and needs no key.
What the agent sees
Each tool carries the same description a human gets from --help, plus
annotations describing what it does:
readOnlyHint— the tool changes nothing (find_contact,get_campaign,render_template_preview).destructiveHint— the tool can cause mail to leave, or change something that cannot be put back (send_email,send_campaign,enroll_contact).
Not every command is exposed as a tool. create-email-template is left out
because push creates missing templates itself, and a tool whose only use is
making an empty template an agent then has to remember to fill is a way to get
half-finished templates rather than mail.
push is available here — it reads files from the machine the server runs
on, which is your own machine in the stdio case. That is also why it will not
appear on the hosted MCP server, where the filesystem is ours.
What a tool returns
The API's JSON result, compactly serialised — no pretty-printing, because the indentation would be paid for out of the agent's context window and read by a parser rather than a person.
A single value longer than 20,000 characters is cut short and marked inline with
…[truncated], and the response carries a second note saying so. In practice
that only affects render-template-preview and render-html-preview, which
return a whole email's HTML. The number of items in a list is governed by
limit, not by this.
Irreversible tools need explicit confirmation
Annotations are advisory — the MCP specification says clients should not make
tool-use decisions based on them, and not every client shows them at all. So
anything that sends mail additionally takes a confirm parameter and
refuses to run without confirm: true — whether it was set to false or left
out altogether:
{
"error": {
"message": "send_email makes changes that cannot be undone, so it requires confirm: true. Re-send the same arguments with confirm set to true if that is what you intend.",
"statusCode": null,
"body": null
}
}A model cannot send your campaign by accident; it has to state in the arguments that it meant to.
Which key to give an agent
The server can only do what its key can do, so the key is the real control.
- Exploring, reporting, drafting — a read-only key. Grant the
:readscopes for the areas the agent should see (contact:read,campaign:read,delivery:read,email-template:read). Write tools still appear in the tool list, but the API rejects them with a403naming the missing scope, and the agent sees that verbatim. - Sending — add only the write scopes you actually intend, such as
sending:writefor transactional mail. full_access— only when the agent is genuinely meant to do everything, and only for a first-party integration you trust. It bypasses every per-resource check.
See API Keys & Scopes for the full list.
Rotate the key, not the config
Revoking an API key in the dashboard immediately stops every client using it, including any running MCP server. There is nothing to un-install.