Skip to content
AgentMail
AgentMail
Agent frameworks

Send & Receive Emails with Mastra

Use the AgentMail MCP server directly in Mastra to send and receive emails

Mastra agents get email two ways: through Mastra’s MCP client, which discovers the tool catalog from AgentMail’s hosted server, or through tools you define over the TypeScript SDK. This walkthrough uses the MCP path. Follow it and your agent ends up with its own @agentmail.to inbox, an email it sent, and a reply to one it received.

0. Get API keys and a project

  • AGENTMAIL_API_KEY authenticates the email tools. Generate one from the AgentMail Console.
  • OPENAI_API_KEY authenticates the model calls. The agent on this page names an OpenAI model, and Mastra’s model router reads this variable to call it. Generate one from the OpenAI dashboard.
export AGENTMAIL_API_KEY="<API_KEY>"
export OPENAI_API_KEY="<OPENAI_API_KEY>"

An existing Mastra app works as the project, and npm create mastra@latest scaffolds a new one. The scripts on this page also run in a plain Node.js project.

1. Connect the AgentMail MCP server

Install Mastra’s core and MCP packages. In a scaffolded Mastra project, @mastra/core and the "type": "module" setting are already in place.

npm install @mastra/core @mastra/mcp
npm install -D tsx
npm pkg set type=module

tsx runs the TypeScript scripts in the next steps, and "type": "module" lets them use top-level await.

Point an MCPClient at the hosted server. It authenticates with your API key in the x-api-key header:

mcp.ts
import { MCPClient } from "@mastra/mcp";

export const mcp = new MCPClient({
  servers: {
    agentmail: {
      url: new URL("https://mcp.agentmail.to/mcp"),
      requestInit: {
        headers: { "x-api-key": process.env.AGENTMAIL_API_KEY! },
      },
    },
  },
});

Check the connection by listing the tools it discovers. Save this next to mcp.ts and run it with npx tsx check.ts:

check.ts
import { mcp } from "./mcp";

console.log(Object.keys(await mcp.listTools()));
Sample output
[
  'agentmail_list_inboxes',
  'agentmail_get_inbox',
  'agentmail_create_inbox',
  'agentmail_list_threads',
  'agentmail_get_thread',
  'agentmail_send_message',
  'agentmail_reply_to_message',
  ...
]
Terminal output of npx tsx check.ts printing 26 tool names prefixed agentmail_

listTools() prefixes each tool with the server name from your config, so the agentmail key becomes the agentmail_ prefix here. Every tool call runs as the organization that owns the API key. To limit an agent to a single inbox, give it an inbox-scoped key instead of an organization key. MCP and Skills covers the hosted server’s full catalog and credential options.

2. Test sending emails

Define the agent with the MCP tools, in agent.ts:

agent.ts
import { Agent } from "@mastra/core/agent";
import { mcp } from "./mcp";

export const emailAgent = new Agent({
  id: "email-agent",
  name: "Email agent",
  instructions:
    "You are an email agent with your own AgentMail inbox. " +
    "You can create inboxes, send email, and read and reply to threads.",
  model: "openai/gpt-5.6-sol",
  tools: await mcp.listTools(),
});

Ask the agent to create an inbox and email an address you can check right now, in Gmail, Outlook, or whichever client you keep open. Save this next to agent.ts and run it with npx tsx send.ts.

Once the script runs, the email turns up in that account within moments, from a fresh @agentmail.to address.

send.ts
import { emailAgent } from "./agent";

const output = await emailAgent.generate(
  "Create an inbox with the display name 'Support agent', then send an email " +
    "from it to you@example.com with the subject 'Hello from my agent' and a " +
    "one-line introduction. When you finish, include the inbox address in " +
    "your answer.",
);
console.log(output.text);

The agent calls agentmail_create_inbox, then agentmail_send_message, and reports back:

Sample output
I created the inbox faircost773@agentmail.to and sent your email to
you@example.com with the subject "Hello from my agent".

Save the inbox address from the output. You will email it in the next step.

This prompt leaves the username to AgentMail, which generates an available one.

3. Test replying to an email

From your everyday account, send an email to the inbox address from step 2. Give it a subject and a question the agent can answer.

Then ask the agent to read and answer it. You should get the reply in your account, threaded under the message you sent.

reply.ts
import { emailAgent } from "./agent";

const output = await emailAgent.generate(
  "Fetch the newest thread in <inbox_id>, read it, and post a brief reply " +
    "in that thread.",
);
console.log(output.text);

Replace <inbox_id> with the address from step 2. The script starts from a blank conversation, with no memory of the earlier run, so the prompt has to name the inbox itself.

The agent finds the conversation with agentmail_list_threads, reads it with agentmail_get_thread, and answers with agentmail_reply_to_message. Expect roughly two seconds of transit in each direction.

Define your own tools with the SDK

MCP hands the agent the whole hosted catalog. For a hand-picked tool surface, use createTool over the AgentMail TypeScript SDK instead. Install it with npm install agentmail.

email-tools.ts
import { createTool } from "@mastra/core/tools";
import { AgentMailClient } from "agentmail";
import { z } from "zod";

const agentmail = new AgentMailClient();

export const sendEmail = createTool({
  id: "send-email",
  description: "Send an email from an AgentMail inbox.",
  inputSchema: z.object({
    inboxId: z.string().describe("Address of the inbox to send from"),
    to: z.string().describe("Recipient address"),
    subject: z.string(),
    text: z.string().describe("Plain-text body"),
  }),
  execute: async ({ inboxId, to, subject, text }) => {
    return await agentmail.inboxes.messages.send(inboxId, { to, subject, text });
  },
});

export const replyToEmail = createTool({
  id: "reply-to-email",
  description: "Reply to an email an AgentMail inbox received, in the same thread.",
  inputSchema: z.object({
    inboxId: z.string().describe("Address of the inbox that received the email"),
    messageId: z.string().describe("message_id of the email to reply to"),
    text: z.string().describe("Plain-text reply body"),
  }),
  execute: async ({ inboxId, messageId, text }) => {
    return await agentmail.inboxes.messages.reply(inboxId, messageId, { text });
  },
});

AgentMailClient() picks up AGENTMAIL_API_KEY from the environment. execute receives the validated input as its first parameter, so the schema fields destructure directly. Both tools return the new message’s messageId and the threadId it belongs to, which the agent can quote back or use to follow up in the same thread.

These drop into the same tools map on the agent, alone or alongside the MCP tools:

tools: { sendEmail, replyToEmail },

Give the agent only this map and its entire reach is sending and replying from inboxes you name.

Scope tools per user

The agent in step 2 bakes one organization’s tools in at construction. When each user of your product has their own AgentMail credentials, keep the agent tool-free and attach that user’s tools per request: build an MCPClient with the user’s API key and pass toolsets to generate or stream.

tenant.ts
import { Agent } from "@mastra/core/agent";
import { MCPClient } from "@mastra/mcp";

const tenantAgent = new Agent({
  id: "tenant-email-agent",
  name: "Tenant email agent",
  instructions: "You manage the email inbox of the user you are acting for.",
  model: "openai/gpt-5.6-sol",
});

export async function runForTenant(tenantApiKey: string, prompt: string) {
  const mcp = new MCPClient({
    servers: {
      agentmail: {
        url: new URL("https://mcp.agentmail.to/mcp"),
        requestInit: {
          headers: { "x-api-key": tenantApiKey },
        },
      },
    },
  });
  try {
    const output = await tenantAgent.generate(prompt, {
      toolsets: await mcp.listToolsets(),
    });
    return output.text;
  } finally {
    await mcp.disconnect();
  }
}

listToolsets() returns the same tools grouped by server, in the shape the toolsets option expects. disconnect() releases the client when the request is done. Skipping it makes the next MCPClient with the same configuration throw, because Mastra treats a duplicate live client as a memory leak. Multi-tenant platforms covers provisioning a per-user inbox and a key scoped to it.

Next Steps

Was this page helpful?Suggest editsRaise issue