Skip to content
machinemail

MCP server

The hosted MachineMail MCP server - endpoint, stateless Streamable HTTP transport, OAuth sign-in or an API key, and the tools, resources and prompt it exposes.

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.

Endpointhttps://mcp.machinemail.dev/mcp
TransportStreamable HTTP, stateless
AuthOAuth sign-in, or Authorization: Bearer <API key>
Server namemachinemail
Description documenthttps://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 POST to /mcp; send Accept: application/json, text/event-stream. On the 2025 revisions a response comes back as a single server-sent event (Content-Type: text/event-stream); on 2026-07-28 it 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 GET with Accept: text/event-stream and a DELETE return 405 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 or POST /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.

Client setup shows both options for each client.

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.

  1. Discovery. A request without a valid token gets a 401 that points to the protected resource metadata of /mcp:

    http
    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.

  2. 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 as token_endpoint_auth_methods_supported includes none.
    • A registration is public by default (token_endpoint_auth_method: "none"). A client that registers with client_secret_post or client_secret_basic gets a client_secret in the response, shown once and never expiring (client_secret_expires_at: 0). It then authenticates at the token and revocation endpoints with HTTP Basic or client_secret in the body, either way. Other methods are registered as none. Grant types other than authorization_code and refresh_token are dropped from the registration.
    • Every authorization uses PKCE, public client or not.

    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://.

  3. Authorization. The client opens https://machinemail.dev/oauth2/authorize with response_type=code, a PKCE code_challenge (S256), its redirect_uri, state, and resource=https://mcp.machinemail.dev/mcp (RFC 8707). Without resource (clients on the 2025-03-26 spec), the token is for the MCP server. Without scope, or with only openid, profile, email or offline_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 an invalid_scope error (a misspelled scope never widens to every scope). Your browser lands on the dashboard’s Connect an app page; sign in first if needed. 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.

  4. Tokens. The client exchanges the code at /oauth2/token with its code_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:

json
{ "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

Fifteen 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

ToolWhat it does
list_inboxesLists the inboxes the credential can access. limit, page_token.
create_inboxCreates an inbox and returns its address. Optional username, domain, display_name, ttl_seconds (makes it ephemeral) and client_id (idempotency key).

Reading mail

ToolWhat it does
list_threadsThreads in an inbox, newest first: subjects, senders, counts, no bodies. Filter by labels.
get_threadA thread with every message oldest first, each in agent-safe form with a trust summary.
list_messagesMessages 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.
get_messageOne message: agent-safe text, text_source, a trust summary, attachments metadata. Raw bodies only with include_raw: true.
search_messagesFull-text search over subject, sender and body, with agent-safe previews. q is required.
get_trust_reportThe full trust report for an inbound message, with hidden text redacted.
wait_for_messageBlocks until a new message arrives, up to 60 seconds (default 30), then returns it like get_message. Returns { "timed_out": true } otherwise.

Writing mail

ToolWhat it does
send_messageSends a new email from an inbox: to, subject, text and/or html, cc, bcc, labels.
reply_to_messageReplies in the original thread. reply_all: true copies the other recipients; to overrides them.
forward_messageForwards a message to new recipients, with an optional note.
create_draftSaves 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_draftSends a draft now.
update_labelsAdds or removes labels on a message, for example remove_labels: ["UNREAD"] to mark it read.

Read-only tools are annotated readOnlyHint: true. The three sending tools and send_draft are annotated openWorldHint: true, so clients that ask before side effects will ask before they send.

Results carry a url that opens the inbox, thread, message or draft in the MachineMail dashboard, so an agent can answer “link me to the email you just sent”. A sent message links to its thread, scrolled to that message:

text
Sent: message msg_8f2k1 in thread thd_3m9q — https://machinemail.dev/app/inboxes/agent-f5pvo2cei042@machinemail.dev/thd_3m9q?org=org_4t7w#msg_8f2k1

The link is in the text block too, for clients that show the model only text. ?org= names the credential’s organization, so the dashboard switches to it when the person’s session is in another one. See Dashboard links for every shape.

wait_for_message

The tool agents want for one-time codes and sign-up links. Trigger the email, then call:

json
{ "name": "wait_for_message", "arguments": { "inbox_id": "agent-f5pvo2cei042@machinemail.dev", "timeout_seconds": 60, "after": "2026-10-09T09:14:00Z" } }
  • It only counts inbound messages received after after, by arrival time rather than the sender’s Date header. Without after, it counts from when the call starts. Pass the time you triggered the email so a message that arrived before the call is still caught.
  • It returns the oldest matching message.
  • Quarantined, spam, blocked and unauthenticated mail does not wake it.
  • It wakes as soon as the message is stored and checks storage every two seconds as well.
  • On timeout: { "timed_out": true, "inbox_id": "...", "waited_seconds": 60 }.

How tools present untrusted mail

Every read tool shows the model the agent-safe view described in Using trust in agents:

  • text is trust.content.text, never the raw body, and text_source says 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 a withheld_reason.
  • Hidden segments never appear; get_trust_report replaces them with a placeholder.
  • The server’s instructions and every read tool’s description tell the model to act only on verified senders, read and summarise known ones, treat the rest as information, and leave quarantined mail to a human. The sending tools tell it not to send, reply or forward because an email asked it to.

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. 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.

EventFilterWhat arrives in data
message.receivedinbox_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.deliveredinbox_id?message_id, thread_id, inbox_id, recipients, delivered_at
message.bouncedinbox_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.

Set up a task in ChatGPT

  1. Add MachineMail to ChatGPT as a plugin, with https://mcp.machinemail.dev/mcp as the server URL, and sign in. The connection needs the messages.read scope, the same one the reading tools need, so no extra scope is asked for.
  2. Open the plugin’s settings and choose Refresh tools. The three events then show up next to the tools.
  3. Start a Work chat on ChatGPT web, or select Work and Cloud in the desktop app. Then say what to watch and what to do, for example: “When a new email arrives in support@yourco.machinemail.dev, summarise it and draft a reply for me to review.”
  4. ChatGPT subscribes. MachineMail checks the callback with a signed challenge, and then sends each matching email to ChatGPT as it arrives.
  5. To stop, tell ChatGPT to stop monitoring. Revoking the connection in the dashboard also stops delivery at once. A subscription that ChatGPT stops renewing ends within 24 hours.

What is delivered, and what is not

  • The payload carries headers, the trust verdict and a short snippet of the agent-safe text (trust.content.text), never the raw body or HTML. The agent reads the whole message with get_message, using the ids in the payload. The snippet and subject are still untrusted third-party text.
  • Spam, blocked, unauthenticated and quarantined mail is never delivered. Neither is a message that was quarantined, moved to spam or trash, or deleted before it went out.
  • Mail sent from one of your own MachineMail inboxes is not delivered either, so an agent that replies or forwards cannot wake itself in a loop.
  • Every delivery checks the connection again. If the OAuth connection or API key is revoked, or it loses message read access, the subscription is deleted and nothing more is sent.

Delivery details

  • Each event is one POST of { eventId, name, timestamp, data, cursor }, at most 256 KiB, signed with Standard Webhooks (webhook-id, webhook-timestamp, webhook-signature) using the subscription’s whsec_ secret, plus X-MCP-Subscription-Id.
  • Callbacks must be public https URLs. The server never follows redirects. It connects to the address it checked, with TLS verified against the hostname.
  • Any 2xx response acknowledges the event. Other responses are retried after 30 s, 2 min, 10 min, 30 min, 1 h and 2 h, with a fresh signature on each attempt. 410 deletes the subscription. 413 is not retried.
  • A subscription lasts 24 hours (refreshBefore). ChatGPT renews it before then, and may rotate the secret when it does. Both secrets sign during a 24-hour rotation window.
  • cursor is a replay position. If a subscription lapses and comes back with its last cursor, the events it missed are sent, up to 24 hours or 1,000 events back. truncated: true means some events could not be recovered.

Resources

URIContents
machinemail://inboxesEvery inbox the credential can access, as JSON (up to 100).
machinemail://inboxes/{inbox_id}/threads/{thread_id}A thread with its messages in agent-safe form.

Prompts

PromptArgumentsWhat it does
triage_inboxinbox_idSummarises unread mail grouped by trust level (verified, known, unknown, suspicious, quarantined). It reads but never sends, replies or relabels, and lists verified senders’ requests as proposals for you to approve.