Test OTP in Playwright with MCP.
Two things want to drive the same signup form: an agent with a browser, and a Playwright test in CI. Both need a real inbox to finish an OTP flow. This guide is the wiring for that pair — SMTP-routed addresses over MCP while you (or the agent) explore, then the same flow frozen into a Playwright + SDK test the pipeline runs forever.
Two hands, two layers
Don't put an MCP client inside signup.spec.ts. MCP is for a model that decides what to call next; your CI runner is a script that should not re-negotiate that every commit. MailFixture draws the line the same place the agent guide does:
- MCP tools — the agent's hands. “Does this flow still work on staging right now?”, exploratory QA after a deploy, and grounding while the agent authors the regression test.
- Playwright + JS SDK — the test that stays. Deterministic, parallel-safe, a green bar your review process trusts without watching a transcript.
Same product surface both times: inbox-per-run, long-poll wait, typed OTP. The only question is whether the caller is a model or npx playwright test.
1 · Give the agent an inbox
One keyed remote MCP server. Route the client once; then any prompt that needs email can call it:
$ claude mcp add --transport http mailfixture https://mailfixture.com/mcp \ --header "Authorization: Bearer $MAILFIXTURE_API_KEY"
Details and the non-Claude config shape live in the MCP quickstart. Use a separate key labeled for the agent so a leaked laptop key doesn't force a CI secret rotate.
2 · Live OTP run (agent + browser + MCP)
With a browser tool (Playwright MCP, Claude-in-Chrome, your existing agent browser — anything that can type into a form) and MailFixture MCP both connected, an unattended OTP pass is three tools with work in between:
> On staging, sign up a fresh user end to end and confirm the email OTP works. > Use a mailfixture inbox. Leave a Playwright test behind if the flow is green. create_inbox(ttl_seconds=900) → k2x9v04qtr@mxsink.sh … browser tools fill /signup with that address, click Send code … wait_for_otp(inbox_id=…, timeout_seconds=45) → { best: "482913" } // server holds until mail lands … browser types the code, asserts Welcome … delete_inbox(inbox_id=…) ✓ teardown
Three properties make this worry-free: the address is real MX (your staging MTA must be able to deliver to it — no local SMTP mock), wait_for_otp is a single tool call that long-polls server-side (the agent never invents a sleep-retry loop or a /\d{6}/ regex over HTML), and a failed agent run can't leave trash behind if you gave the inbox a TTL. Magic links get the same treatment with wait_for_link and optional follow_link if you want the server to GET the verify URL for you.
3 · Freeze it into Playwright
When the agent is done “does this work tonight?”, the leftover should be a normal test. The Playwright quickstart is the source of truth for the SDK API; the shape the agent should leave is exactly this:
import { test, expect } from '@playwright/test'; import { MailFixture } from 'mailfixture'; const mail = new MailFixture(); // MAILFIXTURE_API_KEY in CI secrets test('signup sends a one-time code', async ({ page }) => { const inbox = await mail.createInbox({ ttlSeconds: 900 }); await page.goto('/signup'); await page.fill('#email', inbox.address); await page.click('text=Send code'); const otp = await inbox.waitForOtp({ timeout: 30_000 }); await page.fill('#otp', otp); await expect(page.locator('h1')).toHaveText('Welcome'); });
Why this is the right freeze: OTP waiting stays on the SDK's long-poll path (NOTIFY-backed on the server), each test gets its own inbox so parallel workers can't steal messages, and extraction is the same typed otp.best the MCP tool used — no second pipeline for the test suite. The longer version of inbox-per-test / no-sleep / no-regex is in Testing OTP flows.
MAILFIXTURE_API_KEY for Playwright in the pipeline secret store. Label the MCP key mcp-agent (or similar) and revoke it independently when a laptop walks away.What is whose job
- One-off after a deploy / “is signup broken?” — agent + MCP + browser. Don't open a PR.
- “Add a regression so this never breaks again” — agent authors the Playwright test (optionally verifying with MCP while writing), CI runs the SDK version forever.
- Parallel PR suites — only Playwright + SDK. MCP is a conversational protocol; don't drop an MCP client into the CI job that already has a deterministic SDK.
- Magic link instead of typed OTP — same split: MCP
wait_for_link/follow_linkwhile exploring; PlaywrightwaitForLink({ kind: 'verify' })in the frozen file.
The boundary, stated plainly
This wiring is for your own product's signup, sign-in, and reset flows — staging or production you control. Our terms draw the same line for agents and test runners: automated registration on third-party services you don't operate is prohibited. An agent that farms signups across the web with programmatic inboxes is the abuse case we design against — every address is attributable to an account and an API key, so it is not anonymous.