What an agent needs that a REST API does not give it
There is a REST API on this site, and a programmer integrating it will do fine. An agent will not: it cannot open the reference, decide which of three endpoints it wants, and hand-roll a request with the right query string. It asks a server what it can do, receives machine-readable schemas, and calls one.
So everything the service does is exposed a second time, as tools. Six of them, and no state to manage between calls:
| Tool | What it is for |
|---|---|
create_inbox | Invents a fresh address the agent can give out immediately. Nothing is reserved server-side, so it cannot fail. Takes an optional readable prefix; a random suffix keeps it unique. |
list_domains | The public domains anyone may use — useful when a form has just refused one of them. |
list_messages | Everything waiting at an address, newest first. Returns straight away, including when there is nothing. |
read_message | One message in full: sender, subject, plain text, HTML, attachments. This is where the code or the sign-in link is. |
wait_for_message | Blocks until something arrives, then returns it in full. The tool to call the moment a form has been submitted. |
delete_message | Removes one now rather than waiting 5 days for it to expire. Idempotent, so an agent retrying costs nothing. |
The server also answers initialize with a short paragraph of instructions, which most clients feed straight to the model. An agent therefore arrives already knowing what this service is for and what its one real caveat is, without anybody writing that into a prompt.
Connect a client in one line
The endpoint is one URL, and there is nothing to sign up for on the public domains. Every MCP client takes the same shape of configuration — Claude Desktop, Claude Code, Cursor, Continue, the OpenAI Agents SDK and anything else that speaks the protocol:
{"mcpServers":{"grabmail":{"url":"https://grabmail.io/mcp"}}}The transport is Streamable HTTP: one POST carrying JSON-RPC 2.0, one JSON reply, no stream held open. You can therefore check the whole thing from a terminal before any agent is involved:
curl -sX POST https://grabmail.io/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'Clients that look for a server before asking a human will find /.well-known/mcp.json on the domain, which names the same endpoint and its transport.
The whole sign-up loop, in four tool calls
This is the sequence almost every agent needs, and there is nothing else to it:
- Call
create_inbox. Back comes an address, an alias, the domain, and a note telling the agent which of the two to hand over. Nothing was created — the mailbox begins to exist when the first message lands in it. - Put the alias in the form. The service being signed up to gets a working address that reaches the mailbox and cannot be used to read it.
- Call
wait_for_messagewith the address. Immediately after submitting, not on a timer. It blocks; it does not poll in a loop the agent has to write. - Read the code out of the message. The full body comes back with the wait, so there is usually no second call at all —
read_messageis only needed for something that arrived earlier.
Why wait_for_message returns before the mail does
It is the tool that makes an agent workable, and the one whose behaviour surprises people, so it is worth a minute. A call looks like this:
curl -sX POST https://grabmail.io/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":
{"name":"wait_for_message",
"arguments":{"address":"demo.5kuqarzuch@grabmail.io",
"subject_contains":"code"}}}'It blocks for up to 25 seconds. If nothing has arrived by then, it does not fail — it comes back with a plain answer and asks to be called again:
{
"timed_out": true,
"waited_seconds": 25,
"message": null,
"note": "Nothing arrived yet. Call wait_for_message again ..."
}- Why bounded at all
- Every second of waiting is a server worker doing nothing but sleeping, and there is a fixed number of them. A wait that could last five minutes would be one agent holding a slot that a hundred others need. 25 seconds also sits inside every client’s default timeout, so the call returns rather than the client giving up on it.
- Only 8 waits at once
- Past that the tool answers immediately with
timed_outand a note saying so. Being told to come back is better than being queued behind seven other agents with no way to know it. - Filtering, so the wrong mail does not end it
from_containsandsubject_containsmake the wait ignore anything else that lands meanwhile.since_idis the one to pass when the mailbox already had something in it: hand it the newest id already seen and only genuinely new mail will satisfy the call.
Hand out the alias, poll the address
Every mailbox here has a second address, twelve characters long, that delivers into the same mailbox and cannot read it. That distinction matters far more for an agent than for a person, because an agent will happily paste whatever it was given into whatever field it finds.
So create_inbox does not simply hand back an address and hope. It returns both, and a next_step saying which is which — the agent reads its own tool result, so the instruction arrives where it is needed rather than sitting in a documentation page nobody in the loop can read.
What to put in the agent’s own instructions
The tools describe themselves well enough that a capable model gets this right unprompted. Five lines make it reliable rather than likely:
- One address per sign-up. Not one address reused everywhere: a mailbox holding six services' mail is six confirmations an agent has to tell apart, and one leak that exposes all of them.
- Give out the alias, never the address. Worth saying explicitly even though the tool result says it too.
- Call
wait_for_messagestraight after submitting, and call it again ontimed_outrather than treating that as a failure. Two or three times is normal. - Pass
since_idwhen the mailbox is not new, or an old message satisfies the wait and the agent reads a code that expired an hour ago. - Delete the message once the code is used. Not required — everything goes in 5 days anyway — but it closes the window early and it costs one idempotent call.
Written as an instruction block, that is about as long as this:
When you need an email address, call create_inbox and hand out the ALIAS it
returns, never the address. Immediately after submitting the form, call
wait_for_message with the address. If it answers timed_out, call it again —
that is normal and nothing was lost. Pass since_id if the mailbox already had
mail in it. Delete the message once the code has been used.The limits worth knowing before you build on it
All of them are published rather than discovered, and none of them has a plan that raises it:
| Limit | Value | What it means for an agent |
|---|---|---|
| One wait | 25 seconds | Then timed_out. Call again; do not treat it as an error. |
| Waits at once | 8 | Past that the tool returns at once and says so. Fall back to list_messages. |
| Reads | One a second, per address | Far above what a tool-calling loop does. A blocking wait is one request, not sixty. |
| Message size | 5 MB | Refused during the SMTP conversation, so the sender is told rather than the agent waiting for something that will never come. |
| Retention | 5 days | A hard limit enforced by a job. Anything the agent must keep, it has to write down itself. |
There is no sending endpoint and no tool for one. This service receives only, which is what keeps an unauthenticated mailbox from becoming a spam relay — so an agent that needs to reply to a human needs a real mailbox somewhere else.
When a form refuses the public domains
Plenty of services keep lists of disposable-mail domains, and the three public ones here are on them. An agent meets this as a form that rejects the address it was just given, or worse, accepts it and never sends anything.
The durable answer is a domain you own. One MX record turns every address on it into a mailbox here, it is on nobody’s list because it is printed nowhere on this site, and the same six tools work on it unchanged — create_inbox is the only one that does not, since it invents addresses on the public domains. The agent simply uses you-pick-it@your-domain and calls wait_for_message on that.
MX record for your domain10 smtp.grabmail.io
The full walk-through is here — the record, what publishing it proves, and the limits of a mailbox with no password on it.
What not to let an agent do with this
The honest part, and the part that saves an afternoon:
- Nothing you would need to recover. Anything holding money, identity or work. The mailbox is empty again in 5 days and readable by whoever knows the address, so a password reset sent to it next year reaches nobody — or somebody else.
- Not as a second factor. A mailbox with no password on it is not a factor.
- Not for anything private. Not because we read it, but because the address is the only secret in play and an agent may well have written it into a log, a transcript or a commit message.
- Not for volume. An agent opening accounts by the hundred is the behaviour every blocklist exists for, and it is the fastest way to get the public domains refused for everybody else.
Used for what it is — the confirmation step that stands between an agent and the thing it was actually asked to do — it removes the one step that reliably stops it.
Questions
Do I need an API key or an account?
No. The public domains, the tools and a domain of your own are all free and unauthenticated. The only case that carries a key is a domain that has been closed on request, which then wants an Authorization header.
Which clients does this work with?
Any client that speaks the Model Context Protocol — Claude Desktop, Claude Code, Cursor, Continue, the OpenAI Agents SDK and the rest. The transport is Streamable HTTP, which is what current clients default to, and three protocol versions are accepted so an older one still connects.
Why does wait_for_message come back with timed_out?
Because a single wait is capped at 25 seconds, deliberately. It is not an error and nothing was lost: call it again. Mail routinely takes longer than the page that promised it suggests, and two or three waits in a row is an ordinary sign-up.
Can the agent use my own domain instead?
Yes, and nothing changes but the address. Point an MX record at smtp.grabmail.io and every address on that domain becomes readable through the same tools. Only create_inbox is public-domain-only, because it is the one that invents a name for you.
Can an agent send email through this?
No. There is no sending tool and no sending endpoint, by design: an unauthenticated service that could send mail would be a spam relay within a day. Our SPF is v=spf1 -all and our DMARC is p=reject, so anything claiming to come from an address here is forged.
Is the inbox private?
No, and this is the one caveat to give an agent explicitly. On a public domain anyone who knows or guesses the address can read it. Use an unguessable address, hand out the alias rather than the address, and never let anything private near it.
Can two agents wait on the same address at once?
They can, and both will be handed the message when it arrives. What is capped is the number of waits happening across the whole service at once, 8; past that the tool answers immediately and says so, and list_messages still works.
How long do the messages last?
5 days from arrival, read or not, and there is no setting that extends it. Every message carries expires_at, so an agent never has to work that date out for itself.
How is this different from calling the REST API from a script?
For a script it is not different, and the REST API is the better fit — the guide to testing verification flows covers that shape, deadlines and helpers included. MCP is for the case where nobody wrote the loop: the model decides to open an inbox, and needs the tools to be discoverable rather than documented.


