Skip to main content
Progressive authorization allows users to authorize apps incrementally, rather than all at once. This improves UX by only requesting access when tools actually need it.

How It Works


Configuration

Progressive (incremental) authorization is opt-in under auth in local/remote (orchestrated) mode. Setting an incrementalAuth block turns on app-level gating: the minted access token carries an authorized_apps claim, and a tools/call for an app NOT in that claim is rejected with AUTHORIZATION_REQUIRED. A server with no incrementalAuth block mints no such claim and performs no app-level gating (the historical allow‑all behavior is preserved).
incrementalAuth and consent are independent features. consent gates which tools a token may call (the consent claim); incrementalAuth gates which apps a token may call (the authorized_apps claim). You can enable either or both.

Authorization Hierarchy

Progressive auth operates at three levels:

Authorized-app set (authorized_apps claim)

The granted app set is carried in the minted access token’s authorized_apps claim and expands as the user authorizes more apps. Each incremental authorize mints a new access token whose claim is the union of the apps already granted plus the newly authorized one:

Initial State

Token claim:authorized_apps: ["crm"]

After Slack Auth

New token claim:authorized_apps: ["crm", "slack"]

After GitHub Auth

New token claim:authorized_apps: ["crm", "slack", "github"]
An incremental authorize mints a fresh access token (via the standard OAuth code exchange) whose authorized_apps claim is expanded — the client swaps in the new token. The user’s identity (sub) is preserved, and apps already granted are not re-authorized. Upstream provider tokens themselves stay server-side and encrypted at rest (orchestrated mode); they are never embedded in the JWT.

Authorization Response

When a tool’s parent app is not in the token’s authorized_apps claim, the tools/call resolves to a CallToolResult with isError: true carrying the authorization metadata under _meta (so MCP clients/agents can react structurally and surface the authorize link). The error class is AuthorizationRequiredError (code AUTHORIZATION_REQUIRED, HTTP 403):
In stateless session mode there is no auth_url and supports_incremental is false (the grant lives entirely in the JWT and cannot be extended in place — the user must re-authenticate). In stateful mode the auth_url drives the incremental authorize below.

Handling in Clients


The interactive tool-selection consent screen is rendered when consent.enabled is true: after login (or, for federated flows, after the last provider links), /oauth/callback shows a tool picker, and the chosen tools are embedded in the token’s consent claim and enforced on every tools/call (an unselected tool is rejected with TOOL_NOT_CONSENTED). The mock below illustrates a multi-app authorization variant; the shipped screen is the single tool-selection page. See Local OAuth → Consent.
The intended consent experience lets users choose which apps to authorize:

Multi-Provider Setup

App Configuration

Server Configuration


Standalone vs Nested Apps

Apps can be configured as standalone (direct access) or nested (under parent):

Skip and Authorize Later

Users grant a subset of apps initially and authorize more later.

Initial grant

The client declares the apps to grant on the first authorize via the apps query parameter (comma-separated). If omitted, all registered apps are granted (a plain login keeps working everywhere):
The minted token carries authorized_apps: ["crm"]; calling a slack:* tool then returns AUTHORIZATION_REQUIRED with an auth_url.

Incremental authorization (expand the grant)

To add a skipped app, start an incremental authorize. The client carries its current grant forward via apps= so the new token is the union of the prior apps plus the target app:
This renders a single-app authorization page; on submit, /oauth/callback mints a fresh code → the client exchanges it for a token with authorized_apps: ["crm", "slack"]. The auth_url returned in the 403 _meta points the agent straight at this flow.
Unknown app ids passed via apps=/app= are dropped — a client can never forge a grant to a non-existent app. Gating is per real app, so a bogus id is harmless.

Session Token Structure

A token minted with incremental auth enabled carries the granted set in authorized_apps:
Upstream provider access/refresh tokens are stored server-side and encrypted at rest (orchestrated mode), never embedded in the JWT. Only the non-sensitive authorized_apps id list is carried as a claim.

OpenAPI Adapter Integration

When using OpenAPI adapters, tools are automatically grouped by auth provider:
Tools from each adapter are grouped by their auth configuration and appear in the consent UI accordingly.
When consent is enabled, FrontMCP tracks granular tool-level authorization using these types:

ConsentToolItem

Represents a tool in the consent UI:

ConsentSelection

Captures the user’s tool selection:

ConsentState

Full consent flow state passed to the consent UI:

FederatedLoginState

For multi-provider consent where users select which IdPs to authenticate with:

Best Practices

Request minimal apps - Pass apps= to grant only what the agent needs up front
Provide clear descriptions - Users should understand why each app is needed
Handle the 403 structurally - Read result._meta.auth_url and surface it to the user
Use stateful sessions - Required for the incremental auth_url (stateless omits it)
Carry apps= forward - On an incremental authorize, include the prior grant so it isn’t lost

Troubleshooting

  • Confirm the client sent apps= on the initial authorize (or omit it to grant all apps).
  • The authorized_apps claim only appears when incrementalAuth is enabled — verify the auth block has an incrementalAuth object (an empty {} enables it with defaults).
  • App ids in apps=/app= must match the app’s id (metadata.id, else the slug of name); unknown ids are dropped.
  • The client must carry its current grant forward via apps= on the incremental authorize; otherwise only the target app (plus nothing) is granted.
  • The new token from the code exchange replaces the old one — make sure the client swaps it in before retrying the call.
  • Confirm incrementalAuth.enabled is not false.

Next Steps

Remote OAuth

Configure external identity providers

Tokens & Sessions

Token lifecycle and session management

Production Checklist

Security requirements for deployment

Authorization Modes

Choose the right auth mode