Test Signup, Magic-Link and OTP Emails With AI Agents
To let Claude Code, Codex or Cursor test the emails your app sends, give the agent a disposable inbox it can control over MCP. With the Hooklistener MCP server the loop is: the agent calls create_inbox to get an address, starts wait_for_email, signs up in your app with that address, reads the email with get_email, pulls out the 6-digit code or magic link, and finishes the flow with its own browser or HTTP tool.
This guide shows the setup, four walkthroughs (OTP code, magic link, password reset, content checks), how to run the same check in CI, and how Hooklistener compares with Mailtrap, Mailosaur and MailSlurp for agent-driven email tests.
The Loop in Six Steps
- 01
Connect your agent to the MCP server
Add https://app.hooklistener.com/api/mcp to Claude Code, Codex or Cursor and sign in with OAuth.
- 02
Create a disposable inbox
Ask the agent to call create_inbox. It returns an inbox ID and a public address such as signup-tests-x1y2@hookinbox.com.
- 03
Start waiting before you trigger the email
The agent calls wait_for_email. It returns a task receipt right away and watches the inbox for the next email. A wait only sees emails that arrive after it starts.
- 04
Trigger the flow
The agent signs up, requests a magic link or resets a password in your app with the inbox address, using its browser or HTTP tool.
- 05
Read the email and extract the code or link
The agent follows the task resource until it succeeds, then calls get_email with include_body: true to read the subject, sender, text and HTML bodies.
- 06
Finish the flow and report
The agent enters the code or opens the link with its own tools, checks that the user is verified or signed in, and reports what passed and what did not.
Hooklistener receives the email and hands it to the agent. It does not click links, type codes or drive your app. Those steps belong to the agent's own tools (a Playwright MCP server, a browser tool, or curl), which keeps the test honest: the same code path a user hits is the one being checked.
Setup: Connect the MCP Server
The server is hosted at https://app.hooklistener.com/api/mcp. Nothing to install; you add the URL and sign in through your browser (OAuth). In Claude Code:
claude mcp add --transport http hooklistener https://app.hooklistener.com/api/mcp
# then in Claude Code: /mcp -> hooklistener -> authenticateClient-specific steps: Claude Code, Codex, Cursor. The free plan is enough to try everything below (1 inbox, 10 emails).
Why your agent only sees create_inbox at first
The server has 67 tools in seven toolsets, but a new workspace lists only 26: the webhook tools plus the one tool that starts each other product. For email that is create_inbox. The rest of the email toolset (list_inboxes, list_emails, get_email, wait_for_email) is listed once your workspace has an inbox and the client reconnects. The first create_inbox reply says so and asks the agent to reconnect. In Claude Code, run /mcp, pick hooklistener and choose Reconnect. The hidden tools can still be called before that; they just aren't advertised.
To list the email tools from the first message, send the x-hooklistener-toolsets header with email (or all, or a list such as email,monitors). OAuth keeps working; the header only changes which tools are listed.
claude mcp add --transport http hooklistener https://app.hooklistener.com/api/mcp \
--header "x-hooklistener-toolsets: email"{
"mcpServers": {
"hooklistener": {
"url": "https://app.hooklistener.com/api/mcp",
"headers": { "x-hooklistener-toolsets": "email" }
}
}
}Claude Code's project .mcp.json takes the same headers key, plus "type": "http". In Codex, static headers go in http_headers:
[mcp_servers.hooklistener]
url = "https://app.hooklistener.com/api/mcp"
oauth_resource = "https://app.hooklistener.com/api/mcp"
http_headers = { "x-hooklistener-toolsets" = "email" }Walkthrough 1: Signup Verification Code (OTP)
Your app emails a 6-digit code after signup, and the account is unverified until the user types it in. Start your dev server, make sure the agent has a browser tool, and ask:
> Create a Hooklistener inbox called "signup tests". Start waiting
for an email on it, then sign up at http://localhost:3000/signup
with the inbox address. Read the verification email, extract the
6-digit code, enter it, and confirm the account shows as verified.First call, create_inbox. The reply carries the address the agent will sign up with:
create_inbox({ "name": "signup tests" })
{
"id": "3f0c9a52-…",
"name": "signup tests",
"slug": "signup-tests-x1y2",
"email_address": "signup-tests-x1y2@hookinbox.com",
"toolset_hint": {
"toolset": "email",
"action": "reconnect_mcp_client",
"message": "Reconnect the MCP client to list the rest of the email toolset: …"
}
}Pass slug if you want a predictable local part (lowercase letters, digits and hyphens, unique across Hooklistener). Next, wait_for_email. By default it does not hold the call open: it returns a durable task receipt, so the agent is free to go and trigger the signup while the server watches the inbox.
wait_for_email({ "inbox_id": "3f0c9a52-…", "timeout": 60 })
{
"status": "running",
"task_id": "8d1e…",
"resource_uri": "hooklistener://tasks/8d1e…",
"durable": true,
"mode": "async",
"deadline_at": "2026-09-26T10:15:04Z",
"next_actions": [
{ "action": "send_email", "inbox_id": "3f0c9a52-…" },
{ "action": "read_resource", "resource_uri": "hooklistener://tasks/8d1e…",
"until_status": ["succeeded", "timeout", "failed", "cancelled"] }
]
}Order matters here: a wait only sees emails that arrive after it starts, so the agent starts it before submitting the form. The timeout defaults to 30 seconds and caps at 60. The agent then fills in the signup form and reads hooklistener://tasks/8d1e… until the task finishes. On success the result holds an email summary:
{
"status": "succeeded",
"result": {
"status": "received",
"email": {
"id": "b72d…",
"from": "Acme <no-reply@acme.dev>",
"recipients": ["signup-tests-x1y2@hookinbox.com"],
"subject": "Your Acme verification code",
"has_text_body": true,
"has_html_body": true,
"text_preview": "Your verification code is 482913. It expires in 10 minutes.…",
"resource_uri": "hooklistener://email/messages/b72d…",
"body_resource_uri": "hooklistener://email/messages/b72d…/body"
}
}
}The previews are capped at 500 characters, which is often enough for a code near the top. When it isn't, the agent calls get_email with include_body: true for the full text and HTML. It extracts 482913, types it into the verification screen and checks the result. A good report from the agent reads like: “Email arrived 3.1 s after submit, subject and sender correct, code accepted, account page shows Verified.”
If your client can't read MCP resources:pass blocking: true and the call holds open until an email arrives or the timeout expires (up to 60 s), returning the email directly. The catch is that the agent can't act during a blocking call, so the email has to be triggered by something else, such as you clicking the button or a script already running. If the email is already in the inbox, list_emails finds it without waiting.
Walkthrough 2: Magic Link Sign-in
> Start waiting on the signup tests inbox. Request a magic link at
http://localhost:3000/login for that address. Get the full email body,
find the sign-in link, open it in the browser, and tell me whether I end
up signed in on /dashboard. Don't open any other link in the email.Same pattern: wait_for_email, trigger, follow the task. Then the agent needs the whole body, because magic-link URLs are long and often sit past the preview:
get_email({
"inbox_id": "3f0c9a52-…",
"email_id": "c19a…",
"include_body": true
})The reply adds text_body and html_body. The agent pulls the link out of the href in the HTML (or the plain-text version) and opens it with its browser tool. Hooklistener never visits the link, so a single-use token is still unused when the agent gets it. Two things worth asking the agent to check while it's there: that the link host is your app (not localhost leaking into a staging email, or the reverse), and that opening the same link a second time is rejected.
Walkthrough 3: Password Reset
> Using the account we just created, run the password reset flow:
wait on the inbox, request a reset at /forgot-password, open the reset
link, set the password to "correct-horse-42", then log in with it.
Also request a reset for an address that has no account and tell me
whether any email arrives for it.The second half is the useful one. Send the unknown-account request to a plus address such as signup-tests-x1y2+nobody@hookinbox.com (plus addresses land in the same inbox) and have the agent run wait_for_email with a short timeout. If your app is meant to stay silent for unknown addresses, a timeout status is the pass condition: nothing arrived during that window. The agent can tell the two emails apart by the recipients field.
Walkthrough 4: Assert on the Email Itself
Arrival is half the test. Once the agent has the email, ask it to check the content against your templates:
> Get the latest email in the signup tests inbox with its body and raw
headers. Check that:
- the sender is "Acme <no-reply@acme.dev>" and Reply-To is support@acme.dev
- the subject is "Welcome to Acme, Ada" (the name I signed up with)
- both a text and an HTML part exist, and neither contains "{{" or
"undefined" (unrendered template variables)
- every link in the HTML returns 200 when fetched
Report each check as pass or fail.What each check relies on:
- Sender, recipients, subject, message ID, size: always in
list_emailsandget_email. - Text and HTML parts:
has_text_bodyandhas_html_bodyflags, and the full bodies withinclude_body: true. Leftover{{name}}placeholders show up as plain text in either part. - Reply-To, List-Unsubscribe and other headers: parsed headers with
include_raw_headers: true, or the raw MIME source withinclude_raw: true. - Broken links: the agent extracts the links from
html_bodyand fetches them with its own HTTP tool. Hooklistener doesn't crawl links, render screenshots or score spam.
Clients that support MCP prompts also get a verify_email_flow prompt once the email toolset is listed. It walks the agent through the same steps and ends with a receipt: inbox address, matched message ID and message resource URI.
Keeping Test Runs Apart
wait_for_email finishes on the next email to the inbox; it has no subject or sender filter. With one agent working through one flow at a time that is exactly what you want. When several flows share an inbox:
- Give every run its own plus address,
signup-tests-x1y2+run-8841@hookinbox.com, and have the agent checkrecipientsbefore trusting an email. - If the wait caught someone else's email, fall back to
list_emails(newest first, paginated, up to 100 per page) and pick the matching one. - On a paid plan, use separate inboxes for separate suites (Pro has 5, Production and Scale have no inbox limit).
Running the Same Check in CI (Without an Agent)
Outside an agent, use the inbox REST API. It exposes the same inboxes over plain HTTPS: create an inbox, list or filter its emails, read one email with its links and likely one-time codes already extracted, and long-poll /emails/wait until the next matching email arrives (up to 60 seconds; it returns 204 on timeout). It authenticates with a Hooklistener API key (hklst_…, paid plans) as a Bearer token.
Give every run its own plus-address so parallel runs never read each other's mail, and filter the wait by that address:
API=https://app.hooklistener.com/api/v1
AUTH="authorization: Bearer $HOOKLISTENER_API_KEY"
# Create the inbox once (by hand or in a setup job) and store
# HOOKLISTENER_INBOX_ID and HOOKLISTENER_INBOX_ADDRESS as CI secrets.
# Creating one per run would hit your plan's inbox limit.
INBOX_ID=$HOOKLISTENER_INBOX_ID
ADDRESS=$(echo "$HOOKLISTENER_INBOX_ADDRESS" | sed "s/@/+run-$GITHUB_RUN_ID@/")
SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
./scripts/sign-up.sh "$ADDRESS"
# Wait for the verification email sent to this run's address
CODE=$(curl -sf -G "$API/inboxes/$INBOX_ID/emails/wait" -H "$AUTH" \
--data-urlencode "to=$ADDRESS" --data-urlencode "since=$SINCE" \
-d timeout=60 | jq -r '.data.codes[0] // empty')
test -n "$CODE" || { echo "no verification email"; exit 1; }The same flow in a Playwright test:
import { test, expect } from "@playwright/test";
const API = "https://app.hooklistener.com/api/v1";
const headers = { authorization: `Bearer ${process.env.HOOKLISTENER_API_KEY}` };
const INBOX_ID = process.env.HOOKLISTENER_INBOX_ID!;
const INBOX_ADDRESS = process.env.HOOKLISTENER_INBOX_ADDRESS!; // signup-tests@hookinbox.com
async function waitForEmail(params: Record<string, string>) {
const query = new URLSearchParams({ timeout: "60", ...params });
for (let attempt = 0; attempt < 2; attempt++) {
const res = await fetch(`${API}/inboxes/${INBOX_ID}/emails/wait?${query}`, { headers });
if (res.status === 200) return (await res.json()).data;
if (res.status !== 204) throw new Error(`wait failed: ${res.status}`);
}
throw new Error("no email arrived");
}
test("signup emails a working code", async ({ page }) => {
// Two 60-second waits plus the signup; Playwright defaults to 30 seconds.
test.setTimeout(150_000);
const address = INBOX_ADDRESS.replace("@", `+${Date.now()}@`);
const since = new Date().toISOString();
await page.goto("http://localhost:3000/signup");
await page.getByLabel("Email").fill(address);
await page.getByRole("button", { name: "Create account" }).click();
const email = await waitForEmail({ to: address, since });
expect(email.subject).toContain("verification code");
const code = email.codes[0];
expect(code).toBeDefined();
await page.getByLabel("Verification code").fill(code);
await expect(page.getByText("Verified")).toBeVisible();
});codes and links are simple heuristics (4–8 digit numbers, and http(s) URLs from the HTML and text bodies); the full bodies are always in the response if you need your own pattern. Free inboxes accept 10 emails per rolling 24 hours, which a CI suite outgrows quickly; paid plans are unlimited per inbox.
Hooklistener vs Mailtrap vs Mailosaur vs MailSlurp for Agent-Driven Email Tests
All four let a test read the emails your app sends. They differ in how an AI agent reaches them and in how deep the email analysis goes. Checked September 2026 against each vendor's own docs (sources below).
| Tool | MCP server | How mail reaches it | Agent creates inboxes |
|---|---|---|---|
| Hooklistener | Official, hosted (Streamable HTTP), OAuth sign-in | Public @hookinbox.com address; your real email provider delivers to it | Yes, create_inbox |
| Mailtrap | Official, runs locally with npx and an API token | Email Sandbox via sandbox SMTP credentials or a receive-by-email address; separate Inbound inboxes | Inbound inboxes, yes; no wait tool, so the agent polls |
| Mailosaur | No official server found; community servers exist | Test addresses on Mailosaur servers (email and SMS) | Depends on the community server |
| MailSlurp | Official, hosted at api.mailslurp.com/mcp, OAuth or scoped agent key | Real inboxes and phone numbers created in MailSlurp | No; inboxes are created in the dashboard or via API first |
Mailtrap
Mailtrap's official MCP server runs locally (npx mcp-mailtrap with a MAILTRAP_API_TOKEN) and is broad: sending, templates, campaigns and logs, plus Email Sandbox tools that list messages and return the HTML, text, headers, raw source, attachments, a SpamAssassin spam score and an HTML analysis report. It is not only a sending tool. Mail reaches the Sandbox through its SMTP credentials or a receive-by-email address, and a separate set of Inbound tools can create hosted inboxes and read their messages. There is no wait tool; the agent lists messages until the one it needs shows up. Where Mailtrap is stronger: HTML client-compatibility checks, spam and blacklist reports and a bounce emulator, and if Mailtrap is already your sender, one vendor covers both sides.
Mailosaur
We found no official Mailosaur MCP server; its docs index doesn't list one, though it markets its inboxes, phone numbers and OTP extraction to AI agents over its API. Community servers exist, for example one on Glama with 29 tools including waiting for a matching message, search, spam analysis and TOTP codes (local, needs your API key). Where Mailosaur is stronger: a mature email and SMS testing product with code extraction, a virtual TOTP authenticator, spam and deliverability analysis, and SDKs for Cypress, Playwright and most languages. For a scripted test suite that doesn't need an agent, it is the deeper tool.
MailSlurp
MailSlurp runs a hosted MCP server at https://api.mailslurp.com/mcp with OAuth (scopes such as mcp:email:read) or a scoped agent key. Its tools list, search with match conditions and wait for the latest matching email, extract attachment text, and draft or send replies. Inboxes are created in the dashboard or through the API before the agent uses them; the hosted tool list has no create-inbox tool. Where it is stronger: filtered waits, attachment handling, SMS, deliverability checks and a REST “agent API” for the same operations.
Where Hooklistener fits
Hooklistener is the lighter email tool of the four: no spam scoring, no HTML client checks, no structured attachments, no SMS, and the MCP wait tool has no filters (the REST wait does). What it does well is the agent loop. The agent creates its own inbox, your real email provider delivers to a public address, the wait runs as a durable task while the agent drives the app, and setup is one URL with OAuth.
The bigger difference is scope. A signup rarely sends just an email: it also fires a webhook to your CRM or billing provider, and maybe pushes a WebSocket or SSE event to the UI. The same MCP server has tools for all of it: capture and verify the webhook with wait_for_request, watch the real-time message with wait_for_realtime_message, and forward to localhost through a tunnel. If you only need deep email QA, pick one of the dedicated tools. If you want one agent to test the whole flow, that is where Hooklistener fits.
Sources, checked September 26, 2026: mailtrap/mailtrap-mcp, Mailtrap docs index, Mailosaur docs index, Mailosaur for AI agents, community Mailosaur MCP, MailSlurp agent and MCP guide.
FAQ
How many inboxes and emails does the free plan include?
One inbox that accepts up to 10 emails in any rolling 24 hours, and emails are kept for 24 hours, so the allowance refills on its own. Pro includes 5 inboxes and Production and Scale have no inbox limit. Paid plans accept unlimited emails per inbox and keep them as long as their webhook history (14, 30 or 90 days). MCP access itself is on every plan.
How long are captured emails kept?
As long as your plan keeps webhook history: 1 day on Free, 14 days on Pro, 30 on Production and 90 on Scale. Older emails are deleted automatically. Within that window the agent can read any of them with list_emails and get_email.
Can the agent read the HTML version, headers and attachments?
get_email returns the sender, recipients, subject, message ID, size and 500-character previews by default. With include_body: true it also returns the full parsed text and HTML bodies. include_raw_headers: true adds the parsed headers, and include_raw: true adds the raw MIME source. There is no dedicated attachment tool: attachments are only available inside the raw source, which the agent has to decode itself.
Can wait_for_email wait for a specific subject or recipient?
No. It completes on the next email that arrives in the inbox, whatever its subject. To tell test runs apart, sign up with plus addresses (inbox+run42@hookinbox.com, which deliver to the same inbox) and have the agent check the recipients and subject fields. If the email may already be there, list_emails shows the inbox newest first.
Does this work with Codex, Cursor and other agents?
Yes. The server is a standard remote MCP server over Streamable HTTP with OAuth, so Claude Code, Codex, Cursor, VS Code with Copilot, Gemini CLI, Windsurf and other MCP clients can use the same email tools. The agent needs its own browser or HTTP tool to fill in signup forms and open links.
Can I use Hooklistener inboxes in CI without an AI agent?
Yes. The REST API at https://app.hooklistener.com/api/v1/inboxes lets a test create inboxes, list emails filtered by recipient, sender, subject or time, read one email with its links and likely one-time codes already extracted, and long-poll /emails/wait for the next matching email. It authenticates with a Hooklistener API key (paid plans). You can also read every email in the dashboard at app.hooklistener.com.
Related Reading
- The Hooklistener MCP server — all 67 tools, toolsets and client setup.
- Debug Stripe Webhooks With Claude Code and MCP — the same wait-then-inspect loop, for webhooks.
- Agentic webhook testing — turning agent checks into repeatable tests.
- WebSocket debugger — hosted WebSocket, Socket.IO, MQTT and SSE endpoints for the real-time half of a flow.
Give Your Agent an Inbox
Create a free account at app.hooklistener.com, connect your agent with the steps on the MCP page, and ask it to sign up for your app with an inbox it creates. The free plan includes one inbox and up to 10 emails a day, which is enough to see the whole loop work.