# OAuth for MCP client developers

What an MCP client must send to register with Flovia, ask for authorization and get tokens, and what each OAuth error means.

This page is for developers of MCP clients that register themselves with Flovia through OAuth Dynamic Client Registration (RFC 7591) and then sign people in. To connect an app you already use, follow [Sign in with OAuth](/connect/oauth.md) instead.

Flovia checks every OAuth request strictly, so send the values on this page exactly as written. A person needs a Flovia account to approve a connection; see [Access](/access.md).

## Endpoints

| What | Where |
| --- | --- |
| MCP server, the protected resource | `https://api.flovia.dev/mcp` |
| Protected Resource Metadata (RFC 9728) | `/.well-known/oauth-protected-resource/mcp` on `https://api.flovia.dev` |
| Authorization server (issuer) | `https://console.flovia.dev/oauth` |
| Authorization server metadata (RFC 8414) | `https://console.flovia.dev/.well-known/oauth-authorization-server/oauth` |
| Registration | `POST https://console.flovia.dev/oauth/oauth2/register` |
| Authorization | `GET https://console.flovia.dev/oauth/oauth2/authorize` |
| Token | `POST https://console.flovia.dev/oauth/oauth2/token` |

An MCP request without a token gets HTTP 401 with a `WWW-Authenticate` header. Its `resource_metadata` points to the Protected Resource Metadata, which names the authorization server.

Flovia is not an OpenID Connect provider: there is no OpenID discovery document and no `openid` scope. Do not add a query string to the registration or token URL.

## Register your client

Send one JSON object with `POST` to the registration endpoint. Flovia accepts only these fields:

| Field | What Flovia accepts |
| --- | --- |
| `redirect_uris` | Required. 1 to 10 redirect URIs, following the rules below. |
| `client_name` | Optional. 1 to 200 characters, shown to the person on Flovia's consent screen. |
| `grant_types` | Optional. `authorization_code` and `refresh_token` only. |
| `response_types` | Optional. `code` only. |
| `token_endpoint_auth_method` | Optional. `none` only, which is also the default. |
| `application_type` | Optional. `web` or `native`. Without it, Flovia uses `native` when a redirect URI is loopback and `web` otherwise. |
| `scope` | Optional. `flovia:use`, `offline_access` or both, separated by a space. Without it, the client gets both. An authorization request can ask only for scopes registered here. |
| `logo_uri`, `client_uri` | Optional URLs, up to 2048 characters. Accepted and discarded. |
| `contacts` | Optional. Up to 10 strings of at most 254 characters each. Accepted and discarded. |
| `software_id` | Optional. Up to 200 characters. Accepted and discarded. |
| `software_version` | Optional. Up to 100 characters. Accepted and discarded. |
| `policy_uri`, `tos_uri` | Optional URLs, up to 2048 characters. Accepted and discarded. |

For `client_name`, `logo_uri`, `client_uri`, `contacts`, `software_id`, `software_version`, `policy_uri` and `tos_uri`, `null` means absent. Discarded fields are never stored, forwarded or fetched. All other fields refuse `null`. Unknown fields, including `jwks`, `jwks_uri` and `software_statement` even when null, are refused with `invalid_client_metadata`.

Redirect URIs:

- Use `https` on a public host, or `http` on the loopback interface for an app on the person's computer (RFC 8252 section 7.3: the IPv4 or IPv6 loopback address, or the loopback host name).
- The redirect URI in the authorization request must match a registered one exactly. For a loopback redirect URI, only the port may differ.
- No fragment, user name or password.
- A client with no loopback redirect URI and no `application_type` is registered as `web`. A web client's redirect URIs must all be `https` on hosts that are not loopback, so a custom scheme such as `com.example.app://callback` is refused with `invalid_redirect_uri`.

A redirect URI using Cursor's custom scheme must match Cursor's supported callback exactly.

A successful registration returns HTTP 201 with your `client_id` and no client secret.

- Unused registrations are removed after 24 hours without activity. When registration capacity is full, an unused registration can be removed after 2 hours without a registration or a successful authorization that opens sign-in or consent.
- Registrations that have a connection, including a revoked one, and reviewed Client ID Metadata Document clients are not removed this way.
- If your saved `client_id` stops working, register again.

```json
{
  "client_name": "Your MCP client",
  "redirect_uris": ["https://app.example/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "flovia:use offline_access"
}
```

## Authorization request

Open the authorization endpoint in the person's browser with these query parameters. Send each parameter only once.

| Parameter | Value |
| --- | --- |
| `response_type` | `code` |
| `client_id` | Your `client_id` from registration. |
| `redirect_uri` | One of your registered redirect URIs. |
| `resource` | Optional. Defaults to the sole MCP resource `https://api.flovia.dev/mcp` when omitted. If sent, exactly `https://api.flovia.dev/mcp`: no trailing `/`, not the origin alone, not the issuer. |
| `scope` | Required. `flovia:use`, `offline_access` or both, separated by one space, and only scopes your registration has. `flovia:use` lets the access token call the MCP server, and `offline_access` adds a refresh token. Send `flovia:use offline_access` to get both. |
| `code_challenge` | The PKCE challenge: the base64url SHA-256 of your code verifier without padding, 43 characters. |
| `code_challenge_method` | `S256` |
| `state` | Optional, and recommended (RFC 6749). |

Do not send `max_age` or `prompt=none`; Flovia refuses both. Flovia shows its consent screen every time, and the person signs in and approves there.

For a known, enabled client whose `redirect_uri` matches a registered callback (only the port may differ for a registered HTTP loopback URI), Flovia returns authorization refusals with HTTP 302 to that callback. The URL carries `error`, a fixed ASCII `error_description`, your `state` if sent, and `iss` identifying the authorization server. A mismatched `resource` returns `invalid_target`; an invalid scope returns `invalid_scope`. A scope your registration does not have also returns `invalid_scope` to your registered `redirect_uri` through the OAuth library.

An unknown or disabled client, or a callback that does not match a registered URI, never receives Flovia's error redirect: these refusals stay in the browser as HTTP 400 JSON. A registered HTTP loopback URI may differ only in port; all other parts must match, and other redirect URIs require an exact match. A query longer than 12,000 characters, duplicate parameters or a missing `client_id` also gets HTTP 400 JSON. Library refusals for an unknown client or unregistered callback may instead redirect to a Flovia page, without a code.

## Token requests

Send `POST` requests to the token endpoint with the content type `application/x-www-form-urlencoded`. Flovia refuses JSON bodies. Your client is a public client (`token_endpoint_auth_method` `none`): send `client_id` in the body and no `Authorization` header. Send each parameter only once.

To exchange the authorization code:

| Parameter | Value |
| --- | --- |
| `grant_type` | `authorization_code` |
| `client_id` | Your `client_id`. |
| `code` | The code sent to your redirect URI. |
| `code_verifier` | The code verifier behind your `code_challenge`. |
| `redirect_uri` | The same redirect URI as in the authorization request. |
| `resource` | Optional. If you send it, exactly `https://api.flovia.dev/mcp`; any other value gets `invalid_target`. |

To refresh:

| Parameter | Value |
| --- | --- |
| `grant_type` | `refresh_token` |
| `client_id` | Your `client_id`. |
| `refresh_token` | The newest refresh token you received. |
| `resource` | Optional, as above. |

Access tokens last 15 minutes. Refresh tokens last 30 days and rotate: each refresh returns a new refresh token, so store it and stop using the previous one. If a reply is lost, sending the previous refresh token again within 300 seconds returns the same new tokens. Only the refresh token just before the newest one can be sent again this way. Sending an older refresh token, even within those 300 seconds, or the previous one after them, ends the connection: its tokens stop working and the person has to connect again. An authorization code works once; sending it again also ends the connection.

Send the access token to `https://api.flovia.dev/mcp` as `Authorization: Bearer <token>`.

## Client ID Metadata Documents

Flovia's authorization server metadata sets `client_id_metadata_document_supported` to `true`, but Flovia accepts a Client ID Metadata Document only for clients it has reviewed. Any other URL used as `client_id` gets HTTP 400 `invalid_client`. Other clients register as described above. The metadata lists only `none` as a token endpoint authentication method, so a reviewed client whose document also supports `private_key_jwt` connects as a public client with PKCE; registration accepts only `none`.

## Errors

Flovia answers each refusal with JSON that has an `error` field, except for the HTTP 302 redirects in the table. The HTTP 400 authorization rows apply when a callback cannot safely receive an error or when early query checks fail.

When Flovia's query or token-format checks return HTTP 400 `invalid_request`, `error_description` tells you which input rule failed and `error_uri` links to this section. The description is fixed guidance: it does not echo your client ID, callback, state, code or token. Correct that input before trying again. `invalid_target` also has a fixed ASCII description. Errors returned by the OAuth library may use a different description or omit these fields.

Registration answers `invalid_request` only when its URL has a query string. A registration body Flovia cannot accept gets `invalid_client_metadata`, `invalid_redirect_uri` or `invalid_scope` instead. An `invalid_request` after a successful registration comes from the authorization request or the token request; the Step column below tells them apart.

| Step | HTTP | Error | Common causes |
| --- | --- | --- | --- |
| Register | 400 | `invalid_client_metadata` | A field not listed above, a `null` value for a security field, a value of the wrong type or beyond its bound, a `token_endpoint_auth_method` other than `none` (such as `client_secret_basic`, `client_secret_post` or `private_key_jwt`), another grant type or response type, no `redirect_uris` or more than 10, or a body that is not a JSON object. |
| Register | 400 | `invalid_redirect_uri` | A redirect URI that breaks the rules above: `http` on a host that is not loopback, `https` on a loopback host, a fragment, or a custom scheme for a web client. |
| Register | 400 | `invalid_scope` | A scope other than `flovia:use` and `offline_access`, such as `openid`. |
| Register | 400 | `invalid_request` | A query string on the registration URL. |
| Register | 403 | `request_refused` | An `Authorization` header. Registration takes no credentials. |
| Register | 429 | `rate_limited` | Too many registrations. Wait for the number of seconds in `Retry-After`. |
| Register | 503 | `temporarily_unavailable` | Flovia is holding too many unused registrations. Registrations idle for 2 hours can be removed to make room. `Retry-After` gives the seconds until the oldest unused registration leaves that grace (at most 2 hours). Wait once, then register again; if capacity remains full, stop automatic retries and try connecting later. This registration response is Flovia’s extension, not an error defined by RFC 7591, and differs from the entry budget’s 429 `rate_limited`. |
| Authorize | 400 | `invalid_request` | `code_challenge_method` not `S256`; `code_challenge` not 43 base64url characters; `max_age` sent; `client_id` missing; any parameter sent twice; or a query longer than 12,000 characters. |
| Authorize | 400 | `invalid_target` | `resource` sent but not exactly `https://api.flovia.dev/mcp`. |
| Authorize | 302 | `invalid_target` | Same resource mismatch, sent to the exact registered callback with `state` if supplied and `iss`. |
| Authorize | 302 | `invalid_request` | Invalid PKCE or `max_age`, sent to the exact registered callback with `state` if supplied and `iss`. |
| Authorize | 302 | `consent_required` | `prompt=none`, sent to the exact registered callback with `state` if supplied and `iss`. |
| Authorize | 400 | `invalid_scope` | `scope` missing, or a value other than `flovia:use` and `offline_access`, such as `openid`. |
| Authorize | 400 | `consent_required` | `prompt=none`. Flovia always asks the person. |
| Authorize | 400 | `invalid_client` | A URL `client_id` for a client Flovia has not reviewed. |
| Authorize | 302 | `invalid_redirect` | A `redirect_uri` not registered for the `client_id`. The browser goes to a Flovia page with this `error` in its address and no code, not to your app. |
| Authorize | 302 | `invalid_scope` | A missing or invalid scope, or a scope your registration does not have, such as `offline_access` for a client registered with `flovia:use` only. The browser goes back to the registered `redirect_uri` with this `error`, your `state` if supplied and `iss`. |
| Authorize | 403 | `request_refused` | An `Authorization` header. |
| Authorize | 429 | `rate_limited` | Too many authorization requests. Wait a minute and try again. |
| Token | 400 | `invalid_request` | A body that is not `application/x-www-form-urlencoded`, such as JSON; an unreadable body or one larger than 16,384 bytes; a parameter sent twice; a query string on the token URL; or no `client_id` in a code exchange. |
| Token | 401 | `invalid_request` | A `code_verifier` that does not match the `code_challenge`. |
| Token | 401 | `invalid_client` | An `Authorization` header, Basic included. |
| Token | 400 | `unsupported_grant_type` | A `grant_type` other than `authorization_code` and `refresh_token`. |
| Token | 400 | `invalid_target` | `resource` sent but not exactly `https://api.flovia.dev/mcp`. |
| Token | 400 | `invalid_grant` | An unknown refresh token; a replaced one older than the one just before the newest, or that one after the 300-second window; one issued to another `client_id`; an authorization code sent twice; or a connection that has ended. |
| Token | 503 | `temporarily_unavailable` | Another refresh of the same connection is running, or the account's access is paused. Retry after the seconds in `Retry-After`. |
| Token | 429 | `rate_limited` | Too many token requests. Wait for the number of seconds in `Retry-After`. |

### invalid_grant after refresh token reuse

Within 300 seconds of a refresh, only the refresh token just before the newest one may be sent again, and it gets the same answer (see Token requests). Any other refresh token that has already been replaced, an older one even within those 300 seconds or the previous one after them, is reuse: Flovia ends the whole connection, and all of its access tokens and refresh tokens stop working. The token endpoint answers HTTP 400 `invalid_grant` with an `error_description` that says so and an `error_uri` that links here. Every later refresh with a token of that connection gets the same answer.

This usually happens when two copies of a client share one stored refresh token, for example a copied database, a restored backup or two machines reading the same file. Each refresh replaces the token in only one copy, so the other copy later sends an old one. Keep the refresh token in one place, store the newest one after every refresh, and do not copy or share the database or file that holds it. To recover, connect again: the person signs in and approves your client once more.
