MailKite
Start free
All posts
Gabe 9 min read

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

There are three ways to give an agent email, and they answer different questions. Install the MCP server and the model sends and reads mail as tool calls it decides to make. Hand it the SDK and email is deterministic code your agent runs. Point a signed webhook at it and the agent receives mail the moment it arrives. This is when to use each — with runnable code for all three, and how to mix them. For developers wiring an agent to email with MailKite.

“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 the model calls tools send · read · manage send · when the agent decides SDK mk.send({ ... }) deterministic code send · when your program decides Webhook signed JSON in verifyWebhook receive · the moment mail arrives Receiving is always the webhook. Sending is your choice: a tool the model calls (MCP), or code your program runs (SDK). Most agents mix: receive by webhook, send by whichever fits. 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)
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp

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; open it in StackBlitz. 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
import { MailKite } from "mailkite";
const mk = new MailKite(process.env.MAILKITE_API_KEY);

async function onRunComplete(run) {
  await mk.send({
    from: "agent@yourco.dev",
    to: run.requestedBy,
    subject: `Done: ${run.title}`,
    html: renderReport(run),          // your template, your data
  });
}

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";
import { MailKite } from "mailkite";

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

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

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

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

app.listen(3000);

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 programmable email for agents. But when you host the agent yourself, this handler is the receive half of every wiring above.

Mixing them: the common shape

Most real agents combine wirings. The typical one is receive by webhook, send by whichever fits the decision: inbound arrives at your handler, the model reasons over it, and it replies either through an MCP tool call (if the model is choosing to reply) or through mk.send() (if your code always replies). Here’s the decision in one grid.

Receiving always the webhookor an action:'agent' route if you won't host an endpoint Sending — who decides? the modelMCP tools"reply if theyasked a question" your codethe SDK"always email theresult on finish" Common mix: receive by webhook, send by SDK for deterministic replies or MCP for model-chosen ones.
Which wiring to use. Receiving is the webhook; sending splits on who decides — the model (MCP) or your program (the SDK). Most agents receive by webhook and send by whichever matches the decision.

Where I won’t overclaim

MCP is not automatically the right answer just because you’re building an agent. Handing the model a send tool means giving it discretion over who gets email and when, and for a lot of flows that discretion is a liability, not a feature — a deterministic mk.send() your code controls is easier to test, log, rate-limit, and reason about. Reach for MCP when the agent genuinely owns the decision; reach for the SDK when you do. And if you can’t take a dependency at all, the raw REST API is there — the SDK just saves you the HMAC and the boilerplate. The point isn’t “use all three,” it’s “these are the three, and they answer different questions.”

FAQ

Should an agent send email through MCP or the SDK? Through MCP when the model should decide whether and when to send — it’s one of the tools it reasons with. Through the SDK when your program decides at a known point (“email the result when the run finishes”). Model-chosen sends want discretion; deterministic sends want code you can test and rate-limit.

How do I install the MailKite MCP server? It’s hosted at mcp.mailkite.dev with OAuth. In Claude Code: claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp. Any MCP-speaking client can connect the same way, and there’s also a Claude Code plugin. After that, sending, reading messages, and managing domains are tool calls the agent can make.

How does the agent receive email — is that MCP too? Receiving is the webhook: a signed JSON payload arrives at your handler the moment mail hits the agent’s address, and you verify it with MailKite.verifyWebhook(). The model can also read the inbox on demand through MCP tools, but push delivery of new mail is the webhook (or a managed action: 'agent' route if you don’t want to host an endpoint).

Can I use the webhook and MCP together? Yes, and that’s the common shape: receive by webhook, then reply by an MCP tool call (model-chosen) or mk.send() (deterministic). The wirings compose — receiving and sending are separate decisions.

Do I need a dependency for any of this? No. The SDK (mailkite on npm/PyPI/RubyGems, plus Go, PHP, Java, and a CLI) saves you the HMAC verification and request boilerplate, but the REST API works over plain HTTPS if you’d rather not add one. Prefer the SDK; drop to raw HTTP only if you must.


Receiving is the webhook; sending is a tool the model calls or code your program runs — pick by who owns the decision. Clone demo-email-agent-tool (or run it in your browser) for all three wirings and the mixed loop, then point a domain at MailKite and wire email to your agent.

Related: why we built programmable email for agents, the AgentMail alternative for AI agents, and agent inbox security by design.

Discuss this post: Hacker News Share on X

Related posts