# 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](#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](#api-keys) instead, or you write to [hello@roma.app](mailto: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](mailto: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](/developers/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.
