Authentication
Roma's MCP server accepts two kinds of bearer token on the same endpoint.
- OAuth 2.1, the default and what every client with a browser uses. The client discovers the authorization server from the MCP endpoint, registers itself, sends the person to sign in and approve in their browser, and receives a short-lived token it presents on every call. This is the flow the Model Context Protocol specifies, and the one ChatGPT, Claude, Cursor, Codex and the other clients implement.
- An API key, for an agent that has no browser to open: a scheduled job, a self-hosted agent, a client that only takes a pasted key. The person creates it in Roma under Settings, then Connections, and pastes it where the client asks for a bearer token. See API keys below.
Either way the token acts as the whole person on their own workspace; the tools and their behaviour are the same.
The flow
- The client calls
https://api.roma.app/mcpwithout a token. The server answers401with aWWW-Authenticateheader that names the protected-resource metadata:https://api.roma.app/.well-known/oauth-protected-resource. - The client reads that document. It names the resource (
https://api.roma.app) and one authorization server. - The client reads the authorization server's metadata at its
/.well-known/oauth-authorization-serveraddress and learns the authorize, token and registration endpoints. - The client registers itself with dynamic client registration (RFC 7591) as a public client and starts the authorization-code flow with PKCE (
S256). - The person's browser opens Roma's sign-in, then the consent screen at
https://roma.app/oauth/consent. It names the app and where the app returns them after approval ("claude.ai", "chatgpt.com", "an app on this computer"), so a look-alike cannot pretend to be a real client. - The authorization server issues an access token and a refresh token. The client retries the call with
Authorization: Bearer <token>and every tool is scoped to that person.
The client configuration is therefore header-less: an address and nothing else.
What a client has to support
- The Model Context Protocol over Streamable HTTP. The server negotiates protocol versions 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 and 2024-10-07.
- OAuth 2.1 authorization-code flow with PKCE
S256. Plain PKCE is accepted but not recommended. - Dynamic client registration. The server has no fixed client ids today; a client that can only use a pre-registered id connects with an API key instead, or you write to hello@roma.app.
- A redirect URI reachable from the person's browser: an
httpsaddress on the client's own host, or a loopback address for a command-line tool. Redirect URIs are matched exactly, including the port. - Following the
401into discovery. A client that expects to be told the authorization server up front has to read it from the protected-resource metadata instead.
Tokens
- Access tokens are JWTs signed with ES256 and valid for one hour. The server verifies the signature and expiry locally on every call.
- Refresh tokens are issued on every authorization. Refresh proactively before the access token expires; the token endpoint accepts
grant_type=refresh_tokenwith the client id and no secret. - Scopes do not narrow what a token can do. Every token acts as the whole person on their own workspace; what a client may run without asking is decided by the tool annotations on the client's side, not by scope. Request
openid email offline_accessor leave the scope out. - Revocation. Removing the connector in the client deletes its token. The person can also revoke a grant on Roma's side by writing to hello@roma.app. A revoked or expired refresh token answers a
400from the token endpoint; treat any400there as "sign in again", not as something to retry.
API keys
A key is for the client that cannot open a browser. It is created in Roma (Settings, then Connections, at the bottom), shown once, and sent on every call as a bearer token:
POST https://api.roma.app/mcp
Authorization: Bearer roma_…
Content-Type: application/json
- A key starts with
roma_and is 48 characters long. Roma stores only its hash, so a key that is lost cannot be shown again; create a new one and revoke the old. - A key acts as the whole person on their own workspace, exactly like an OAuth token. Keep it as private as a password, out of prompts, shared configs and repositories.
- Settings holds one key at a time: creating a new key replaces the old one, which answers
401on its next call. Revoking takes effect the same way. - The same rate limit applies per key as per OAuth token.
- A client that supports both should prefer OAuth: the person approves it in their own browser, the token is short-lived, and nothing has to be pasted.
In a client that asks for a header rather than a bearer field, the header is Authorization: Bearer roma_…. Claude Code, Cursor, VS Code, Codex, Gemini CLI, OpenClaw and Poke each have a place for it; ChatGPT and Claude.ai do not, and use OAuth. The same key opens the REST API.
Rate limits
Each token may make 60 requests a minute. Above that the server answers 429 with a Retry-After header and a JSON-RPC error. A platform-level limit per IP address sits above that. Tool lists are cheap; search runs an embedding per query and is the call to pace.
What the token can reach
Only the person's own tasks, notes, projects, collections, automations, trash and the read-only view of their memory. Not their connected apps, not their email, not other people's shared items, not Roma's own instructions to its model. Those stay inside Roma.
Troubleshooting
| What you see | What it means | What to do |
|---|---|---|
| The client never opens a browser | It did not follow the 401 into discovery, or it expects a static client id | Use an API key, or a client that supports dynamic registration |
| "redirect_uri mismatch" at sign-in | The URI the client registered and the one it sent differ, often only by port | Let the client register again; for a command-line tool, pin its loopback port if it offers to |
Sign-in succeeds, tools fail with 401 | The access token expired and the refresh failed | Remove and re-add the connector |
Tools fail with 429 | More than 60 calls a minute on one token | Wait for Retry-After; batch list reads through get_context |
| A tool you expect is missing | The client cached an older tool list | Refresh the connector in the client and start a new conversation |
| The consent screen names the wrong app | Somebody else's client registered under a familiar name | Do not approve; the screen says where approval would return you |
For the curious
The authorization server is Roma's own identity provider, the same sign-in the web and iOS apps use, so a person has one account and one set of sessions. The consent screen is Roma's, the tokens are the same kind the apps carry, and the MCP server verifies them without a network round-trip. An API key is verified by a hash lookup on the same request path. The trade-off is that the server does not yet publish client-id metadata documents or resource indicators; when it does, this page will say so.