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

  1. The client calls https://api.roma.app/mcp without a token. The server answers 401 with a WWW-Authenticate header that names the protected-resource metadata: https://api.roma.app/.well-known/oauth-protected-resource.
  2. The client reads that document. It names the resource (https://api.roma.app) and one authorization server.
  3. The client reads the authorization server's metadata at its /.well-known/oauth-authorization-server address and learns the authorize, token and registration endpoints.
  4. The client registers itself with dynamic client registration (RFC 7591) as a public client and starts the authorization-code flow with PKCE (S256).
  5. 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.
  6. 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 https address 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 401 into 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_token with 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_access or 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 400 from the token endpoint; treat any 400 there 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:

http
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 401 on 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 seeWhat it meansWhat to do
The client never opens a browserIt did not follow the 401 into discovery, or it expects a static client idUse an API key, or a client that supports dynamic registration
"redirect_uri mismatch" at sign-inThe URI the client registered and the one it sent differ, often only by portLet the client register again; for a command-line tool, pin its loopback port if it offers to
Sign-in succeeds, tools fail with 401The access token expired and the refresh failedRemove and re-add the connector
Tools fail with 429More than 60 calls a minute on one tokenWait for Retry-After; batch list reads through get_context
A tool you expect is missingThe client cached an older tool listRefresh the connector in the client and start a new conversation
The consent screen names the wrong appSomebody else's client registered under a familiar nameDo 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.