MCP server · MachineMail Docs
Transport
MachineMail runs a remote MCP server, so any MCP client can give its model email: create an inbox, wait for a message, read it safely, reply. It calls the same service layer as the REST API, so scopes, permissions and trust reports behave identically.
| Endpoint | https://mcp.machinemail.dev/mcp |
| Transport | Streamable HTTP, stateless |
| Auth | OAuth sign-in, or Authorization: Bearer <API key> |
| Server name | machinemail |
| Description document | https://mcp.machinemail.dev/.well-known/mcp.json |
Client setup has the exact configuration for Claude Code, Cursor, Codex, OpenCode, VS Code and others.
Legacy endpoint
Earlier versions of these docs gave https://api.machinemail.dev/mcp; https://machinemail.dev/mcp also works. Both are legacy aliases of the same server and keep working, so existing configurations need no change. Use https://mcp.machinemail.dev/mcp for new setups. A client that signed in on https://machinemail.dev/mcp keeps its tokens; tokens for either URL work on both. On https://api.machinemail.dev/mcp API keys work, but the OAuth metadata there names https://machinemail.dev/mcp, which clients that check the resource against the URL they connected to refuse: if OAuth sign-in fails there, switch to https://mcp.machinemail.dev/mcp.
mcp.machinemail.dev serves only the MCP server: /mcp, its protected resource metadata and /.well-known/mcp.json. Opening https://mcp.machinemail.dev/ in a browser brings you to this page. Sign-in, the dashboard and the REST API stay on machinemail.dev. Clients written to the 2025-03-26 MCP auth spec, which look for the authorization server on the MCP URL’s own host, reach it there: /.well-known/oauth-authorization-server and /authorize on mcp.machinemail.dev redirect (307) to machinemail.dev, and /token and /register answer in place as the authorization server’s own endpoints, so HTTP Basic client credentials are kept (a cross-origin redirect would drop them).
Transport
The server speaks Streamable HTTP in stateless mode. It serves MCP 2026-07-28 (clients discover it with server/discover) and the earlier revisions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05 (clients that start with initialize), so current clients keep working unchanged:
- Every JSON-RPC request is a
POSTto/mcp; sendAccept: application/json, text/event-stream. On the 2025 revisions a response comes back as a single server-sent event (Content-Type: text/event-stream); on2026-07-28it is plain JSON. - There is no session. The server does not issue an
Mcp-Session-Id, and each request is handled on its own, so any request can go to any instance. - A
GETwithAccept: text/event-streamand aDELETEreturn405 Method Not Allowed: there is no standalone server-to-client stream and no session to end. - A plain
GET(from a browser, say) returns a JSON description of the endpoint instead of speaking MCP. - A request is cancelled when the client disconnects, which also ends a pending
wait_for_message.
Call a tool with curl
curl https://mcp.machinemail.dev/mcp \
-H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_inboxes","arguments":{}}}'
Response
event: message
data: {"result":{"content":[{"type":"text","text":"- agent-f5pvo2cei042@machinemail.dev (Docs example agent) ephemeral, expires 2026-10-10T09:06:15.234Z"}],"structuredContent":{"count":1,"inboxes":[{"inbox_id":"agent-f5pvo2cei042@machinemail.dev","email":"agent-f5pvo2cei042@machinemail.dev","ephemeral":true,"expires_at":"2026-10-10T09:06:15.234Z","...":"..."}]}},"jsonrpc":"2.0","id":1}
MCP clients do all of this for you. The curl form is useful for checking a key.
Authentication
Every request carries a bearer credential. Three kinds work:
- OAuth sign-in. Add the server URL to your client with no credentials. The client receives a 401, finds MachineMail’s authorization server, and opens your browser. You sign in to the dashboard, choose what the client can reach, and approve. The client then holds a short-lived access token and a refresh token, and renews them on its own. This is the option to use for Claude, Cursor, VS Code, OpenCode and other interactive clients.
- An API key (
mm_live_...) from the dashboard’s API keys page orPOST /v0/api-keys, sent as a header. Use it where no browser is available: CI, servers, headless agents. The key’s scope and permissions apply unchanged. A key scoped to one inbox sees only that inbox through MCP. Resources outside the scope return not-found errors, exactly as on/v0. - An access token from auth.md. An agent that registered itself uses its access token here too. Before it is claimed, the token reaches only its sandbox inbox.
OAuth sign-in
MachineMail follows the MCP authorization specification for revisions 2026-07-28, 2025-11-25 and 2025-06-18. MCP clients handle these steps for you; they are listed for client authors and for debugging.
- Discovery. A request without a valid token gets a 401 that points to the protected resource metadata of
/mcp:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.machinemail.dev/.well-known/oauth-protected-resource/mcp", scope="inboxes.read inboxes.write messages.read ..."
{"error":{"type":"unauthorized","message":"Missing or invalid API key or access token"}}
If the request did send a token and it was rejected, the header adds error="invalid_token". A HEAD request gets the same 401, for clients that probe before connecting. The metadata document names the resource, https://mcp.machinemail.dev/mcp, and the authorization server, https://machinemail.dev. The authorization server’s own metadata is at https://machinemail.dev/.well-known/oauth-authorization-server (RFC 8414). It lists the authorization, token, registration and revocation endpoints, code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none", "client_secret_post", "client_secret_basic"], client_id_metadata_document_supported: true and authorization_response_iss_parameter_supported: true.
- Client identity. A client either uses the https URL of its Client ID Metadata Document as its
client_id, or registers at/oauth2/register(Dynamic Client Registration, RFC 7591). ChatGPT, Claude, Claude Code, VS Code, Codex, Zed and Goose use a metadata document; other clients register.
- A metadata document client is public: it authenticates with PKCE alone. A document may prefer another method (ChatGPT’s prefers
private_key_jwt) as long astoken_endpoint_auth_methods_supportedincludesnone.- A registration is public by default (
token_endpoint_auth_method: "none"). A client that registers withclient_secret_postorclient_secret_basicgets aclient_secretin the response, shown once and never expiring (client_secret_expires_at: 0). It then authenticates at the token and revocation endpoints with HTTP Basic orclient_secretin the body, either way. Other methods are registered asnone. Grant types other thanauthorization_codeandrefresh_tokenare dropped from the registration. - Every authorization uses PKCE, public client or not.
- A registration is public by default (
Redirect URIs must use https, or http to a loopback address (127.0.0.1, [::1] or localhost, on any port, and any of the three matches the others), or a native app’s own scheme such as cursor://.
- Authorization. The client opens
https://machinemail.dev/oauth2/authorizewithresponse_type=code, a PKCEcode_challenge(S256), itsredirect_uri,state, andresource=https://mcp.machinemail.dev/mcp(RFC 8707). Withoutresource(clients on the 2025-03-26 spec), the token is for the MCP server. Withoutscope, or with onlyopenid,profile,emailoroffline_access, the client asks for every scope it may use. Any other scope MachineMail doesn’t define is ignored next to ones it does, and on its own is aninvalid_scopeerror (a misspelled scope never widens to every scope). Your browser lands on the dashboard’s Connect an app page; sign in first if needed. Connect an agent without code shows this page step by step. The page shows:
- the client’s name, marked as unverified unless it came from a metadata document, in which case it shows the document’s host;
- where your browser will be sent back to, with a warning when that is a program on your computer;
- the scopes the client asked for.
Choose the whole organization, one pod or one inbox, then approve (⌘↵) or deny. The browser returns to the client with a code, or with error=access_denied. Both responses carry state and iss.
- Tokens. The client exchanges the code at
/oauth2/tokenwith itscode_verifier. A code works once, for 2 minutes. The response holds:
- an access token: a JWT that lasts one hour, bound to
https://mcp.machinemail.dev/mcp;- a refresh token.
Every refresh returns a new refresh token, and the old one stops working. Presenting a replaced refresh token within 60 seconds of its replacement returns the current refresh token, so processes refreshing at once, or a late retry after a lost response, are safe, even when another refresh happened in between. After that, presenting an old refresh token revokes the whole grant, because one of the two copies has leaked. A refresh may ask for fewer scopes; scopes outside the grant are dropped. A refresh token expires after 30 days without use. Redeeming a code twice also revokes what was issued for it.
What a token can do is the intersection of three things:
- the scopes you approved;
- the organization, pod or inbox you picked;
- the permissions those scopes map to.
The sandbox rules and trust handling are the same as for API keys.
Tokens are bound to their resource. A token issued for https://mcp.machinemail.dev/mcp works only on the MCP server, at https://mcp.machinemail.dev/mcp and at the legacy https://machinemail.dev/mcp (the same server); a token issued for the legacy resource=https://machinemail.dev/mcp works on both as well. Neither works on /v0. To call the REST API with OAuth, request resource=https://machinemail.dev/ instead; that token works on both /v0 and the MCP server. The MCP host’s origin alone (resource=https://mcp.machinemail.dev) also means the MCP server. Any other resource is refused with invalid_target.
Disconnecting an app
Connected clients are listed under Settings → Connected apps in the dashboard, and at GET /v0/oauth-grants. Each entry shows the app, what it can reach, who approved it and when it was last used. Disconnect (or DELETE /v0/oauth-grants/{grant_id}) revokes the grant at once: its access and refresh tokens get 401 on the next request.
A client can also sign itself out with POST /oauth2/revoke and its refresh token (RFC 7009). A confidential client must authenticate there with its secret, as at the token endpoint; without it, revocation, a replayed code or an old refresh token is answered 401 invalid_client and the grant stays. The API reference documents every endpoint.
Permissions
Each tool requires the same permission as the REST endpoint it mirrors. A key without it gets a tool error, not an HTTP error:
{ "isError": true, "content": [{ "type": "text", "text": "forbidden: API key lacks the message_send permission" }] }
Every tool error has this shape: isError: true, a text block of <type>: <message> using the error types of the REST API, and the same error object in structuredContent.
Tools
Seventeen tools. Each returns a readable text block for the model and structuredContent with the snake_case data. Inbox ids are email addresses. Full parameters and output shapes are in the MCP tools reference.
Inboxes
| Tool | What it does |
|---|---|
list_inboxes |
Lists the inboxes the credential can access. limit, page_token. Points clients with MCP Events to message.received for new mail. |
create_inbox |
Creates an inbox and returns its address. Optional username, domain, display_name, ttl_seconds (makes it ephemeral) and client_id (idempotency key). Points clients with MCP Events to message.received for new mail. |
Reading mail
| Tool | What it does |
|---|---|
list_threads |
Threads in an inbox, newest first: subjects, senders, counts, no bodies. Filter by labels. |
get_thread |
A thread with every message oldest first, each in agent-safe form with a trust summary. |
list_messages |
Messages in an inbox, newest first, with a 200-character agent-safe preview and trust_level. Spam, blocked, unauthenticated and quarantined mail is hidden; include_quarantine: true lists quarantined mail with content still withheld. Not for polling on a schedule: watch an inbox with message.received. |
get_message |
One message: agent-safe text, text_source, a trust summary, attachments metadata. Raw bodies only with include_raw: true. |
search_messages |
Full-text search over subject, sender and body, with agent-safe previews. q is required. |
get_trust_report |
The full trust report for an inbound message, with hidden text redacted. |
get_attachment |
One attachment of a message: small text files as text, small images as an image, anything else as a download link that works for 10 minutes. Withheld for quarantined, spam, blocked and unauthenticated mail and for high-risk files. See get_attachment. |
wait_for_message |
Blocks until a new message arrives, up to 60 seconds (default 30), then returns it like get_message. Returns { "timed_out": true } otherwise. A short wait, not ongoing monitoring. |
Writing mail
| Tool | What it does |
|---|---|
send_message |
Sends a new email from an inbox: to, subject, text and/or html, cc, bcc, labels, attachments. |
reply_to_message |
Replies in the original thread. reply_all: true copies the other recipients; to overrides them. |
forward_message |
Forwards a message to new recipients, with an optional note and the original’s attachments (include_attachments, default true). |
create_draft |
Saves a draft instead of sending. It is listed in the dashboard under Drafts, where a person can send or delete it; send_at schedules it. |
send_draft |
Sends a draft now. |
update_labels |
Adds or removes labels on a message, for example remove_labels: ["UNREAD"] to mark it read. |
Feedback
| Tool | What it does |
|---|---|
send_feedback |
Tells the MachineMail team how the tools worked: rating, category, a message of up to 2,000 characters and optionally the tool_name it is about. Any credential can use it, within the feedback rate limit. See send_feedback. |
The send, reply, forward and draft tools take attachments: small files as base64 content (up to 1.4 MB per call, since an MCP request is at most 2 MiB), a public url MachineMail downloads, or message_id and attachment_id to send on a file already in MachineMail without passing its bytes through the model. Files get_attachment withholds cannot be sent on. See Attachments.
How tools present untrusted mail
Every read tool shows the model the agent-safe view described in Using trust in agents:
textistrust.content.text, never the raw body, andtext_sourcesays so.- The text block puts the trust level, score and reasons first, then a notice that the content is untrusted, then the body inside
<untrusted_email_content>tags. - Quarantined messages are withheld: no text,
text_source: "withheld", and awithheld_reason. get_attachmentwithholds attachments of quarantined, spam, blocked and unauthenticated mail and high-risk files, and returns text attachments inside the same<untrusted_email_content>tags.
Events (ChatGPT)
The server implements MCP Events, so a client can ask to be woken when mail arrives instead of polling. ChatGPT uses it to run a task whenever a matching event comes in, and ChatGPT dots use it to watch an inbox. The server advertises events in its capabilities and answers events/list, events/subscribe and events/unsubscribe on /mcp, with the same credential as the tools.
| Event | Filter | What arrives in data |
|---|---|---|
message.received |
inbox_id? |
message_id, thread_id, inbox_id, from, to, subject, received_at, trust_level, labels, attachment_count, and a snippet of up to 1,000 characters |
message.delivered |
inbox_id? |
message_id, thread_id, inbox_id, recipients, delivered_at |
message.bounced |
inbox_id? |
message_id, thread_id, inbox_id, recipients, bounce_type, bounce_sub_type, bounced_at |
Leave out inbox_id to watch every inbox the connection can access. Give it to watch one inbox. message.received is the event for monitoring an inbox; message.delivered and message.bounced fire only for mail sent from your inboxes.