OAuth for MCP client developers
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 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.
Endpoints
Section titled “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
Section titled “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
httpson a public host, orhttpon 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_typeis registered asweb. A web client’s redirect URIs must all behttpson hosts that are not loopback, so a custom scheme such ascom.example.app://callbackis refused withinvalid_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_idstops working, register again.
{ "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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.