Connect your agent
Give your agent a real email address it can send from, receive to, and act on. In Claude Code it's two lines — install the MailKite plugin and authenticate in your browser, no key to copy. Any other MCP client points at the hosted remote server; prefer a static key or offline use? Run it 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.
Step 1 — Get your API key
Every MailKite account gets one key the moment you sign up — a single
mk_live_… token, full scope across all your domains:
it can create & register domains, verify DNS, set webhooks, send, and read
inbound. The same key works for your agent and your own code, so there's
nothing extra to provision.
Open the dashboard → it's shown on your first screen and
under Settings → API key. Copy it and hand it to your agent as
MAILKITE_API_KEY:
MAILKITE_API_KEY=mk_live_3a9f… Treat it like a password — keep it in an env var or secrets vault, never commit it to a repo. Rotate it anytime from the dashboard (the old key stops working at once). Tighter, scoped agent keys are on the roadmap.
Step 2 — Connect your agent
MailKite ships an MCP server that exposes email as tools — and unlike most email MCPs, that includes reading inbound. There are three ways to connect, easiest first: the Claude Code plugin, the hosted remote server (browser OAuth, works in any MCP client), or the local stdio server for a static key or offline use.
Claude Code plugin — one install, no key to copy
The fastest path in Claude Code. Add the marketplace and install the plugin:
/plugin marketplace add mailkite/claude-code
/plugin install mailkite@mailkite
Then run /mcp, pick mailkite → Authenticate, and
sign in through your browser — you'll see ✓ Connected. No
mk_live_… key to paste; OAuth handles it.
Remote MCP server — browser OAuth, any client
Point any MCP client at the hosted endpoint
https://mcp.mailkite.dev/mcp — Streamable HTTP, secured with OAuth 2.1 / PKCE.
The client walks you through sign-in on first connect; nothing to copy.
Claude Code
Register the remote server, then run /mcp → Authenticate to sign in via the browser:
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp Headless, CI, or no browser? Pass a static key as a Bearer header instead:
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp \
--header "Authorization: Bearer mk_live_3a9f…" Cursor, Claude Desktop, Windsurf, Cline & Gemini
These take a remote (type: "http") server block — paste it in, then
restart the client. With OAuth the client handles auth on first connect:
{
"mcpServers": {
"mailkite": {
"type": "http",
"url": "https://mcp.mailkite.dev/mcp"
}
}
} The remote-URL field name differs per client — swap the key, keep the URL:
- Claude Code / Cursor / Zed / Cline / Roo —
url - Windsurf —
serverUrl - Gemini (HTTP) —
httpUrl - VS Code / Copilot — top-level
serverswith"type": "http"
Prefer a static key over the OAuth flow? Add a
headers object with your mk_live_… token:
{
"mcpServers": {
"mailkite": {
"type": "http",
"url": "https://mcp.mailkite.dev/mcp",
"headers": { "Authorization": "Bearer mk_live_3a9f…" }
}
}
} VS Code & GitHub Copilot
VS Code uses a top-level servers key — drop this into .vscode/mcp.json (or your user MCP config) and use Copilot's agent mode:
{
"servers": {
"mailkite": {
"type": "http",
"url": "https://mcp.mailkite.dev/mcp"
}
}
} 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.
Claude Code
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 & Gemini
These all take the same mcpServers block — paste it in, then restart the client:
{
"mcpServers": {
"mailkite": {
"command": "npx",
"args": ["-y", "@mailkite/mcp"],
"env": { "MAILKITE_API_KEY": "mk_live_3a9f…" }
}
}
} - Cursor —
.cursor/mcp.json(project) or~/.cursor/mcp.json(global) — or click Add to Cursor on a deeplink badge - Claude Desktop —
claude_desktop_config.json(Settings → Developer → Edit Config) - Windsurf —
~/.codeium/windsurf/mcp_config.json - Cline / Roo — the MCP Servers panel → Configure (
cline_mcp_settings.json/.roo/mcp.json) - Gemini CLI —
~/.gemini/settings.json
VS Code & GitHub Copilot
VS Code uses 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
Codex 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 20 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. |
| 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. |
| Domains | |
mailkite_list_domains | List your domains, each with its webhook URL. |
mailkite_create_domain | Add a domain; returns the DNS records to set. |
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_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). |
| 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. |
| 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. |
Drop-in AI skill
Prefer a coding agent? The MailKite agent skill wires email into your agent in one step — download the zip, drop it into Claude Code or claude.ai, and it drives the whole lifecycle (domains, DNS, webhooks, sending, inbound) over MCP, the CLI, any SDK, or REST.
Download the skill Install guide →
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.
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.
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.