How It Works
Configuration
Progressive (incremental) authorization is opt-in underauth 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’sauthorized_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
Consent UI
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.Multi-Provider Setup
App Configuration
Server Configuration
Standalone vs Nested Apps
Apps can be configured as standalone (direct access) or nested (under parent):| Configuration | Direct Access | Federated Auth |
|---|---|---|
standalone: true | /slack/oauth/authorize | Also in parent consent |
standalone: false (default) | N/A | Only via 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 theapps query
parameter (comma-separated). If omitted, all registered apps are granted (a plain
login keeps working everywhere):
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 viaapps= so the new token is the union of the prior apps plus the
target app:
/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 inauthorized_apps:
OpenAPI Adapter Integration
When using OpenAPI adapters, tools are automatically grouped by auth provider:Tool Consent Types
When consent is enabled, FrontMCP tracks granular tool-level authorization using these types:ConsentToolItem
Represents a tool in the consent UI:| Field | Type | Description |
|---|---|---|
id | string | Tool identifier |
name | string | Display name |
description | string | Tool description |
appId | string | Parent app ID |
requiredScopes | string[] | OAuth scopes needed |
category | string | Grouping category |
ConsentSelection
Captures the user’s tool selection:| Field | Type | Description |
|---|---|---|
selectedTools | string[] | Tool IDs the user authorized |
allSelected | boolean | Whether all tools were selected |
consentedAt | number | Timestamp of consent |
consentVersion | string | Schema version for migration |
ConsentState
Full consent flow state passed to the consent UI:| Field | Type | Description |
|---|---|---|
availableTools | ConsentToolItem[] | All tools requiring consent |
preSelected | string[] | Tools pre-selected by default |
groupBy | string | Grouping field (e.g., appId, category) |
FederatedLoginState
For multi-provider consent where users select which IdPs to authenticate with:| Field | Type | Description |
|---|---|---|
providers | FederatedProviderItem[] | Available identity providers |
required | string[] | Providers that cannot be skipped |
optional | string[] | Providers the user can skip |
Best Practices
Request minimal apps - Pass
apps= to grant only what the agent needs up frontProvide clear descriptions - Users should understand why each app is needed
Handle the 403 structurally - Read
result._meta.auth_url and surface it to the userUse 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 lostTroubleshooting
Every tool call returns AUTHORIZATION_REQUIRED
Every tool call returns AUTHORIZATION_REQUIRED
Incremental authorize didn't expand the grant
Incremental authorize didn't expand the grant
Auth link (auth_url) not working
Auth link (auth_url) not working
- The
auth_urlis in the failed call’sresult._meta.auth_url(only in stateful session mode; stateless mode omits it). - Verify the target app is registered with the server and redirect URIs are configured.
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