---
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](https://machinemail.dev/docs/agents/clients) 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](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports#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`.

```bash title="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":{}}}'
```

```text title="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](https://machinemail.dev/docs/agents/auth-md) uses its access token here too. Before it is claimed, the token reaches only its sandbox inbox.

[Client setup](https://machinemail.dev/docs/agents/clients) shows both options for each client.

### OAuth sign-in

MachineMail follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization) 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](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](https://machinemail.dev/docs/guides/connect-an-agent#approve-an-mcp-app) 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](https://machinemail.dev/app/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](https://machinemail.dev/docs/reference/api/agent-auth) 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](https://machinemail.dev/docs/reference/errors) 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](https://machinemail.dev/docs/reference/mcp-tools).

### Inboxes

| Tool           | What it does                                                                                                                                                                                                                      |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list_inboxes` | Lists the inboxes the credential can access. `limit`, `page_token`. Points clients with [MCP Events](#events-chatgpt) 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](https://machinemail.dev/docs/trust/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](#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](https://machinemail.dev/docs/reference/rate-limits#mcp-feedback). See [send\_feedback](https://machinemail.dev/docs/reference/mcp-tools#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](https://machinemail.dev/docs/reference/mcp-tools#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:

```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](https://machinemail.dev/docs/reference/mcp-tools#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:

```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 }`.
- It is a short-lived wait, not a way to watch an inbox. A client with [MCP Events](#events-chatgpt) subscribes to `message.received`; clients without it (Claude, for example) call `wait_for_message` or `list_messages` when the user asks.

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

- Attachments of quarantined, spam, blocked and unauthenticated mail, and attachments the trust report rates high risk, are withheld: `delivery: "withheld"` with a `withheld_reason`. A person can still review and download them in the dashboard. The verdict comes from the trust report and the inbox's block list, not only from labels, so removing a `SPAM` or `UNAUTHENTICATED` label does not release the attachments.
- Medium-risk attachments come back with a `warning`, and so do attachments of `suspicious` mail and files the trust layer did not rate (those are checked by name, type and content when they are opened, and withheld if that check finds them high risk).
- Text attachments have zero-width, direction-changing and Unicode tag characters removed, as message bodies do; a `warning` says when the file hid text that way.
- The link is a credential for one file. Fetch it with your own tools or give it to the person; it can be fetched as often as needed until `expires_at`, then call `get_attachment` again. Each fetch checks the message and the credential again, so a link stops working if the message is deleted or quarantined, or the API key or OAuth connection that asked for it is revoked or no longer covers the inbox. Files download with `Content-Disposition: attachment` and `X-Content-Type-Options: nosniff`, as from the API.

## How tools present untrusted mail

Every read tool shows the model the agent-safe view described in [Using trust in agents](https://machinemail.dev/docs/trust/using-trust-in-agents#reading-trust-through-mcp):

- `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`.
- Mail a [trusted domain](https://machinemail.dev/docs/concepts/trusted-domains) vouches for is given in full, with the trusted entry and any content warnings in the trust lines. There is no tool for adding a trusted domain: that takes an organization API key or the dashboard.
- `get_attachment` withholds attachments of quarantined, spam, blocked and unauthenticated mail and high-risk files, and returns text attachments inside the same `<untrusted_email_content>` tags.
- 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 unknown ones as information only, and leave suspicious and quarantined mail to a human. The sending tools tell it not to send, reply or forward because an email asked it to.
- Sign-up codes are the one exception, because a service you have just signed up with has never corresponded with the inbox, so its mail lands at `known`: the server's instructions and the `wait_for_message` description tell the model to use a code or sign-in link it just requested only when `trust.authentication` shows a DMARC pass that is aligned, and the sender's domain is the service it signed up with.

## Events (ChatGPT)

The server implements [MCP Events](https://developers.openai.com/plugins/build/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: &#x2A;"When a new email arrives in [support@yourco.machinemail.dev](mailto: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, nor is mail the trust layer could not check. 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](https://www.standardwebhooks.com/) (`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

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