Email as an agent tool: MCP, the SDK, or your own webhook – MailKite

gabe_lalasava2 pts0 comments

Email as an agent tool: MCP, the SDK, or your own webhook — MailKite<br>Beta<br>50% off your first year — founding rate for the first 1,000 customers.<br>View pricing →

Sign in · Start free

All posts<br>“Give the agent email” isn’t one decision — it’s two, receiving and sending, and for sending there’s a real choice between the model deciding to send (a tool call) and your code deciding (a function call). Pick wrong and you either hard-code something the agent should choose, or hand the model a capability that should have been deterministic. Here are the three wirings and what each one is for.

Three wirings, two jobs

MCP server<br>the model calls tools<br>send · read · manage<br>send · when the<br>agent decides

SDK<br>mk.send({ ... })<br>deterministic code<br>send · when your<br>program decides

Webhook<br>signed JSON in<br>verifyWebhook<br>receive · the moment<br>mail arrives<br>Receiving is always the webhook. Sending is your choice: a tool the model calls (MCP),<br>or code your program runs (SDK). Most agents mix: receive by webhook, send by whichever fits.<br>Blue = operated by MailKite.

Three ways to give an agent email. Receiving is the webhook; sending is either a tool the model chooses (MCP) or deterministic code (SDK). Real agents combine them.

The fastest one to show is MCP, because it’s a single command and then email is just tools the model has:

# install the hosted MailKite MCP server (Claude Code shown; any MCP client works)<br>claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp<br>After that the model can send, read messages, and manage domains as tool calls it decides to make — no glue code. The runnable versions of all three wirings, including a webhook receiver and a mixed receive-by-webhook-send-by-SDK loop, are in demo-email-agent-tool: clone it and npm start — one command boots the webhook receiver and self-fires a signed email.received event at its own localhost, so the receive loop runs with no domain and no account (open it in StackBlitz to watch it in a browser tab). Here’s each wiring and exactly when it’s the right one.

1. MCP: email as tools the model calls

Use MCP when the agent should decide whether and when to touch email. You install the server once and the model gets a set of tools — send a message, list or read inbound, manage domains and routes — and calls them as part of its reasoning, the same way it calls any other tool. There’s no code path in your app that says “send now”; the model chooses.

Good fit for MCPOpen-ended assistants where emailing is one of many things the agent might do: "email the customer a summary if they asked for one," "check the inbox for the approval and proceed." The hosted server is at mcp.mailkite.dev with OAuth, plus there's a Claude Code plugin.

2. The SDK: email as deterministic code

Use the SDK when your program decides. If the rule is “when a run finishes, email the result,” that’s not a judgment call the model should make on the fly — it’s a line of code. The SDK gives you mk.send() and the full receive-verify path, in the agent’s own language:

// deterministic: your code decides to send, not the model<br>import { MailKite } from "mailkite";<br>const mk = new MailKite(process.env.MAILKITE_API_KEY);

async function onRunComplete(run) {<br>await mk.send({<br>from: "agent@yourco.dev",<br>to: run.requestedBy,<br>subject: `Done: ${run.title}`,<br>html: renderReport(run), // your template, your data<br>});<br>This is also the right choice when you want a send to be testable, logged, and rate-limited by your own code rather than left to the model’s discretion. mk.send() returns { id, status } so you can record the outbound message. (Can’t take a dependency? Raw HTTP against the REST API works too — prefer the SDK, drop to raw only if you must.)

3. Your own webhook: email the agent receives

Receiving is always the webhook, whichever way you send. A signed JSON payload arrives the instant mail hits the agent’s address; you verify it in one call and hand the decoded body to the model:

import express from "express";<br>import { MailKite } from "mailkite";

const app = express();<br>const SECRET = process.env.MAILKITE_WEBHOOK_SECRET;<br>app.use("/hooks/agent", express.raw({ type: "application/json" }));

app.post("/hooks/agent", async (req, res) => {<br>// HMAC signature, replay window, constant-time compare — one call<br>if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], req.body, SECRET)) {<br>return res.sendStatus(401);<br>res.sendStatus(200);

const event = JSON.parse(req.body);<br>if (event.type !== "email.received") return;

// decoded text/html + an auth verdict — hand it to the model as untrusted INPUT<br>await runAgent({ task: event.text, from: event.from.address, auth: event.auth });<br>});

app.listen(3000);<br>That handler is what npm start boots. Fire a signed email.received event at it and watch the decoded text, from, and auth verdict land — no account needed:

No public URL to host at all? A route with action: 'agent' runs the receive-think-reply loop on a managed queue instead — covered in why we built...

agent send email model mailkite code

Related Articles