MCP
MiniMailer ships a native MCP (Model Context Protocol) server. An AI agent connects to it and can send email, check suppressions, read inbound mail, and run campaigns as first-class tool calls — no custom integration code, no HTTP client glue, no prompt-pasted API docs.
Connect
The server speaks Streamable HTTP at:
https://minimailer.app/mcp/v1
Authenticate one of two ways:
- OAuth — MCP clients that support the OAuth flow (Claude, and most desktop clients) can connect and authorize interactively.
- API token — send a personal access token with the
mcp:usescope as a bearer header. Right for headless agents and CI.
For Claude Code:
claude mcp add --transport http minimailer https://minimailer.app/mcp/v1
For clients configured by JSON, the shape is the usual:
{ "mcpServers": { "minimailer": { "type": "http", "url": "https://minimailer.app/mcp/v1", "headers": { "Authorization": "Bearer <token>" } } } }
MCP traffic shares the REST API's rate limits — an agent burning tokens can't starve your own API usage beyond your tier's budget.
Discovery
Agents that find MiniMailer on their own, rather than being handed a config, can read everything they need from the host itself:
/auth.md— the authentication recipe: how to register an OAuth client, which grant to run, and where a personal access token comes from./.well-known/mcp/server-card.json— the MCP server card: transport, endpoint, authorization servers, capabilities./.well-known/agent-skills/index.json— an Agent Skills index with aminimailerskill an agent can load to learn the send, receive, and campaign tools./.well-known/ai-catalog.json— the Agentic Resource Discovery catalog listing the MCP server, the OpenAPI description, the skill, and thellms.txtdocs index.
The OAuth authorization-server and protected-resource metadata documents sit at their standard well-known locations and are described in the API reference.
Protocol revisions
The server speaks MCP revision 2026-07-28 and answers the earlier initialize handshake as well, so a client written against either revision connects without changes. The list responses (tools/list, prompts/list, and server/discover) carry cache hints, so a client can keep the tool roster for an hour instead of asking for it on every session.
Tools
| Tool | What it does |
|---|---|
send-email |
Send a transactional email from a verified domain |
check-suppression |
Check whether an address is suppressed for a domain |
remove-suppression |
Remove a non-permanent suppression |
get-email-stats |
Delivery/open/click stats for a sent email |
list-email-events |
Resumable feed of delivery events across your sends |
list-inbound-emails |
Recent inbound email summaries, with verdicts |
get-inbound-email |
One inbound email in full (headers, bodies) |
send-campaign |
Dispatch a prepared campaign |
list-campaigns |
List campaigns and their statuses |
get-campaign-stats |
Aggregate stats for a campaign |
Each tool also advertises a human-readable title (for example "Send an email"), which clients use in their tool pickers and permission prompts; the wire name in the table is what an agent calls.
The server also exposes resources an agent can read for context — your verified sending domains (sending-domains) and your inboxes (inboxes) — and a prompt (compose-transactional-email) that guides a model through composing a well-formed transactional message.
Tool inputs mirror the REST API: a send takes to, subject, html/text (at least one), optional cc/bcc copy recipients (up to 10 each), optional reply_to addresses (up to 10, on the sending domain's family), optional attachments — files and inline images, each given as base64 content or an https URL in path, with a content_id to embed an image in the HTML body (the same shapes and limits as attachments on the REST API) — and resolves the sender from from or domain; when from is omitted the sender defaults to noreply@{domain}. Ownership is enforced exactly as in the REST API: an agent can only ever send from domains on the authenticated account.
get-inbound-email returns the parsed message: from, to, subject, body_html, body_text, headers, attachment metadata, verdicts (the spam, virus, spf, dkim and dmarc checks run on arrival), status, the message's own message_id header, and remote_message_id — the identifier the receiving mail system assigned to the message on arrival, the same value the inbound email resource exposes.
The sender controls the from address and name, the subject, the bodies, the headers and the attachment names, and an agent that reads them can be steered by them. Give a reading agent a token without emails:send, or read Prompt injection for the other options.
What each tool tells your agent
A tool is more than a name and a description. Every primitive on this server carries the metadata an MCP client needs to call it safely and to trust what comes back:
- Behaviour hints.
check-suppression,get-email-stats,list-email-events,list-inbound-emails,get-inbound-email,list-campaigns, andget-campaign-statsare declared read-only and idempotent — a client may retry them freely and may run them without asking.send-emailandsend-campaignare declared as reaching the outside world: they put mail in someone's inbox, and a client should treat them as the consequential calls they are.remove-suppressionis declared destructive, because it re-opens an address to delivery. - Structured results. Every tool result carries
structuredContentthat matches the tool's advertisedoutputSchema, so a client can validate fields — a send id, a status, a cursor — without parsing text. The text content is still there for models that read it, but the structured copy is the one to program against. - Input constraints in the schema. Enumerations (event types, suppression reasons), ranges (
limitbounds, recipient counts), and formats (email addresses, ISO 8601 timestamps) are advertised in each tool's input schema, so a client that honours the schema never sends a call the server would reject. - Argument completion on the prompt. The
compose-transactional-emailprompt offers completions for its arguments — the email types and tones it knows — so an agent can pick a valid value instead of guessing one.
Following delivery outcomes without an endpoint
Webhooks are the right way to learn what happened to your mail, but they need a URL, and an agent does not always have a server to host one. list-email-events is the pull-shaped alternative: it returns delivery events across your sends — oldest first — and every call hands back a next_cursor. Pass that cursor as since on the next call and you get everything recorded in between, whether that was a minute or a week ago.
{ "since": "evt_01JZX8K7Q2M3N4P5R6S7T8V9WB", "types": ["email.bounced", "email.complained"], "limit": 100 }
The type values are the same event names the webhook payloads use, so a handler you wrote for email.bounced works on either path without translation. Bounce events additionally carry bounce_type, and only Permanent means the address is gone for good.
The cursor walks the order events were recorded, not the order they are timestamped. That difference matters: providers do not report delivery events in chronological order, so a feed paged by timestamp would step past events that arrive late and never show them again. Paging by next_cursor means being away does not cost you events.
Two details worth knowing. The feed trails real time by a few seconds, deliberately, so that an event still being written cannot slip in behind a cursor you have already passed. And a cursor records a position in the feed, not a query — if you change the types filter, start from a fresh cursor, because events your old filter skipped sit below the position you reached and resuming will not bring them back.
Webhooks stay the better choice when you do have an endpoint, because they arrive without being asked and retry on their own. The event feed exists so that not having one is no longer a reason to fly blind.
Built for agents, honestly
Two behaviours are specific to the MCP path:
- AI disclosure is on by default. Sends through the MCP server attach a machine-readable
X-AI-Generatedheader marking the message as AI-originated. An explicitai_disclosure: falseopts out. The header does not replace any human-facing disclosure your use case requires in the email body — it complements it. - Per-agent reputation. Pass an
agent_id(up to 128 chars,[A-Za-z0-9_-.]) on sends and MiniMailer tracks reputation per agent, not just per domain. One misbehaving agent surfaces in its own metrics — and inagent.*webhooks — before it can drag down everything else you send, and an agent whose reputation degrades is rate-capped on its own rather than eating your account's allowance. How reputation works →
Examples
See MCP examples for copy-paste agent prompt recipes — password resets, receipts, replying to inbound mail, attributing sends to an agent, and dispatching campaigns.
Errors
Tool calls return structured errors, not stack traces: an unverified or unowned domain reads as not found, an inactive domain names its current status, and free-tier quota exhaustion says exactly that. Agents can act on the message text without parsing HTML or guessing at HTTP semantics.
Those are in-band tool errors: the call succeeded at the protocol level and the tool answered with a result flagged as an error. A prompt called without one of its required arguments (compose-transactional-email without email_type) is different — it is rejected before the prompt runs, as a protocol-level JSON-RPC error with code -32602 (invalid params) on an HTTP 400, exactly as a malformed request would be.