mcp.md


title: "MCP server" description: "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." canonical: "https://machinemail.dev/docs/agents/mcp"

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.

Source: https://machinemail.dev/docs/agents/mcp

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:

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":{}}}'
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:

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/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](https://datatracker.ietf.org/doc/draft-ietf-oauth-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. [Connect an agent without code](/content/docs/guides/connect-an-agent#approve-an-mcp-app/index.html) 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`.

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](#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](/content/app/settings#connected-apps/index.html) 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](/content/docs/reference/api/agent-auth/index.html) 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

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.

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.

Dashboard links

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:

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, or a reply expected within a minute. Trigger the email, then call:

{ "name": "wait_for_message", "arguments": { "inbox_id": "agent-f5pvo2cei042@machinemail.dev", "timeout_seconds": 60, "after": "2026-10-09T09:14:00Z" } }

get_attachment

Attachment ids are in get_message's attachments. What comes back depends on the file:

File Result
Text up to 128 KB of valid UTF-8 (text/* except HTML, JSON, CSV, XML, Markdown, YAML) The text, inside <untrusted_email_content> tags, and in structuredContent.text
PNG, JPEG, GIF or WebP up to 1 MB, whose bytes match the declared type An MCP image block, so the model can look at it
Anything else: PDFs, archives, Office files, SVG, HTML, larger files, mislabelled files download_url, a signed link to this one file that works without credentials for 10 minutes

Every result has the file's filename, content_type, size, the trust layer's risk (low, medium, high, or not_assessed) with risk_reasons, the message's trust_level, how it was delivered (delivery) and the message's dashboard url. Clients on MCP 2025-06-18 or later also get the download link as a resource_link content block.

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.

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

Delivery details

Resources

URI Contents
machinemail://inboxes Every 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

Prompt Arguments What it does
triage_inbox inbox_id Summarises 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.