MailKite
Get your API key
All guides Agent email

Agent email with the Gmail API

Give an AI agent email and it can hold a real conversation — field a support request, follow up on a thread, confirm a booking. This guide wires an agent into a Gmail account with the Gmail API, then shows the same thing the MailKite way, where the agent gets email as native tools over MCP.

What you'll need

  • A Google Cloud project with the Gmail API enabled.
  • OAuth 2.0 credentials, and a Google account that consents to the scopes.
  • An agent runtime that can call tools (function calling or MCP).
  • For real-time inbound: a Cloud Pub/Sub topic and a public HTTPS endpoint.

Part 1 — Give your agent a Gmail mailbox

1. Authenticate

The Gmail API acts on behalf of a Google account over OAuth 2.0. Create a Google Cloud project, enable the Gmail API, and set up OAuth credentials. The account owner then consents to the scopes your agent needs — narrow scopes like gmail.send keep the grant tight, but restricted scopes such as gmail.modify pull you into Google's OAuth verification review before you can go past a handful of test users:

oauth
# 1. Create a Google Cloud project and enable the Gmail API.
# 2. Configure the OAuth consent screen and create OAuth 2.0 credentials.
# 3. Have the account owner consent to the scopes your agent needs:

https://www.googleapis.com/auth/gmail.send # send only
https://www.googleapis.com/auth/gmail.modify # read, label, modify

# Exchange the consent for tokens, then call the API with the access token:
Authorization: Bearer $GOOGLE_ACCESS_TOKEN

2. Send from the agent

Expose sending as a tool. Gmail takes the whole message as a base64url-encoded RFC 2822 MIME string in the raw field, POSTed to users.messages.send — so the tool assembles the MIME itself:

gmail-send.js
// The agent's send tool. Build an RFC 2822 message, base64url-encode it,
// and POST it as "raw" to users.messages.send.
function base64url(str) {
return Buffer.from(str)
.toString("base64")
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

async function gmailSend({ to, subject, html }) {
const mime =
`To: ${to}\r\n` +
`Subject: ${subject}\r\n` +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/html; charset=UTF-8\r\n\r\n" +
html;

await fetch(
"https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.GOOGLE_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ raw: base64url(mime) }),
},
);
}

3. Receive

Two ways in. Poll with users.messages.list and users.messages.get, or subscribe the mailbox to Cloud Pub/Sub with users.watch for real-time notifications:

receive
// Poll: list message ids, then fetch each one for headers and body.
GET https://gmail.googleapis.com/gmail/v1/users/me/messages?q=is:unread
GET https://gmail.googleapis.com/gmail/v1/users/me/messages/{id}

// Real-time: subscribe the mailbox to a Cloud Pub/Sub topic. Gmail
// publishes { emailAddress, historyId } when the mailbox changes; renew
// the watch at least every 7 days.
POST https://gmail.googleapis.com/gmail/v1/users/me/watch
{ "topicName": "projects/your-project/topics/gmail", "labelIds": ["INBOX"] }

A watch notification carries a historyId, not the mail — your push endpoint calls history.list to find what changed, fetches the message, and hands it to the agent's loop:

handle-inbound.js
// Your Pub/Sub push endpoint. Gmail sends a historyId, not the message —
// call history.list since your last id to find what arrived, then get it.
app.post("/hooks/gmail", async (req, res) => {
const { emailAddress, historyId } = JSON.parse(
Buffer.from(req.body.message.data, "base64").toString(),
);
const changes = await gmailHistoryList(emailAddress, historyId);
for (const msg of changes) {
agent.handle({ from: msg.from, subject: msg.subject, body: msg.text });
}
res.sendStatus(200);
});

That's an agent on Gmail: OAuth into the account, wrap send and the inbound flow as tools, and let your agent run the conversation.

Part 2 — The same, the MailKite way

MailKite gives the agent its own address on your domain — no OAuth app to verify, no Pub/Sub topic, no base64 MIME to assemble by hand. It's MCP-native, so your agent doesn't call a REST API you wrapped — it speaks MCP and gets email as first-class tools. Connect the server once and the same send-and-reply loop is a few tool calls.

1. Connect the MCP server

Add the hosted server. On the first connect it signs you in over OAuth in the browser — no key to copy — or pass your account key for headless runs:

connect
# Hosted MCP server — OAuth in the browser on first connect, no key to copy
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp

# Headless / CI — pass your account key instead of the browser flow
claude mcp add --transport http mailkite https://mcp.mailkite.dev/mcp \
--header "Authorization: Bearer mk_live_..."

Any MCP client — Cursor, Claude Desktop, Cline, Zed — takes the same URL:

mcp config
{
"mcpServers": {
"mailkite": { "url": "https://mcp.mailkite.dev/mcp" }
}
}

2. Send from your agent

The agent calls mailkite_send over any verified domain — HTML, text, cc/bcc, attachments, and in-thread replies. Pick AI in the card for the agent's view, or any language to make the same send from your own code:

send
import { MailKite } from "mailkite";

const mk = new MailKite(process.env.MAILKITE_API_KEY);

const { id, status } = await mk.send({
from: "hello@myapp.ai",
to: "ada@example.com",
subject: "Your invoice #1042",
html: "<p>Thanks! Receipt attached.</p>",
});
Install Docs →
response
202 Accepted
{ "id": "msg_2Hk9…", "status": "queued" }

3. Receive — or hand the whole inbox to the agent

Every address on the domain is receivable. The agent can read inbound with its own tools, or you turn an address into an inbox agent with one route — the model reads and replies on its own:

create the agent route
import { MailKite } from "mailkite";

const mk = new MailKite(process.env.MAILKITE_API_KEY);

// One route turns an address into an AI inbox agent — the prompt
// is the program, and unresolved mail escalates to a human.
await mk.createRoute({
match: "support@myapp.ai",
action: "agent",
agentPrompt:
"Answer billing and account questions for myapp.ai. Reply in-thread. " +
"Escalate anything you can't resolve to humans@myapp.ai.",
agentForwardTo: ["humans@myapp.ai"],
});
Install Docs →
read inbound
// Or read inbound yourself — the agent has tools for it
mailkite_list_messages({ limit: 20 }) // newest first
mailkite_get_message({ id: "msg_2Hk9…" }) // full body, headers, attachment links

Same key manages the domain too — mailkite_create_domain, mailkite_verify_domain — so the agent can provision its own inbox end to end. See Connect your agent for the full tool list.

A little about MailKite

MailKite is programmable email for developers and AI agents. Connect the MCP server once and your agent can send over a verified domain, read its inbound as clean JSON, spin up new inboxes, manage domains, and become an inbox agent that answers mail on its own — all as native tools, no REST wrapper to maintain. It works the same over the SDKs, the CLI, or raw HTTP, and agent inboxes are on the free tier.

Next: Connect your agent for every MCP tool, or Set up your agent's email to go from zero to a working inbox.