MailcrateDocs

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/cli

Or run it without installing:

npx @mailcrate/cli list-campaigns

Authenticate

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

What it can do

Run mailcrate --help for the current list, and mailcrate <command> --help for one command's flags. Broadly:

AreaCommands
Templatespush, list, get-email-template, create-email-template, update-email-template, delete-email-template, render-template-preview, render-html-preview
Contactsfind-contact, upsert-contact, list-contact-deliveries, get-contact-subscriptions, set-contact-subscription
Sendingsend-email, send-email-batch
Campaignslist-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
Journeyslist-journeys, get-journey, enroll-contact, exit-contact
Deliverabilitysearch-deliveries, get-delivery-events, check-suppression, add-suppression, list-suppressions, delete-suppression
Segmentslist-segments, get-segment-members
Subscription groupslist-subscription-groups, get-subscription-group
Webhookslist-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 domainslist-sending-domains, create-sending-domain, check-sending-domain-verification, delete-sending-domain, set-tracking-domain, check-tracking-domain-verification
Sender identitieslist-sender-identities, create-sender-identity, check-sender-identity-verification
Hosted pageslist-hosted-page-domains, create-hosted-page-domain, check-hosted-page-domain-verification, delete-hosted-page-domain
Workspaceget-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 prompting

Rate 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 mcp

The 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 :read scopes 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 a 403 naming the missing scope, and the agent sees that verbatim.
  • Sending — add only the write scopes you actually intend, such as sending:write for 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.

On this page