Connect a client
Two transports, one server.
Endpoints
Streamable HTTP is the one to build against. SSE is there for clients that have not moved yet.
| Transport | URL |
|---|---|
| Streamable HTTP | https://api.openemail.uk/mcp |
| SSE | https://api.openemail.uk/sse |
How the grant works
- Point the client at the Streamable HTTP URL.
- It reads the authorization-server metadata and registers itself.
- You are sent here to sign in and are shown what is asking and where your access would be sent.
- You choose what it gets: one workspace, a set of permissions, every address you can reach or only some domains and addresses, and how long the approval lasts.
- Approving issues the client an access token held to exactly that.
OAuth, and only OAuth. A Better Auth session cookie in the Authorization header does not work. It was how this connected before the grant existed, and the endpoints no longer accept it.
Account → Connected apps lists every app holding a token and what each may reach, changes that, and removes one. Terminals signed in with the command line tool are under Account → Command line instead. Removing an app deletes every token it holds and the approval behind it, so the next attempt has to ask again. A client connected before approvals existed holds none, so its next call is refused and it asks you again.
Tokens, refresh and revocation
Approving sends the person back to the client with a code, which the client exchanges at the token endpoint, with its PKCE verifier, for an access token and a refresh token. The access token lasts an hour, and expires_in in the answer says so. The discovery document names every endpoint, so a client never has to hard-code one.
The token endpoint takes a form, JSON or multipart body and reads each field once. A request that sends a field such as grant_type, code or refresh_token more than once with different values, or as anything but a single string, such as a JSON array or a number, is refused with 400 invalid_request before anything is checked. So is a body with no Content-Type, or one of any other type. A field sent twice with the same value is read once.
| Endpoint | Path |
|---|---|
| Discovery | GET /.well-known/oauth-authorization-server, also with /mcp or /sse after it |
| Registration | POST /auth/mcp/register |
| Authorization | GET /auth/mcp/authorize |
| Token | POST /auth/mcp/token |
| Revocation | POST /auth/mcp/revoke |
A refresh token works once. A successful refresh returns a new access token and a new refresh token, so save the new refresh token before anything else. The refresh token it replaced stops working at that moment. The access token it replaced keeps working until it expires or until the next refresh, so a call already on its way still succeeds. The same client presenting the replaced refresh token again within 30 seconds gets the same new pair back, with expires_in counting down, so two processes that share one sign-in and refresh at once both end up holding one pair.
An app registered with a client secret sends it on a refresh too, as client_secret in the body or in a Basic Authorization header, and is refused with 401 invalid_client without it. Each app can refresh 60 times an hour for each person. The next refresh is refused with 429 too_many_requests and a Retry-After header, and the refresh token works again once that time has passed.
Presenting a used refresh token more than 30 seconds after it was replaced is read as a stolen token. The request is refused with invalid_grant, and every token the app received from that one sign-in goes at once, the first pair and every refresh since, together with any verification the app had completed. The approval stays, and so do the app’s other sign-ins with that person, so the app only has to sign in again. A client whose refresh answer was lost can retry with the same refresh token within the 30 seconds, and should sign in again after that.
POST /auth/mcp/revoke is how a client signs itself out. It takes a form or JSON body with token, which may be the access token or the refresh token, and client_id. token_type_hint is accepted and not needed, because both kinds are looked up. A refresh token that was already replaced still counts for 7 days, so signing out with an old copy still ends the app’s access. The answer is 200 with {} even for a token it does not know, so a retry is always safe. A token issued to a different client_id is refused with 401 invalid_client, and nothing is revoked. The discovery document advertises it as revocation_endpoint.
curl -X POST "https://api.openemail.uk/auth/mcp/revoke" \ -d token="$REFRESH_TOKEN" \ -d client_id="$CLIENT_ID" {}Revoking signs the app out completely, whichever of its tokens you present. Every token it holds for that person, the approval behind them and any verification it had completed go at once, exactly as if the person had removed it in Account → Connected apps, so the next sign-in asks for approval again.
The same access token is asked for a verification code before a few sensitive changes: on the REST API, such as deleting a domain or changing a webhook, and through the tools that make the same changes, such as createRule and removeDomain. One verified code covers both for 60 minutes. A client that cannot ask the person for a code, such as one that only speaks MCP, is covered the same way when the person chooses Allow changes for 60 minutes on it in Account → Connected apps.