Connect your agent
Give your agent a real email address it can send from, receive to, and act on.
Point any MCP client at the hosted remote MCP server —
https://mcp.mailkite.dev/mcp — and sign in over OAuth in the
browser, no key to copy. In Claude Code it's two lines with
the MailKite plugin. Prefer a static key or offline use?
Run the server locally. Works with Claude Code, Cursor,
VS Code, Windsurf, Codex, Gemini, and any MCP client. Prefer code? It's the
same REST API as everything else.
Why give an agent an inbox
- Email is your agent's identity. Sign-ups, verification, and 2FA codes land in an inbox the agent owns and reads — no OAuth, no human in the loop.
- Inbound it can act on. Received mail is parsed to clean JSON and kept in a queryable store, so the agent reasons over the full message and replies in-thread.
- One inbox per agent. Unlimited domains and addresses — a fleet of agents means a fleet of inboxes, with no per-domain tax.
Drop-in AI skill
Prefer a coding agent? The MailKite agent skill wires email into your agent with one command — then it drives the whole lifecycle (domains, DNS, webhooks, sending, inbound) over MCP, the CLI, any SDK, or REST. Works in Claude Code, Cursor, Codex, Amp, and any agent that reads Agent Skills:
npx skills add mailkite/agent-skills Install the MailKite agent skill. Download
https://github.com/mailkite/claude-code/releases/latest/download/mailkite.zip
and unzip it into ~/.claude/skills/ (user-wide) or .claude/skills/ (this project), then load it. View on GitHub Install guide →
How much email should the agent control?
You decide the agent's reach — the whole account, one domain, or a single mailbox — and each grant is revocable on its own:
Account key
*
Every domain, every mailbox
One key runs all your email: the agent can create domains, set webhooks, send and read everywhere — unlimited domains and mailboxes. For your own trusted automation.
Domain-scoped key
*@agents.yourapp.com
Every mailbox on one domain
The agent sends, reads, and replies on that domain and nothing else — the rest of your account is out of reach. Mint one per agent; revoke one without disturbing the others.
Agent route
support@yourapp.com
One mailbox
A route hands one address's mail to your agent — that inbox is all it sees and all it replies from. The narrowest grant: the agent never holds a key at all.
The first two grants are API keys — step 1 below. The third needs no key on the
agent's side: a route with action
agent hands one address's inbound to a built-in
inbox agent that reads and replies in-thread
there, and nowhere else.
Step 1 — Get your API key
MailKite has two kinds of key, both mk_live_… tokens — the first
two rungs of the ladder above:
- Account key — one per account, unrestricted across every domain: it can create & register domains, verify DNS, set webhooks, send, and read inbound. Great for getting started and for your own trusted code.
- Domain-scoped key — tied to a single domain: it can send from it, manage it, and read its mail and routes, and nothing else. It can't touch your other domains, mint keys, or change account-wide settings. Recommended for agents — and the right choice for anything in production or multi-tenant, where each agent (or tenant) should be confined to its own domain and revocable on its own.
Open the dashboard. The account key is shown on your first
screen and under Settings → API key. To mint a domain-scoped key,
go to API keys, pick a domain, give it a label (e.g.
support agent), and create it. Hand whichever you chose to your agent as
MAILKITE_API_KEY — the rest of this guide works the same either way:
MAILKITE_API_KEY=mk_live_3a9f… Treat the account key like a superadmin password — it's here for convenience, to get you up and running immediately. For production, mint a domain-scoped key per agent so each one is confined to its own domain and revocable on its own, without disrupting everything else. Either way, keep it in an env var or secrets vault, never commit it to a repo, and rotate the account key or revoke any scoped key anytime from the dashboard (it stops working at once).
Step 2 — Connect your agent to MailKite Remote MCP
MailKite's hosted remote MCP server exposes email as tools — including reading inbound, which most email MCPs skip. Any MCP client that speaks Streamable HTTP connects to one URL:
https://mcp.mailkite.dev/mcp Pick your agent below and copy the snippet. Every path signs you in over OAuth in the browser — no key to paste — and if you're already in the dashboard it connects with no extra login. The server is open source at github.com/mailkite/mailkite-mcp.
Using a chat-based agent (claude.ai, ChatGPT, your own assistant)? Paste this into the prompt — it adds the server and walks you through browser sign-in.
Connect to the MailKite MCP server (Streamable HTTP) at
https://mcp.mailkite.dev/mcp and authenticate in the browser when prompted.
Then use the mailkite_* tools to send, read inbound, and reply to email. Run both in Claude Code, one after the other. Then /mcp → mailkite signs you in. You also get the /mailkite:* commands and the setup skill.
/plugin marketplace add mailkite/claude-code /plugin install mailkite@mailkite Prefer the bare CLI over the plugin? One claude mcp add registers the remote server (add -s user for every project). Then /mcp → mailkite signs you in over OAuth — no key to paste.
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp Any MCP client — Cursor, Claude Desktop, Cline, Roo, Zed. Paste the block, restart, and sign in on first connect.
{
"mcpServers": {
"mailkite": {
"type": "http",
"url": "https://mcp.mailkite.dev/mcp"
}
}
} Windsurf uses a serverUrl field — drop this into ~/.codeium/windsurf/mcp_config.json and restart.
{
"mcpServers": {
"mailkite": {
"serverUrl": "https://mcp.mailkite.dev/mcp"
}
}
} Gemini CLI uses httpUrl in ~/.gemini/settings.json. Same idea for any other MCP client — keep the URL, swap the field name.
{
"mcpServers": {
"mailkite": {
"httpUrl": "https://mcp.mailkite.dev/mcp"
}
}
} No browser (CI, servers)? Skip OAuth and pass your account key as a Bearer header — Claude Code, or any client's config.
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp \
--header "Authorization: Bearer mk_live_3a9f…" {
"mcpServers": {
"mailkite": {
"type": "http",
"url": "https://mcp.mailkite.dev/mcp",
"headers": { "Authorization": "Bearer mk_live_3a9f…" }
}
}
}
On VS Code, Codex, or an offline/local server? Those are just
below. Whichever path you pick, the same mailkite_* tools appear.
Run the server locally — static key, offline, or a custom base URL
Prefer a static key, offline use, or a custom base URL? Run
@mailkite/mcp locally over stdio via npx (no install;
needs Node 18+). The key from step 1 goes in as MAILKITE_API_KEY.
Unlike the remote server it runs fully offline —
mailkite_verify_webhook needs no network at all — and it honors
MAILKITE_BASE_URL to target a staging API.
One command registers it — add -s user to enable it across every project.
claude mcp add mailkite \
-e MAILKITE_API_KEY=mk_live_3a9f… \
-- npx -y @mailkite/mcp Cursor, Claude Desktop, Windsurf, Cline, Roo & Gemini all take the same mcpServers block — paste it into the client's config and restart. Where it lives: Cursor .cursor/mcp.json (project) or ~/.cursor/mcp.json (global) · Claude Desktop claude_desktop_config.json (Settings → Developer → Edit Config) · Windsurf ~/.codeium/windsurf/mcp_config.json · Cline / Roo the MCP Servers panel → Configure · Gemini CLI ~/.gemini/settings.json.
{
"mcpServers": {
"mailkite": {
"command": "npx",
"args": ["-y", "@mailkite/mcp"],
"env": { "MAILKITE_API_KEY": "mk_live_3a9f…" }
}
}
} VS Code & GitHub Copilot use a top-level servers key with "type": "stdio".
{
"servers": {
"mailkite": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@mailkite/mcp"],
"env": { "MAILKITE_API_KEY": "mk_live_3a9f…" }
}
}
} Codex CLI keeps MCP servers in TOML.
[mcp_servers.mailkite]
command = "npx"
args = ["-y", "@mailkite/mcp"]
env = { MAILKITE_API_KEY = "mk_live_3a9f…" }
Already use the CLI? mailkite mcp launches
the same local server with your saved token.
Whichever path you pick, the 23 mailkite_* tools below appear — the
five inbox-agent tools are rolling out in
preview. The server is a thin layer over the
MailKite SDK and the same contract it uses: one
tool per endpoint, with inputs validated against the shared JSON Schemas
before any request is made — so a tool can't put a malformed call on the wire.
A couple of tools (like mailkite_verify_webhook) run locally,
with no API call at all.
What your agent can do
| Tool | What it does |
|---|---|
| Sending | |
mailkite_send | Send a message over a verified domain — HTML, text, cc/bcc, attachments, and in-thread replies via inReplyTo. |
mailkite_send_batch | Send one personalized message per recipient (up to 50) in a single call — per-recipient templateData/headers merged over the shared base; per-recipient results. |
mailkite_upload_attachment | Upload a file and get a secure, time-limited URL to attach in send (filename + url) or link inline — instead of base64-inlining large files. |
| Inbound messages | |
mailkite_list_messages | List stored messages, newest first. |
mailkite_get_message | Fetch one message in full — body, headers, deliveries, and attachment links. |
mailkite_retry_delivery | Re-deliver a stored message to its webhook. |
mailkite_create_realtime_token | Mint a short-lived, single-use token for one Realtime API connection (GET /v1/realtime). The stream itself is not an MCP tool — a tool call cannot hold a subscription open. |
| Domains | |
mailkite_register | Create an account from just an email — returns the API key; sending stays blocked until the address is verified. |
mailkite_me | The account behind this credential: email, verification state, plan. |
mailkite_register_oauth_client | Register an OAuth client (RFC 7591) to link a user's existing MailKite account. |
mailkite_exchange_oauth_token | Exchange an authorization code for an access token, or rotate a refresh token. |
mailkite_get_api_key | Get the account's API key (read-or-create). |
mailkite_rotate_api_key | Rotate the account API key; the old one stops working immediately. |
mailkite_list_scoped_keys | List domain-scoped API keys (one per site/CI job). |
mailkite_create_scoped_key | Create a key limited to one domain. |
mailkite_delete_scoped_key | Revoke a domain-scoped key. |
mailkite_list_app_passwords | List app passwords — mailbox credentials scoped to a domain + address pattern. |
mailkite_create_app_password | Create an app password for one domain and address pattern (imap and/or api). |
mailkite_update_app_password | Change an app password's label, address pattern or protocols. Its domain is fixed for life. |
mailkite_rotate_app_password | Replace an app password's secret, keeping its scope. The old secret stops working immediately. |
mailkite_delete_app_password | Revoke an app password; IMAP sessions and API calls using it stop immediately. |
mailkite_list_mailbox_messages | List a mailbox's messages, newest first — an agent reading its own inbox over HTTPS. |
mailkite_get_mailbox_message_raw | Fetch one message's raw RFC822 bytes from a mailbox. |
mailkite_set_mailbox_message_flags | Replace a message's IMAP flags (e.g. mark it Seen). |
mailkite_get_usage | This period's email/AI usage vs the plan's included bucket. |
mailkite_list_suppressions | List addresses this account will not send to. |
mailkite_add_suppression | Suppress an address (unsubscribe, bounce, complaint, or manual). |
mailkite_remove_suppression | Remove an address from the suppression list. |
mailkite_list_domains | List your domains, each with its webhook URL. |
mailkite_create_domain | Add a domain; returns the DNS records to set. |
mailkite_suggest_subdomain | Suggest a free, unclaimed subdomain label to prefill an input with. |
mailkite_check_subdomain | Check whether a free subdomain label can be claimed. |
mailkite_claim_subdomain | Claim a free MailKite subdomain (<label>.<base>) — verified instantly, no DNS to publish. |
mailkite_get_domain | Get one domain with DNS records + webhook. |
mailkite_verify_domain | Re-check DNS and update verification status. |
mailkite_delete_domain | Remove a domain. |
mailkite_check_domain_availability | Check whether a domain is available to register, with pricing. |
mailkite_register_domain | Register a domain through MailKite and auto-provision its DNS. |
| Webhooks | |
mailkite_set_webhook | Set or replace the domain's catch-all webhook. |
mailkite_delete_webhook | Remove the domain's webhook. |
mailkite_set_webhook_events | Opt the domain's inbound webhook into engagement events — one webhook, all events; pass "all" or a list of email.* types. |
mailkite_delete_webhook_events | Opt back out of engagement events on the inbound webhook (email.received only — the default). |
mailkite_set_tracking_webhook | Split-endpoint override: send engagement events to a dedicated URL instead of the inbound webhook. |
mailkite_delete_tracking_webhook | Remove the domain's dedicated tracking-event webhook. |
mailkite_test_webhook | Send a signed test event to the webhook. |
mailkite_verify_webhook | Verify an x-mailkite-signature header on an inbound delivery — runs locally, no API call. |
| Routes | |
mailkite_list_routes | List inbound routing rules. |
mailkite_create_route | Create a route (match, action, destination) — action is webhook, forward, store, drop, or agent (hand inbound to an inbox agent). |
mailkite_delete_route | Delete an inbound routing rule by id — pair with create_route to register and tear down a webhook destination (e.g. an automation subscribing on enable, cleaning up on disable). |
mailkite_agent | Send a message to one of your inbox agents and get its reply — defaults to the account's default agent; routeId/address pick a specific one, model overrides the model. |
mailkite_route | Route a message to one of your registered routes (by routeId or address), running that route's action — agent, webhook, or forward. |
| Templates | |
mailkite_list_templates | List your saved email templates. |
mailkite_list_base_templates | List the built-in base templates you can start from. |
mailkite_get_template | Fetch one template (subject + rendered HTML/text). |
mailkite_create_template | Save a new email template, then send it with templateId + templateData. |
| Contact lists | |
mailkite_list_lists | List your contact lists (static, curated broadcast audiences), each with its member count. |
mailkite_create_list | Create a contact list; add contacts with add_list_contacts. |
mailkite_get_list | Get one contact list with its member count. |
mailkite_update_list | Rename a contact list. |
mailkite_delete_list | Delete a contact list — the list is removed, the contacts themselves are kept. |
mailkite_list_list_contacts | List the contacts that are members of a list. |
mailkite_add_list_contacts | Add contacts (by id, ctr_…) to a list; returns how many were newly added. |
mailkite_remove_list_contact | Remove one contact from a list (the contact itself is kept). |
| Broadcasts | |
mailkite_list_broadcasts | List your broadcasts (one-to-many sends) with status and send stats. |
mailkite_create_broadcast | Create a broadcast draft — from is required; set audience to { type: "all" } or a list. |
mailkite_get_broadcast | Get one broadcast with its status and recipient summary. |
mailkite_update_broadcast | Edit a draft broadcast (from/subject/audience/html/…). Drafts only. |
mailkite_delete_broadcast | Delete a broadcast draft. |
mailkite_send_broadcast | Send a broadcast now, or pass an ISO 8601 scheduledAt to schedule it — a one-click unsubscribe is always added. |
| Documentation | |
mailkite_semantic_search | Semantic search over the MailKite documentation — the most relevant doc sections for a natural-language query. |
| Inbox agents (preview) | |
mailkite_create_agent | Create a built-in inbox agent — instructions, reply mode, spam, tags, escalation. Preview. |
mailkite_list_agents | List your inbox agents. Preview. |
mailkite_get_agent | Fetch one inbox agent's config. Preview. |
mailkite_update_agent | Update an inbox agent's config. Preview. |
mailkite_delete_agent | Remove an inbox agent. Preview. |
Run your own agent loop
If you run your own agent loop, route the
inbound webhook straight into it, then take the
agent's response and hand it to the SDK's
send with inReplyTo (the
Send API under the hood) to respond in-thread:
Don't want to host a loop at all? A built-in inbox agent can triage, reply, filter spam, tag, and escalate in MailKite's pipeline — no server to run. It runs on Claude at a flat $0.10 per action, or free on your own Anthropic key. Running your own agent over the webhook is free too — you just pay your model provider.
Dashboard → Routes → [ + Add route ]
Match support@myapp.ai
Receiver Webhook
URL https://myapp.ai/hooks/mailkite
Click [ Add route ]
# Inbound to that address now POSTs to your handler,
# which runs the agent and replies (code tabs →).Create a webhook route for support@myapp.ai
that delivers to https://myapp.ai/hooks/mailkite.import { MailKite } from "mailkite";
const mk = new MailKite(process.env.MAILKITE_API_KEY);
// Hand inbound mail to your agent loop, then send its reply.
app.post("/hooks/mailkite", async (req, res) => {
res.sendStatus(200); // ack fast
const event = req.body;
if (event.type !== "email.received") return;
// 1. Let the agent reason over the message and produce a reply.
const reply = await agent.run({
role: "user",
content: `New email from ${event.from.address}: ${event.subject}\n\n${event.text}`,
});
// 2. Feed the agent's response straight to the Send API, in-thread.
await mk.send({
from: event.to[0].address, // the agent's own address
to: event.from.address,
subject: `Re: ${event.subject}`,
text: reply.content,
inReplyTo: event.threadId, // keeps it in the same conversation
});
});import os
from mailkite import MailKite
mk = MailKite(os.environ["MAILKITE_API_KEY"])
# Hand inbound mail to your agent loop, then send its reply.
@app.post("/hooks/mailkite")
def mailkite():
event = request.get_json()
if event["type"] != "email.received":
return "", 200
# 1. Let the agent reason over the message and produce a reply.
reply = agent.run(
role="user",
content=f"New email from {event['from']['address']}: "
f"{event['subject']}\n\n{event['text']}",
)
# 2. Feed the agent's response straight to the Send API, in-thread.
mk.send({
"from": event["to"][0]["address"], # the agent's own address
"to": event["from"]["address"],
"subject": f"Re: {event['subject']}",
"text": reply.content,
"inReplyTo": event["threadId"], # keeps it in the same conversation
})
return "", 200<?php
$mk = new \MailKite\Client(getenv('MAILKITE_API_KEY'));
// Hand inbound mail to your agent loop, then send its reply.
$event = json_decode(file_get_contents('php://input'), true);
http_response_code(200); // ack fast
if (($event['type'] ?? '') === 'email.received') {
// 1. Let the agent reason over the message and produce a reply.
$reply = $agent->run(
"New email from {$event['from']['address']}: "
. "{$event['subject']}\n\n{$event['text']}"
);
// 2. Feed the agent's response straight to the Send API, in-thread.
$mk->send([
'from' => $event['to'][0]['address'], // the agent's own address
'to' => $event['from']['address'],
'subject' => "Re: {$event['subject']}",
'text' => $reply,
'inReplyTo' => $event['threadId'], // keeps it in the same conversation
]);
}MailKite mk = new MailKite(System.getenv("MAILKITE_API_KEY"));
// Spring Boot — hand inbound mail to your agent loop, then send its reply.
@PostMapping("/hooks/mailkite")
public ResponseEntity<Void> mailkite(@RequestBody Map<String, Object> event) {
if (!"email.received".equals(event.get("type"))) return ResponseEntity.ok().build();
var from = (Map<String, Object>) event.get("from");
var to = ((List<Map<String, Object>>) event.get("to")).get(0);
// 1. Let the agent reason over the message and produce a reply.
String reply = agent.run(
"New email from " + from.get("address") + ": "
+ event.get("subject") + "\n\n" + event.get("text"));
// 2. Feed the agent's response straight to the Send API, in-thread.
mk.send(Map.of(
"from", to.get("address"), // the agent's own address
"to", from.get("address"),
"subject", "Re: " + event.get("subject"),
"text", reply,
"inReplyTo", event.get("threadId") // keeps it in the same conversation
));
return ResponseEntity.ok().build(); // ack fast
}mk := mailkite.New(os.Getenv("MAILKITE_API_KEY"))
// net/http — hand inbound mail to your agent loop, then send its reply.
http.HandleFunc("/hooks/mailkite", func(w http.ResponseWriter, r *http.Request) {
var event map[string]any
json.NewDecoder(r.Body).Decode(&event)
w.WriteHeader(http.StatusOK) // ack fast
if event["type"] != "email.received" {
return
}
from := event["from"].(map[string]any)
to := event["to"].([]any)[0].(map[string]any)
// 1. Let the agent reason over the message and produce a reply.
reply := agent.Run(fmt.Sprintf("New email from %s: %s\n\n%s",
from["address"], event["subject"], event["text"]))
// 2. Feed the agent's response straight to the Send API, in-thread.
mk.Send(mailkite.Message{
From: to["address"].(string), // the agent's own address
To: from["address"].(string),
Subject: "Re: " + event["subject"].(string),
Text: reply,
InReplyTo: event["threadId"].(string), // keeps it in the same conversation
})
})require "mailkite"
mk = Mailkite::Client.new(ENV["MAILKITE_API_KEY"])
# Sinatra — hand inbound mail to your agent loop, then send its reply.
post "/hooks/mailkite" do
event = JSON.parse(request.body.read)
next status 200 unless event["type"] == "email.received"
# 1. Let the agent reason over the message and produce a reply.
reply = agent.run(
"New email from #{event['from']['address']}: " \
"#{event['subject']}\n\n#{event['text']}"
)
# 2. Feed the agent's response straight to the Send API, in-thread.
mk.send(
"from" => event["to"][0]["address"], # the agent's own address
"to" => event["from"]["address"],
"subject" => "Re: #{event['subject']}",
"text" => reply,
"inReplyTo" => event["threadId"] # keeps it in the same conversation
)
status 200 # ack fast
end# Your webhook handler runs the agent loop, then POSTs the reply
# to the Send API — in-thread via inReplyTo:
curl https://api.mailkite.dev/v1/send \
-H "Authorization: Bearer $MAILKITE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "agent@myapp.ai",
"to": "ada@example.com",
"subject": "Re: invoice #1042",
"text": "<the agent reply>",
"inReplyTo": "<a1b2c3@mail.example.com>"
}' Give each agent its own address — a per-address route (mailkite_create_route) or its own domain (mailkite_create_domain) — so its mail, context, and replies stay cleanly separated.
Ready to wire one up? Create an agent inbox or read the API reference.