> ## Documentation Index
> Fetch the complete documentation index at: https://docs.prelude.so/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Login

> Let MCP clients sign your users in by configuring Prelude Auth as the OAuth 2.0 authorization server for your MCP server.

The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) authorization spec has MCP servers delegate authentication to an OAuth 2.0 authorization server. Prelude Auth **is** that authorization server: enable the OAuth server surface on your app and MCP clients — Claude, IDE agents, or any spec-compliant client — sign users into your MCP server against your existing Prelude Auth users, with no extra identity provider to run.

This guide configures the authorization server. Your MCP server stays the OAuth **resource server**: it advertises Prelude Auth as its authorization server and verifies the access tokens Prelude issues.

## How it works

Prelude Auth implements the authorization-code flow with PKCE ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749) + [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)) and publishes [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) authorization-server metadata. Clients register themselves — no manual onboarding — through either mechanism the MCP authorization spec allows:

* **Dynamic Client Registration (DCR, [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591))** — the client `POST`s its metadata to a registration endpoint and receives a generated `client_id`.
* **Client ID Metadata Document (CIMD, MCP 2025-11-25 / SEP-991)** — the client's `client_id` **is** an `https` URL that serves its metadata document. Prelude fetches and caches it on first use; there is no registration call.

A login looks like this:

<Steps>
  <Step title="Discovery">
    The client reads `GET /.well-known/oauth-authorization-server` to learn the authorize, token, registration, and JWKS endpoints.
  </Step>

  <Step title="Registration">
    The client registers via DCR or presents a CIMD `client_id` URL — whichever you enabled.
  </Step>

  <Step title="Authorize">
    The client redirects the browser to `/v1/session/oauth/authorize` with PKCE. Prelude persists the request and `302`s to your login UI.
  </Step>

  <Step title="Login & consent">
    The user signs in with any method your app supports (OTP, password, passkey, social, SAML). Your login UI then shows the consent screen with the client name and requested scopes.
  </Step>

  <Step title="Code & token">
    On approval, Prelude redirects back to the client's `redirect_uri` with a one-time `code`. The client exchanges it at `/v1/session/oauth/token` (with its PKCE `code_verifier`) for an access token and refresh token.
  </Step>
</Steps>

The issued session is a **child** of the user's browser session. Its scopes are the intersection of the requested scopes and the parent session's scopes (or the parent's full set when the client requests the `prld:oauth:inherit` scope). A session minted through the OAuth flow cannot itself start another OAuth flow — the chain is capped at one level.

## Prerequisites

Before you start, make sure you have:

* A Prelude account with access to Prelude Auth
* An **Application ID** (`appID`) — see [Applications](/auth/documentation/applications)
* Your **Management API key** for backend calls
* A verified [custom domain](/auth/documentation/domain-names), or your default `${APP_ID}.session.prelude.dev` host — the authorization-server endpoints are anchored to this **auth domain**
* A login UI built with the [Web SDK](/auth/documentation/frontend-sdks/web/introduction) that can host the sign-in and consent screens (see [Host the login and consent screens](#host-the-login-and-consent-screens))

## Enable the OAuth authorization server

Configure the OAuth server on your app with a single `PUT`. The presence of the `registration.dcr` and `registration.cimd` sub-objects is what enables each mechanism — omit one to leave it disabled.

<Steps>
  <Step title="Configure the authorization server">
    ```bash theme={null}
    curl -X PUT https://api.prelude.dev/v2/session/apps/${APP_ID}/oauth \
      -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
      -H "Content-Type: application/json" \
      -d '{
        "default_provider_url": "https://auth.example.com",
        "registration": {
          "redirect_uri_allowlist": [
            "https://claude.ai/api/mcp/auth_callback",
            "http://localhost/*",
            "http://127.0.0.1/*"
          ],
          "dcr": {
            "default_scopes": ["mcp:read"],
            "auto_registered_client_ttl_seconds": 2592000
          },
          "cimd": {
            "client_url_allowlist": ["https://claude.ai/*"],
            "default_scopes": ["mcp:read"],
            "cache_ttl_seconds": 86400
          }
        }
      }'
    ```

    | Field                                                 | Required | Description                                                                                                                                                                                                                                                                                                                     |
    | ----------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `default_provider_url`                                | Yes      | Origin of your login UI. Prelude `302`s the browser to `<default_provider_url>/sign-in` from `/authorize` and to `<default_provider_url>/oauth/consent` after login. Must be an absolute `http(s)` URL.                                                                                                                         |
    | `registration.redirect_uri_allowlist`                 | Yes      | URI patterns every client `redirect_uri` is validated against. Each entry is a literal URI or a prefix ending in a single `*` (e.g. `https://*.anthropic.com/*`). Loopback redirects (`http://localhost`, `http://127.0.0.1`) are matched with the port removed, per [RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252). |
    | `registration.dcr`                                    | No       | Enables Dynamic Client Registration. Omit to disable it (the `registration_endpoint` is then dropped from discovery and `/oauth/register` returns `403`).                                                                                                                                                                       |
    | `registration.dcr.default_scopes`                     | No       | Scopes seeded onto every DCR-registered client, on top of what it requests.                                                                                                                                                                                                                                                     |
    | `registration.dcr.auto_registered_client_ttl_seconds` | No       | Expiry for DCR-issued clients. Omit for no expiry; must be positive when set.                                                                                                                                                                                                                                                   |
    | `registration.cimd`                                   | No       | Enables Client ID Metadata Documents. Omit to disable it.                                                                                                                                                                                                                                                                       |
    | `registration.cimd.client_url_allowlist`              | No       | `https`-only patterns restricting which `client_id` URLs Prelude will fetch. Same literal-or-`*`-suffix shape as the redirect allowlist. `http` is rejected.                                                                                                                                                                    |
    | `registration.cimd.default_scopes`                    | No       | Scopes seeded onto every CIMD client. CIMD clients skip the registration step, so cap them tighter here.                                                                                                                                                                                                                        |
    | `registration.cimd.cache_ttl_seconds`                 | No       | How long a fetched metadata document is reused across flows. Omit for the resolver default; must be positive when set.                                                                                                                                                                                                          |
  </Step>
</Steps>

<Note>
  You can enable DCR, CIMD, or both. Modern MCP clients prefer CIMD (no registration round trip); DCR remains the widest-supported fallback. Enabling both lets each client use whichever it implements.
</Note>

Read the current config back with `GET /v2/session/apps/${APP_ID}/oauth`, and turn the surface off entirely with `DELETE /v2/session/apps/${APP_ID}/oauth`. See the [OAuth Server Config API](/auth/api-reference/management/config/oauth-server/update-oauth-server-config) reference for the full schema.

## Register a client manually (optional)

For a known integration you can pre-register a client instead of relying on DCR or CIMD. This is also the only way to give a single client its own login UI, distinct from the app's `default_provider_url`.

```bash theme={null}
curl -X POST https://api.prelude.dev/v2/session/apps/${APP_ID}/oauth/clients \
  -H "Authorization: Bearer ${MANAGEMENT_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Internal MCP client",
    "redirect_uris": ["https://mcp.example.com/oauth/callback"],
    "scopes": ["mcp:read", "mcp:write"],
    "provider_url": "https://auth.example.com"
  }'
```

| Field           | Required | Description                                                                                                                |
| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------- |
| `client_name`   | Yes      | Human-readable name shown on the consent screen.                                                                           |
| `redirect_uris` | Yes      | Allowed redirect URIs for this client.                                                                                     |
| `scopes`        | No       | Scopes granted to this client.                                                                                             |
| `provider_url`  | Yes      | Login UI origin for this client. Manually-registered clients do not inherit `default_provider_url` and must set their own. |

The response returns the generated `client_id` (an `oac_…` identifier). List clients with `GET …/oauth/clients` and remove one with `DELETE …/oauth/clients/{clientID}`.

## Authorization server endpoints

Once enabled, Prelude serves the OAuth surface on your **auth domain** — your verified custom domain, or `https://${APP_ID}.session.prelude.dev` by default. Each is documented under the [OAuth Server](/auth/api-reference/frontend/oauth-server-authorize) API reference:

| Endpoint                                  | Method | Purpose                                                                                                                                                                        |
| ----------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `/.well-known/oauth-authorization-server` | GET    | RFC 8414 metadata. Advertises `registration_endpoint` only when DCR is enabled and `client_id_metadata_document_supported` when CIMD is enabled.                               |
| `/v1/session/oauth/authorize`             | GET    | Starts the flow. Requires `response_type=code`, PKCE `code_challenge` with `code_challenge_method=S256`, `client_id`, `redirect_uri`, `scope`, and `state`.                    |
| `/v1/session/oauth/register`              | POST   | RFC 7591 Dynamic Client Registration. Present only when DCR is enabled.                                                                                                        |
| `/v1/session/oauth/token`                 | POST   | Exchanges an authorization `code` (with the PKCE `code_verifier`) for tokens, and handles `grant_type=refresh_token`. Public clients — `token_endpoint_auth_method` is `none`. |
| `/.well-known/jwks.json`                  | GET    | Signing keys your MCP server uses to verify the issued access tokens. See [JWKS](/auth/documentation/jwks).                                                                    |

<Note>
  PKCE with `S256` is mandatory: `/authorize` rejects a request with no `code_challenge`, and `plain` is not accepted. Authorization codes are single-use and expire after 60 seconds.
</Note>

## Host the login and consent screens

Prelude handles the OAuth protocol but hands the browser to **your** login UI (`default_provider_url`) to authenticate the user and collect consent. Build two routes with the [Web SDK](/auth/documentation/frontend-sdks/web/introduction):

<Steps>
  <Step title="Sign-in route (`/sign-in`)">
    Prelude redirects here with an `?oauth_req=<oar_…>` query parameter. Read it, sign the user in with any method your app supports, then resume the flow by calling the SDK's OAuth `continue` action with that `oauth_req`. Prelude returns the consent URL to navigate to next.
  </Step>

  <Step title="Consent route (`/oauth/consent`)">
    Prelude redirects here with the same `?oauth_req=`. Fetch the pending request to display the requesting **client name**, **redirect URI**, and **requested scopes**, then submit the user's Approve or Deny decision. Prelude returns the client `redirect_uri` (with `code` and `state` on approval, or `error=access_denied` on denial) for the browser to follow back to the client.
  </Step>
</Steps>

<Note>
  The `continue`, `pending`, and `decision` calls run against the user's live session and are bound to it with [DPoP](https://datatracker.ietf.org/doc/html/rfc9449). The Web SDK manages the session and the DPoP proof for you — you do not construct these requests by hand.
</Note>

## Point your MCP server at Prelude

On the MCP server side, advertise Prelude Auth as your authorization server so clients discover it, then verify the tokens Prelude issues:

* Serve OAuth protected-resource metadata that points at your Prelude auth domain as the `authorization_servers` entry.
* Validate incoming access tokens against the [JWKS endpoint](/auth/documentation/jwks) and check the scopes your MCP tools require.

Clients follow the discovery chain from your MCP server to Prelude's `/.well-known/oauth-authorization-server` and run the flow above with no further configuration.

## What's next?

<CardGroup cols={2}>
  <Card title="Web SDK" icon="js" href="/auth/documentation/frontend-sdks/web/introduction">
    Build the sign-in and consent screens your login UI hosts.
  </Card>

  <Card title="Verify tokens (JWKS)" icon="key" href="/auth/documentation/jwks">
    Verify the access tokens Prelude issues on your MCP server.
  </Card>

  <Card title="Custom domains" icon="globe" href="/auth/documentation/domain-names">
    Anchor the authorization-server endpoints to your own domain.
  </Card>

  <Card title="Groups & scopes" icon="layer-group" href="/auth/documentation/groups">
    Control which scopes a session — and its OAuth children — can carry.
  </Card>
</CardGroup>
