DEV Community

Francisco Perez
Francisco Perez

Posted on • Originally published at uncorreotemporal.com

Automating Email Verification Flows with MCP and Claude

Introduction

Most LLM agents can call REST APIs, browse the web, write and run code, and coordinate multi-step tasks. But there is one failure mode that surfaces reliably in real-world automation: email verification.

The pattern shows up everywhere. A service sends a six-digit OTP before granting access. A SaaS requires clicking a confirmation link before an account activates. A CI/CD job provisions a test user and then stalls because it has nowhere to receive the token. In each case, the automation is fully capable of handling every step of the workflow except the one that goes through an inbox.

This article is a concrete walkthrough of how Claude — using the Model Context Protocol and the uncorreotemporal.com MCP server — can complete these flows end to end. The focus is on the mechanics: what tool calls Claude makes, how it polls for incoming messages, how it extracts a verification link or OTP from email body text, and what guard rails keep the system from becoming a liability.


The Problem with Email Verification Automation

Email verification is designed to slow down machines. The implicit assumption behind every "check your inbox" prompt is that a human is on the other end of the email address — that access is being granted to a person with a real mailbox, not a script with a temporary address.

That assumption breaks clean automation in several ways:

No programmatic access by default. Receiving email requires SMTP infrastructure: a domain with MX records, a server on port 25, and some mechanism to query delivered messages. None of that comes out of the box when you spin up a test environment.

Real inboxes accumulate state. Using a shared QA mailbox like qa@company.com means parallel test runs write into the same inbox simultaneously. There is no clean way to correlate an OTP from a specific test run when ten jobs are executing at once.

OAuth-based inbox access is heavyweight. Reading Gmail programmatically requires an OAuth 2.0 flow, scoped permissions, token refresh logic, and ongoing credential management. For a CI job that runs every commit, the maintenance cost is disproportionate to the task.

Agents have no "inbox" concept. LLM agents are built around function calls and HTTP. There is no standard way to tell an agent "wait here until a specific email arrives." The agent has to know both that email is happening and how to interact with it — and that knowledge is not baked into any standard framework.

The result is that email verification is the last manual step in workflows that are otherwise fully automated. Someone has to check the inbox.


Making Email Programmable

Temporary email infrastructure flips the model. Instead of an inbox that exists independently and happens to receive email, you get an inbox created on demand, with a fixed lifetime, accessible via a structured API.

The uncorreotemporal.com stack implements this with real SMTP delivery. In production, email arrives via AWS SES, which delivers to an SNS topic that POSTs to a webhook at /api/v1/ses/inbound. The webhook validates the SNS signature, runs the SES spam/virus verdicts, parses the raw RFC 2822 message, and calls deliver_raw_email() — which inserts a Message row into PostgreSQL and publishes a Redis event to mailbox:{address}.

The result is a live inbox that can receive actual SMTP traffic from any sender on the internet, queryable via REST, and linked to a user account via an API key.

For agents specifically, the MCP server is the right entry point. Not because REST is insufficient, but because MCP lets Claude discover and invoke these capabilities without any custom integration code — just a server configuration and an API key.


MCP Servers and AI Tooling

The Model Context Protocol defines a standard interface for giving AI agents access to external tools. An MCP server exposes a named set of tools with typed input schemas. Any MCP-compatible runtime — Claude Desktop, Cursor, AutoGen, LangGraph — can call tools/list to discover what is available and tools/call to invoke them, with no adapter code per framework.

The uncorreotemporal MCP server runs as a subprocess over stdio. Its configuration in claude_desktop_config.json looks like this:

{
  "mcpServers": {
    "uncorreotemporal": {
      "command": "python",
      "args": ["-m", "mcp.server"],
      "env": {
        "UCT_API_KEY": "uct_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The server exposes five tools: create_mailbox, list_mailboxes, get_messages, read_message, and delete_mailbox. Each maps directly to a database operation — the MCP layer is thin on purpose. There is no HTTP hop between the MCP server and the database; mcp/server.py imports and calls SQLAlchemy model operations directly, with its own AsyncSessionLocal() per call.

Authentication happens once. On the first tool call, _get_user_id() hashes UCT_API_KEY with SHA-256, queries the ApiKey table, resolves the associated User, and caches _authenticated_user_id in module memory. Every subsequent call in the same server process skips the auth lookup entirely.


Creating Inboxes from an AI Agent

When Claude needs an inbox, it calls create_mailbox. The call is minimal:

{
  "name": "create_mailbox",
  "arguments": { "ttl_minutes": 15 }
}
Enter fullscreen mode Exit fullscreen mode

The server resolves the user's active plan from their subscription table (no plan_id FK on the users row — plan is derived from the active subscription, fallback to free). The TTL is clamped to [1, plan.max_ttl_minutes], an expires_at timestamp is calculated, and the address is generated using generate_friendly_address() — an adjective-noun-number@domain format that produces human-readable names like coral-tiger-17@uncorreotemporal.com.

The mailbox is tagged owner_type=mcp in the database — structurally identical to an API-created mailbox, but distinguishable in audit logs. The tool returns:

{
  "address": "coral-tiger-17@uncorreotemporal.com",
  "expires_at": "2026-03-15T10:45:00Z",
  "ttl_minutes": 15
}
Enter fullscreen mode Exit fullscreen mode

The address is live immediately. Any SMTP sender can deliver to it.


Detecting Incoming Emails

The system has two delivery notification mechanisms. Browser clients use WebSocket, subscribing to a Redis pub/sub channel keyed mailbox:{address}. When deliver_raw_email() inserts a new message, it publishes {"event": "new_message", "message_id": "<uuid>"} to that channel. The WebSocket client receives the push event within milliseconds.

The MCP tools do not expose the WebSocket channel. Agents poll.

Polling with get_messages is intentionally lightweight. The tool returns only message metadata — id, from_address, subject, received_at, is_read, has_attachments — without body content. This keeps each poll response small. A typical agent loop looks like this:

# Agent polling loop (pseudocode)
for attempt in range(12):           # max ~60 seconds
    result = mcp.call("get_messages", address=address, limit=5)
    unread = [m for m in result["messages"] if not m["is_read"]]

    if unread:
        break

    time.sleep(5)
else:
    raise TimeoutError("No verification email arrived within 60 seconds")
Enter fullscreen mode Exit fullscreen mode

Claude naturally writes this kind of loop when it understands the task: create an inbox, trigger the external flow, then poll until a message with a relevant subject arrives. The is_read flag on each message lets the agent filter to genuinely new arrivals without re-reading messages it has already processed.

The received_at DESC ordering on the query means the most recent message is always first — no reverse scanning needed.


Extracting Verification Links and OTP Codes

read_message fetches the full message content and marks it is_read=True:

{
  "name": "read_message",
  "arguments": {
    "address": "coral-tiger-17@uncorreotemporal.com",
    "message_id": "3f8a1c2b-9e14-4a77-b832-d47ac1f30512"
  }
}
Enter fullscreen mode Exit fullscreen mode

The response includes both body_text and body_html, parsed from the raw RFC 2822 bytes stored at ingest time. For verification flows, body_text is usually sufficient — it strips HTML tags and gives the agent a clean string to reason over.

For link-based verification, Claude extracts the URL directly from body_text:

body_text: "Welcome! To confirm your account, click the link below:
https://service.com/verify?token=eyJhbGciOiJIUzI1NiJ9.abc123

This link expires in 24 hours."
Enter fullscreen mode Exit fullscreen mode

Claude identifies the URL in plain text and navigates to it. No regex is required in the agent code itself — the LLM parses the text naturally. For more structured extraction in deterministic pipelines, a regex like r"https?://[^\s]+" on body_text is reliable.

For OTP codes, the pattern is slightly different. Services typically send a message with a prominent numeric code:

body_text: "Your verification code is: 847392

Do not share this code with anyone."
Enter fullscreen mode Exit fullscreen mode

Claude reads the body, identifies the six-digit code, and uses it in the next step of the workflow. In a programmatic context, r"\b\d{6}\b" captures most OTP formats. For codes with different lengths or formats (XXXX-YYYY, alphanumeric), the agent's reasoning handles the variation without code changes.

The body always contains both representations — body_text for easy parsing, body_html for cases where the link is in an anchor tag and not visible in the plaintext version.


A Fully Automated Verification Flow

Here is the complete sequence, annotated with the actual MCP tool calls Claude makes:

Step 1 — Create an inbox with a short TTL

 create_mailbox({"ttl_minutes": 10})
 {"address": "coral-tiger-17@uncorreotemporal.com", "expires_at": "...", "ttl_minutes": 10}
Enter fullscreen mode Exit fullscreen mode

Step 2 — Use the address during signup

Claude fills the email field in a registration form with coral-tiger-17@uncorreotemporal.com. The exact mechanism depends on the agent's browser tool — Playwright, Selenium, or a browser-use MCP tool. The point is that the address is real and Claude owns the inbox.

Step 3 — Wait for the verification email

 get_messages({"address": "coral-tiger-17@uncorreotemporal.com", "limit": 5})
 {"messages": []}

 get_messages({"address": "coral-tiger-17@uncorreotemporal.com", "limit": 5})
 {
    "messages": [{
      "id": "3f8a1c2b-...",
      "from_address": "noreply@service.com",
      "subject": "Confirm your account",
      "received_at": "2026-03-15T10:35:44Z",
      "is_read": false,
      "has_attachments": false
    }]
  }
Enter fullscreen mode Exit fullscreen mode

Step 4 — Read the message and extract the token

 read_message({"address": "coral-tiger-17@uncorreotemporal.com", "message_id": "3f8a1c2b-..."})
 {
    "from_address": "noreply@service.com",
    "subject": "Confirm your account",
    "body_text": "Click to confirm: https://service.com/verify?token=abc123\n\nExpires in 1 hour.",
    "body_html": "<p>Click <a href=\"https://service.com/verify?token=abc123\">here</a>...</p>",
    "attachments": []
  }
Enter fullscreen mode Exit fullscreen mode

Claude extracts https://service.com/verify?token=abc123 from body_text.

Step 5 — Complete the verification

Claude navigates to the extracted URL (or submits the OTP in a form). The account is now verified.

Step 6 — Clean up

 delete_mailbox({"address": "coral-tiger-17@uncorreotemporal.com"})
 {"success": true, "address": "coral-tiger-17@uncorreotemporal.com"}
Enter fullscreen mode Exit fullscreen mode

The entire flow — inbox creation to account verification — runs in under 30 seconds with no human in the loop.


Why AI Agents Need Real Email Infrastructure

The alternative to a live SMTP inbox is interception. Some test setups run a local SMTP server that captures outbound messages before they leave the network. This works for controlled environments but breaks for any flow involving a third-party service, because those services send to a real domain and expect actual delivery.

Agents working with real-world systems — creating accounts on external services, automating procurement workflows, testing SaaS integrations, running compliance checks — need inboxes that receive genuine SMTP traffic. A fake inbox that captures messages internally is only useful if you control both the sender and the recipient.

There is also a composability argument. An agent that uses a temporary inbox via MCP requires no special infrastructure knowledge. It calls create_mailbox, gets a live address, submits it to an external service, polls get_messages until the verification email arrives, reads body_text via read_message, extracts the token, completes the flow, and cleans up with delete_mailbox.

The five-tool surface is intentionally small. There is no tool for raw SMTP access, no streaming subscription, no attachment download — just the operations an agent needs to complete a verification workflow. Complexity stays in the infrastructure layer, not in the agent's reasoning loop.


Conclusion

Email verification is a blocking dependency in a large class of real-world automation tasks. The underlying problem is structural: receiving email requires infrastructure, and most agent runtimes have no standard way to access it.

The approach described here — a real SMTP backend exposed as MCP tools — resolves this without requiring agents to understand anything about email delivery. Claude calls create_mailbox, gets a live address, submits it to an external service, polls get_messages until the verification email arrives, reads body_text via read_message, extracts the token, completes the flow, and cleans up with delete_mailbox.

As agent runtimes mature and autonomous workflows become more common, email needs to be a first-class tool — not a special case requiring a human fallback. The infrastructure to make that real exists today. The full stack is at uncorreotemporal.com.

Top comments (0)