Back to Blog
🤖
/17 min read/By SMTPfast Team

How to Send Email From Your AI Agent: Claude, Codex and Cursor With MCP

Let your AI agent send email over MCP: connect Claude Code, Claude Desktop, Codex or Cursor to SMTPfast, with exact configs, real runs and a safe key setup.

mcpai-agentsclaudecodextutorial
Share:𝕏in

After this guide, your AI agent can send an email through SMTPfast, list your sending domains, and tell you whether a message was delivered. It takes a few minutes per client. We cover Claude Code, Claude Desktop, Codex and Cursor, show what real runs from Claude Code and Codex looked like, and then set up an API key and approvals so that the agent sends only what you approve.

What you need

  • An SMTPfast account with a verified sending domain. Every send must come from a verified domain, for agents too.
  • An MCP client: Claude Code, Claude Desktop, the Codex CLI or Cursor.
  • A way to sign in: OAuth from the client, or an SMTPfast API key.

What MCP is

The Model Context Protocol (MCP) is an open standard for connecting AI applications to outside tools and data. A server publishes a list of tools, each with a name, a description and an input schema. The client (Claude, Codex, Cursor) shows those tools to the model and runs the calls the model makes, usually after you approve them.

Our MCP server is at https://smtpfa.st/api/mcp. Each tool call goes through the same REST API that your code uses, so the same scopes, rate limits, suppression list and verified-domain check apply. The server has 104 tools. This guide uses six of them:

Tool What it does
get_capabilities Shows the scopes, plan and rate limit of the connection. Needs no scope.
list_domains Lists sending domains with DKIM, SPF and DMARC status.
check_before_send Checks the sender domain, content and links before a send.
send_email Sends one email.
get_email One email with its status and event history.
explain_email What happened to an email, with the next step if it bounced.

The full list is on the MCP docs page.

Sign in with OAuth or an API key

OAuth needs no key. Add the server URL to your client with no auth header. The client opens our consent page, where you pick a team and approve a list of scopes. The client gets a token that expires after 8 hours and renews itself for up to 30 days. It shows up under API Keys as a connected app, and Disconnect revokes it. The token works only with the MCP endpoint, not with the REST API.

An API key is better when the agent runs without a person at the keyboard (claude -p, codex exec, CI), and when you want to choose the exact scopes. Create one under API Keys in the dashboard. We come back to which scopes to pick in the safety section.

Connect Claude Code

With OAuth:

claude mcp add --scope user --transport http smtpfast https://smtpfa.st/api/mcp

Then start claude, run /mcp, select smtpfast and sign in. You can also run claude mcp login smtpfast from your shell.

With an API key:

export SMTPFAST_API_KEY="sf_..."
claude mcp add --scope user --transport http smtpfast https://smtpfa.st/api/mcp \
  --header 'Authorization: Bearer ${SMTPFAST_API_KEY}'

The single quotes matter. They keep ${SMTPFAST_API_KEY} as a reference in the config, and Claude Code fills it in from your environment when it connects, so the key itself is not written to a file. With --scope project instead of --scope user, the server goes into a .mcp.json in the current folder that you can commit for your team:

{
  "mcpServers": {
    "smtpfast": {
      "type": "http",
      "url": "https://smtpfa.st/api/mcp",
      "headers": {
        "Authorization": "Bearer ${SMTPFAST_API_KEY}"
      }
    }
  }
}

There is also a Claude Code plugin that installs our agent skill and this MCP server (with OAuth) together:

/plugin marketplace add smtpfast/smtpfast-skill
/plugin install smtpfast@smtpfast

Use the plugin or claude mcp add, not both. With both, you get two servers called smtpfast, and each one needs its own sign-in.

Connect Claude Desktop and claude.ai

Claude Desktop connects to remote MCP servers as custom connectors, and the connector is the same in Claude Desktop and on claude.ai:

  1. Open Customize > Connectors.
  2. Click + Add, then Add custom connector.
  3. Name it SMTPfast and enter https://smtpfa.st/api/mcp. Click Continue.
  4. Review the authentication settings Claude found, choose when to sign in, then click Add and sign in on the SMTPfast consent page.
  5. In a chat, turn the connector on from the + button, under Connectors.

The connection to our server comes from Anthropic's cloud, not from your computer. On the Free plan you can add one custom connector. On Team and Enterprise plans, an owner adds the connector under Organization settings > Connectors, and each member connects with their own sign-in. A connector can also send a fixed API key header instead of OAuth, but on a Team plan everyone who uses the connector then shares that key, so OAuth is the better choice there. Anthropic's custom connector guide has the details. We did not run Claude Desktop for this post; these steps follow that guide.

Connect Codex CLI

With an API key:

export SMTPFAST_API_KEY="sf_..."
codex mcp add smtpfast --url https://smtpfa.st/api/mcp --bearer-token-env-var SMTPFAST_API_KEY

That writes this to ~/.codex/config.toml. We added the last line by hand; the safety section explains it.

[mcp_servers.smtpfast]
url = "https://smtpfa.st/api/mcp"
bearer_token_env_var = "SMTPFAST_API_KEY"
default_tools_approval_mode = "writes"

Codex reads the key from that environment variable when it starts, so the key is not in the file.

With OAuth, leave out --bearer-token-env-var:

codex mcp add smtpfast --url https://smtpfa.st/api/mcp

Codex finds our authorization server and prints a sign-in URL right away. By default it asks for every scope the server supports, which includes deleting domains and received mail. To ask for less, put the scopes in the config and sign in after:

[mcp_servers.smtpfast]
url = "https://smtpfa.st/api/mcp"
scopes = ["email:send", "email:read", "domain:read"]
codex mcp login smtpfast

codex mcp login smtpfast --scopes email:send,email:read,domain:read does the same for one login. We checked both with Codex 0.153.4: the sign-in URL asked for exactly those three scopes. The full option list is in OpenAI's Codex MCP docs.

Connect Cursor

Put this in .cursor/mcp.json in your project, or in ~/.cursor/mcp.json for every project:

{
  "mcpServers": {
    "smtpfast": {
      "url": "https://smtpfa.st/api/mcp",
      "headers": { "Authorization": "Bearer ${env:SMTPFAST_API_KEY}" }
    }
  }
}

Cursor fills in ${env:SMTPFAST_API_KEY} from its environment. To use OAuth instead, remove the headers line and Cursor signs in when it connects. Cursor asks for approval before each MCP tool call by default. See Cursor's MCP docs for more. We did not have Cursor set up for this post, so we did not run it.

What it looks like: three prompts, two agents

We connected Claude Code and Codex to the live server with an API key that has these scopes, which is more than this guide needs: email:read, email:send, domain:read, contact:read, inbound:read, webhook:read and team:read. Each agent got the same three prompts:

1. List my SMTPfast domains and their status.
2. Send a test email from [email protected] to [email protected] with the
   subject "Sent by <agent> through SMTPfast MCP" and a one-line plain-text body.
3. Check whether that email was delivered.

[email protected] is an address of the Amazon SES mailbox simulator. It accepts mail and reports it as delivered, so no real person gets a test email.

Both agents ran headless, so nobody could click an approve button. We allowed the tools for each step on the command line. Everything below is real output. We removed some lines and wrapped long ones; [trimmed] marks each removal.

Claude Code

Setup, in an empty folder:

claude mcp add --scope project --transport http smtpfast https://smtpfa.st/api/mcp \
  --header 'Authorization: Bearer ${SMTPFAST_API_KEY}'

Prompt 1. --allowedTools approves the tools named in it for this run. In claude -p nobody can approve anything else, so a call to any other tool is refused unless your settings already allow it. With --output-format stream-json, the output shows each tool call. We condensed it to the tool calls, their results and the final answer.

claude -p "List my SMTPfast domains and their status." \
  --mcp-config .mcp.json --strict-mcp-config \
  --allowedTools mcp__smtpfast__get_capabilities mcp__smtpfast__list_domains \
  --output-format stream-json --verbose
tool_use  mcp__smtpfast__list_domains {}
result    {"object":"list","has_more":false,"data":[{"domain":"smoke-tests.smtpfa.st",
          "status":"pending","dkim_status":"pending", [trimmed] },{"domain":"smtpfa.st",
          "status":"verified","dkim_status":"verified", [trimmed] }]}

You have two SMTPfast domains:
[table trimmed: smtpfa.st verified, smoke-tests.smtpfa.st pending]

smoke-tests.smtpfa.st was added over a month ago and DKIM, SPF and DMARC all still show
as pending. It can receive email but can't send until it's verified. [trimmed]

Claude Code loads MCP tools on demand, so each run also started with a ToolSearch call to load the SMTPfast tools it needed. We left those calls out.

Prompt 2. This run allows send_email, but not get_email. We also gave it a session id, so prompt 3 can continue the same conversation:

SESSION_ID=$(uuidgen)
claude -p 'Send a test email from [email protected] to [email protected] with the subject "Sent by Claude Code through SMTPfast MCP" and a one-line plain-text body.' \
  --session-id "$SESSION_ID" \
  --mcp-config .mcp.json --strict-mcp-config \
  --allowedTools mcp__smtpfast__get_capabilities mcp__smtpfast__check_before_send mcp__smtpfast__send_email \
  --output-format stream-json --verbose
tool_use  mcp__smtpfast__get_capabilities {}
result    {"scopes":["email:read","domain:read","email:send","inbound:read","contact:read",
          "webhook:read","team:read"], [trimmed] "account":{"tier":"starter","status":"active"},
          [trimmed] "rate_limit":{"per_second":50, [trimmed] }}
tool_use  mcp__smtpfast__send_email {"from":"[email protected]","to":"[email protected]",
          "subject":"Sent by Claude Code through SMTPfast MCP","text":"This is a test email sent
          by Claude Code through the SMTPfast MCP server.","category":"transactional"}
result    {"id":"cmvjc81n3002xd9utaicm19s7"}
tool_use  mcp__smtpfast__get_email {"id":"cmvjc81n3002xd9utaicm19s7"}
result    Claude requested permissions to use mcp__smtpfast__get_email, but you haven't granted it yet.

I sent the email through SMTPfast and it was accepted, with ID `cmvjc81n3002xd9utaicm19s7`.
I haven't confirmed delivery: the delivery check needs a permission you haven't granted.
[trimmed]

In this run, Claude called get_capabilities first, as the tool description tells agents to, and it set category to transactional without being asked. It then tried to check delivery, and Claude Code refused because we had not approved get_email. Sends work the same way: in an earlier run without send_email in the list, Claude Code refused the send with the same message.

Prompt 3, in the same session:

claude -p "Check whether that email was delivered." \
  --resume "$SESSION_ID" \
  --mcp-config .mcp.json --strict-mcp-config \
  --allowedTools mcp__smtpfast__get_email mcp__smtpfast__explain_email mcp__smtpfast__list_emails \
  --output-format stream-json --verbose
tool_use  mcp__smtpfast__get_email {"id":"cmvjc81n3002xd9utaicm19s7"}
result    {"id":"cmvjc81n3002xd9utaicm19s7", [trimmed] "category":"transactional",
          "status":"delivered", [trimmed] "sent_at":"2026-10-22T09:27:31.513Z",
          "delivered_at":"2026-10-22T09:27:31.933Z", [trimmed] }

Yes, the email was delivered. SMTPfast shows its status as **delivered**, with no bounce or error.

It moved through SMTPfast's steps in about 1.3 seconds (times in UTC on 2026-10-22):

| Event | Time |
|---|---|
| Queued | 09:27:30.753 |
| Sending | 09:27:31.143 |
| Sent | 09:27:31.521 |
| Delivered | 09:27:31.949 |

[Message-ID and SES message ID trimmed]

No opens or clicks were recorded, which is normal: the simulator address accepts mail but doesn't open it.

Codex

We did not change Codex's config file for this. The -c flags add the server for one run only. --disable apps turns off Codex's app connectors for the run.

Prompt 1:

codex exec --skip-git-repo-check --disable apps \
  -c 'mcp_servers.smtpfast.url="https://smtpfa.st/api/mcp"' \
  -c 'mcp_servers.smtpfast.bearer_token_env_var="SMTPFAST_API_KEY"' \
  -c 'mcp_servers.smtpfast.default_tools_approval_mode="writes"' \
  "List my SMTPfast domains and their status."
[trimmed]
mcp: smtpfast/get_capabilities started
mcp: smtpfast/get_capabilities (completed)
mcp: smtpfast/list_domains started
mcp: smtpfast/list_domains (completed)
codex
You have 2 SMTPfast domains:

| Domain | Sending status | DKIM / SPF / DMARC | Receiving |
|---|---|---|---|
| `smtpfa.st` | Verified | All verified | Disabled |
| `smoke-tests.smtpfa.st` | Pending | All pending | Active |

Prompt 2. With default_tools_approval_mode = "writes", Codex asks before any tool that is not marked read-only, and codex exec cannot ask. So for this run we approved send_email and no other write tool:

codex exec --skip-git-repo-check --disable apps \
  -c 'mcp_servers.smtpfast.url="https://smtpfa.st/api/mcp"' \
  -c 'mcp_servers.smtpfast.bearer_token_env_var="SMTPFAST_API_KEY"' \
  -c 'mcp_servers.smtpfast.default_tools_approval_mode="writes"' \
  -c 'mcp_servers.smtpfast.tools.send_email.approval_mode="approve"' \
  'Send a test email from [email protected] to [email protected] with the subject "Sent by Codex through SMTPfast MCP" and a one-line plain-text body.'
[trimmed]
mcp: smtpfast/get_capabilities started
mcp: smtpfast/get_capabilities (completed)
mcp: smtpfast/check_before_send started
mcp: smtpfast/check_before_send (completed)
mcp: smtpfast/send_email started
mcp: smtpfast/send_email (completed)
codex
Sent from [email protected] to [email protected].

**Subject:** Sent by Codex through SMTPfast MCP
**Body:** This is a test email sent by Codex through SMTPfast MCP.

**Email ID:** `cmvjc498o002sd9ut8gz3kq1b`

Codex ran check_before_send before it sent. Its session log shows that it also set category to transactional and turned open and click tracking off.

Prompt 3 continues the same session. The id is the one Codex printed at the top of the prompt 2 run:

codex exec --skip-git-repo-check --disable apps \
  -c 'mcp_servers.smtpfast.url="https://smtpfa.st/api/mcp"' \
  -c 'mcp_servers.smtpfast.bearer_token_env_var="SMTPFAST_API_KEY"' \
  -c 'mcp_servers.smtpfast.default_tools_approval_mode="writes"' \
  resume 01a162ec-ecc0-71a2-a25e-fa5e36aa6114 "Check whether that email was delivered."
[trimmed]
mcp: smtpfast/explain_email started
mcp: smtpfast/explain_email (completed)
codex
[trimmed] SMTPfast confirms **delivered** to [email protected] on October 22, 2026
at [local time trimmed] (09:24:34 UTC).

For this check, Codex chose explain_email and Claude chose get_email. Both emails show as delivered in our logs, each about a second after it was queued.

Keep the agent safe

An agent with a send tool can send email to real people under your domain. These are the settings we use.

Give the agent its own key, with only the scopes it needs

Create a separate key for each agent and name it after the agent, for example codex-laptop. If something goes wrong, revoke that one key. Revoking is immediate, and the next request with that key gets a 401.

A key can have any set of scopes, so a send-only key is possible: tick only email:send. The agent can then send, but it cannot check delivery, because get_email needs email:read, and it cannot see your domains. For "send and confirm", the dashboard's starting selection is a good choice: email:send, email:read and domain:read. That set cannot change your domains, contacts or webhooks. If you also want the agent to explain bounces with explain_email, add contact:read, because that tool checks the suppression list.

Some things to know about scopes and MCP:

  • The tools that send mail need email:send. That includes batches, templates, broadcasts, replies, forwards and sending inbox drafts. So a key with email:send can also send a broadcast to all your contacts. Keep approvals on for every send tool, not only send_email.
  • One exception: approving a pending signup on a double opt-in form (manage_pending_signups) needs form:write, not email:send, and it sends the form's welcome email if the form has one. Do not give an agent form:write unless it needs it.
  • The tool list shows all 104 tools whatever the key can do. When the agent calls a tool outside the key's scopes, it gets an error that names the missing scope. get_capabilities tells the agent its scopes up front.
  • For an agent that answers a shared inbox, give it inbound:read without email:send. It can read, label and archive threads and write drafts with save_inbox_draft, but it cannot send. A person reviews the drafts and sends them. The Inboxes docs cover this.
  • There are no MCP tools to create or change API keys or to invite, remove or change team members. The scopes for those, apikey:manage and team:manage, are off by default; leave them off the agent's key. Then a message the agent reads cannot get it to make a new key or add someone to your team, through MCP or through the API.
  • With OAuth, read the scope list on the consent page before you approve. In Codex, set scopes as shown above. In Claude Code, "oauth": { "scopes": "email:send email:read domain:read" } in the server's config does the same.

Make the agent ask before it sends

Each of our tools tells the client whether it is read-only. 42 of the 104 are read-only, and every tool that sends is marked as one that changes things. Use that to approve reads and keep a person in the loop for sends:

  • Claude Code asks before every MCP tool that you have not allowed. Allow the reads in .claude/settings.json and do not add the send tools:

    {
      "permissions": {
        "allow": ["mcp__smtpfast__list_*", "mcp__smtpfast__get_*", "mcp__smtpfast__explain_email"]
      }
    }
    

    Every list_ and get_ tool on our server is read-only. In claude -p, a tool call that is not approved is refused, as the get_email call above shows.

  • Codex: default_tools_approval_mode = "writes" asks before every tool that is not marked read-only. In codex exec, where nobody can answer, the send is refused. This is the real output from our first try at prompt 2, before we approved send_email:

    mcp: smtpfast/send_email started
    mcp: smtpfast/send_email (failed)
    MCP tool call requires approval, but approval policy is never
    codex
    The email was not sent: SMTPfast’s send tool requires approval, but this session’s
    approval policy is set to `never`.
    [trimmed]
    

    To let one tool run without asking, set approval_mode = "approve" under [mcp_servers.smtpfast.tools.send_email]. That is the flag our headless send used.

  • Cursor asks before every MCP tool call by default.

These approvals cover MCP tool calls only. If the agent can also run shell commands and the API key is in its environment, it can call the API with curl or the CLI and skip the MCP approval step. The scopes on the key are the limit that always holds, so keep them narrow, and keep shell commands on approval too.

The server also tells agents to treat the content of received email as data, never as instructions. If your agent reads mail and can also send it, keep approvals on for sends.

Rate limits and plan limits

Each API key gets a number of requests per second: 10 on Free, 50 on Starter, 100 on Growth and 500 on Scale. A tool call counts as an API request, and explain_email can make up to six. Over the limit, the agent gets a 429 error and can try again a second later. Your plan's monthly email limit applies to agent sends too. See Rate Limits.

Test with the SES simulator first

Point the agent's first sends at [email protected]. To see how it handles a bounce, use [email protected] and ask it to explain what happened (with explain_email, the key needs contact:read, as noted above). SMTPfast does not put simulator addresses on your suppression list or count them in your bounce rate, and they stay out of your dashboard metrics. They still appear in your email list and logs.

Agents like to invent addresses such as [email protected]. The API refuses addresses on example.com, example.net, example.org and the reserved test domains (.test, .example, .invalid, .localhost) with a 400, so a made-up address of that kind is never sent.

Retries and idempotency

If a send times out and the agent tries again, it can send twice. The send_email_batch tool takes an idempotency_key. A retry with the same key and the same emails within 24 hours returns the first result instead of sending again. A batch can hold a single email, so for sends that might be retried, you can tell the agent to use send_email_batch with a key. The send_email tool does not take a key. The REST endpoint and the CLI do (the Idempotency-Key header and --idempotency-key).

No MCP client? Use the CLI, SMTP or the skill

The CLI. Any agent that can run shell commands can use the smtpfast CLI. It reads the key from SMTPFAST_API_KEY:

npx smtpfast send --from [email protected] --to [email protected] \
  --subject "Test" --text "It works"
npx smtpfast emails get <email-id>
npx smtpfast domains list

When the output goes to a pipe, the CLI prints JSON, which agents read well.

SMTP. For tools that only speak SMTP, use smtp.smtpfa.st on port 587 or 2525 with STARTTLS, the username smtpfast and an API key with email:send as the password. The Emails docs have the details.

The agent skill. The SMTPfast skill on its own does not send anything. It teaches Claude Code and other agents that support skills how our API works, so the code they write for your app uses the right endpoints and auth. The skill docs compare it with the MCP server.

Next steps

Start with a narrow key and the simulator address. When the agent does what you expect, give it a real recipient.

Ready to get started?

Start sending transactional email today. Free to start, no credit card required.

Get Started for Free