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:

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

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

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

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

  1. 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. Connect an agent without code shows this page step by step. The page shows:

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.

  1. Tokens. The client exchanges the code at /oauth2/token with its code_verifier. A code works once, for 2 minutes. The response holds:

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

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.