ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop on the web portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
    Develop locally with the CLI
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingDynamic MCP Server - Quickstart
Concepts
Development
Policies
    Policy Catalog
    Authentication
    Authorization
    MCP Authorization
      MCP OAuthMCP Auth0 OAuthMCP Clerk OAuthMCP Amazon Cognito OAuthMCP Microsoft Entra OAuthMCP Google OAuthMCP Keycloak OAuthMCP Logto OAuthMCP Okta OAuthMCP OneLogin OAuthMCP Ping OAuthMCP WorkOS OAuthMCP Token Exchange
    Security & Validation
    Metrics, Billing & Quotas
    Testing
    Request Modification
    Response Modification
    Upstream Authentication
    GraphQL
    Other
    Guides
Handlers
API Keys
Rate Limiting
MCP Server
MCP Gateway
AI Gateway
Developer Portal
Monetization
GraphQL
Deploying & Source Control
Analytics
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
MCP Authorization

MCP Token Exchange Policy

MCP Gateway Policy

This policy is for use with the MCP Gateway. See the MCP Gateway documentation to learn how to proxy and secure MCP servers with Zuplo.

Resolve gateway-managed upstream MCP credentials and apply them to the request.

Use this after gateway auth when the upstream requires Zuplo-managed credentials: per-user OAuth, shared OAuth, or ID-JAG (Cross-App Access) token exchange. Omit it for public upstreams or upstreams handled by ordinary Zuplo header/API-key policies. The route should use McpProxyHandler with the upstream URL configured on the handler.

Configuration

The configuration shows how to configure the policy in the 'policies.json' document.

Code
{ "name": "my-mcp-token-exchange-inbound-policy", "policyType": "mcp-token-exchange-inbound", "handler": { "export": "McpTokenExchangeInboundPolicy", "module": "$import(@zuplo/runtime)", "options": { "displayName": "Linear", "id": "linear", "idJag": { "idp": {}, "resourceAs": {}, "scopeDelimiter": " ", "scopes": [] }, "scopes": [], "summary": "Native Linear remote MCP server." } } }

Policy Configuration

  • name <string> - The name of your policy instance. This is used as a reference in your routes.
  • policyType <string> - The identifier of the policy. This is used by the Zuplo UI. Value should be mcp-token-exchange-inbound.
  • handler.export <string> - The name of the exported type. Value should be McpTokenExchangeInboundPolicy.
  • handler.module <string> - The module containing the policy. Value should be $import(@zuplo/runtime).
  • handler.options <object> - The options for this policy. See Policy Options below.

Policy Options

The options for this policy are specified below. All properties are optional unless specifically marked as required.

  • id <string> - Stable id for the upstream connection. Used to namespace per-user OAuth state and audit events. If omitted, the gateway tries to infer it from the policy name (mcp-token-exchange-{id}).
  • displayName (required) <string> - Display name shown in connect-required responses, audit logs, and the setup UI.
  • summary <string> - Optional human-readable summary of the upstream, shown on the consent page.
  • authMode (required) <string> - Authentication mode. user-oauth performs per-user OAuth federation; shared-oauth uses a gateway-wide OAuth grant; id-jag exchanges the caller's identity assertion at the configured IdP for an ID-JAG (Identity Assertion JWT Authorization Grant, also known as Cross-App Access / XAA), then redeems it at the upstream Resource Authorization Server for an upstream access token. Allowed values are user-oauth, shared-oauth, id-jag.
  • scopes <string[]> - OAuth scopes to request from the upstream (for OAuth modes).
  • protectedResourceMetadataUrl <string> - Optional override for the upstream's OAuth protected-resource metadata URL. Defaults from the route handler's rewritePattern.
  • scopeDelimiter <string> - Delimiter used to join scopes in the OAuth authorization request. Defaults to a single space.
  • prompt <undefined> - Overrides the OIDC prompt parameter on upstream authorization requests. The MCP SDK adds prompt=consent whenever the requested scope includes offline_access, which forces a consent screen on every authorization on providers like Microsoft Entra ID even after a tenant admin has granted org-wide admin consent. Leave unset to keep the SDK's behavior; set to false to strip the prompt parameter; set an explicit value to replace it. Warning: stripping prompt=consent while requesting offline_access can silently prevent refresh-token issuance on providers that require a fresh consent grant for offline access (e.g. Google) — only use this when the provider grants offline access via pre-authorized/admin consent (e.g. Entra tenant admin consent).
  • clientRegistration <undefined> - OAuth client registration mode. Defaults to auto, which uses Client ID Metadata Documents when the upstream advertises support and falls back to Dynamic Client Registration otherwise.
  • idJag <object> - ID-JAG (Cross-App Access / XAA) token-exchange configuration. Required when authMode is id-jag; not used by the OAuth modes.
    • scopes <string[]> - Scopes to request on the ID-JAG token exchange at the IdP. Defaults to [].
    • scopeDelimiter <string> - Delimiter used to join scopes in the token-exchange request. Defaults to a single space. Defaults to " ".
    • idp (required) <object> - The identity provider that exchanges the caller's identity assertion for an ID-JAG.
      • tokenUrl (required) <string> - The IdP token endpoint used for the ID-JAG token exchange.
      • clientAuth (required) <undefined> - Client authentication used at the token endpoint.
    • resourceAs (required) <object> - The upstream Resource Authorization Server that redeems the ID-JAG for an upstream access token.
      • tokenUrl (required) <string> - The Resource Authorization Server token endpoint where the gateway redeems the ID-JAG via the jwt-bearer grant.
      • audience (required) <string> - The audience value sent on the ID-JAG token exchange, identifying the Resource Authorization Server (typically its issuer).
      • resource <string> - Optional resource indicator sent on the token exchange, identifying the upstream MCP resource. Defaults to the upstream MCP server URL from the route handler's rewritePattern.
      • clientAuth (required) <undefined> - Client authentication used at the token endpoint.

Using the Policy

Overview

The mcp-token-exchange-inbound policy resolves gateway-managed upstream MCP credentials and applies them to the request before the normal Zuplo route handler forwards it.

Use this policy only when Zuplo manages upstream credentials: per-user OAuth (user-oauth), a gateway-wide shared OAuth grant (shared-oauth), or ID-JAG token exchange (id-jag). If the upstream is public, uses an API key header, or only needs static routing/context headers, omit this policy and compose the existing Zuplo header/auth policies instead.

Zuplo is only the gateway. It discovers the upstream MCP server, sends users through the upstream OAuth flow, stores the resulting upstream connection, and adds the upstream credential before forwarding tool traffic. It does not invent provider scopes, register provider apps on your behalf when the provider blocks registration, or hide provider setup failures behind a generic gateway error.

The policy does not perform the normal upstream fetch and does not pass hidden context to the handler. The route should use McpProxyHandler with a deterministic upstream MCP URL configured on the handler. McpProxyHandler handles GET stream probes and delegates POST forwarding to Zuplo's urlRewriteHandler. The policy only installs a response hook for MCP OAuth retry/connect-required cases.

Projects using this policy must run with a compatibility date that enables chained response hooks, currently 2026-03-01 or later. The retry hook must receive the latest response in the policy chain so later response hooks cannot accidentally replace an upstream OAuth retry or connect-required response.

Configuration

Code
{ "name": "mcp-token-exchange-linear", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Linear", "authMode": "user-oauth", "scopes": [], "clientRegistration": { "mode": "auto" } } } }

The upstream MCP server URL comes from the route handler's rewritePattern, the same place McpProxyHandler uses when forwarding traffic.

Scope Selection

Set scopes when the upstream provider requires specific OAuth scopes that are not discoverable from the MCP challenge or protected resource metadata. Some providers reject an otherwise valid authorization request when scope is empty or incomplete.

When scopes is omitted or empty, the gateway uses the first scope source it can discover:

  1. The upstream WWW-Authenticate challenge scope value.
  2. The upstream protected resource metadata scopes_supported value.
  3. No scope parameter if the upstream does not advertise one.

Explicit configured scopes always win: they are sent as the scope parameter on the upstream authorization request even when the upstream advertises a different scopes_supported in its protected resource metadata. Use them for providers such as Microsoft 365 where the correct resource-specific application scope is known from the provider configuration rather than from MCP discovery, or to add offline_access when the upstream requires it for refresh-token issuance but does not advertise it.

Consent Prompt Override

The MCP SDK that the gateway uses as its upstream OAuth client adds prompt=consent to the authorization request whenever the requested scope includes offline_access, because most providers require a fresh consent grant before issuing a refresh token. The gateway itself never injects prompt; the prompt option strips or replaces the SDK's parameter at the gateway's authorization seam. Some providers honor prompt=consent literally: Microsoft Entra ID forces the consent screen on every single authorization, even after a tenant admin has granted org-wide admin consent.

Set prompt to control this behavior:

  • Unset (default): keep the SDK's behavior, including the automatic prompt=consent for offline_access.
  • false: strip the prompt parameter from the authorization request. Use this for Entra upstreams where a tenant admin has already granted admin consent — users connect without seeing the consent screen, and refresh tokens are still issued because admin consent satisfies the requirement.
  • "none" / "login" / "consent" / "select_account": set or replace the prompt parameter with an explicit value.

Note that the effective scope can include offline_access even when scopes is not configured, because the gateway also reads scopes from the upstream's WWW-Authenticate challenge and protected resource metadata.

Warning: stripping prompt=consent while requesting offline_access can silently prevent refresh-token issuance on providers that require a fresh consent grant for offline access (for example, Google). Only use false when the provider grants offline access through pre-authorized or admin consent, such as Entra tenant admin consent. If connections start requiring frequent reauthorization after setting prompt: false, remove the option so the consent grant is requested again.

ID-JAG Token Exchange (Cross-App Access)

Set authMode to id-jag when the upstream is protected by an Identity Assertion JWT Authorization Grant (ID-JAG) Resource Authorization Server, the pattern also known as Cross-App Access (XAA). Instead of sending users through the upstream's browser OAuth flow, the gateway obtains the upstream credential in two server-to-server steps:

  1. IdP token exchange: the gateway exchanges the caller's identity assertion at the configured identity provider's token endpoint for an ID-JAG, sending the configured audience (the Resource Authorization Server) and resource (the upstream MCP server).
  2. Resource AS redemption: the gateway redeems the ID-JAG at the upstream Resource Authorization Server's token endpoint using the jwt-bearer grant and receives the upstream access token it attaches to the forwarded request.

Use this mode in enterprise deployments where the gateway and the upstream trust the same identity provider (for example, Okta Cross-App Access), so upstream access follows the caller's identity without a per-user upstream consent flow. The user-oauth/shared-oauth options — scopes, scopeDelimiter, prompt, and clientRegistration — do not apply; the id-jag mode is configured entirely through the idJag option.

Code
{ "name": "mcp-token-exchange-acme", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Acme", "authMode": "id-jag", "idJag": { "scopes": ["mcp:tools"], "idp": { "tokenUrl": "https://idp.example.com/oauth2/token", "clientAuth": { "method": "client_secret_post", "clientId": "gateway-client", "clientSecret": "$env(IDP_CLIENT_SECRET)" } }, "resourceAs": { "tokenUrl": "https://auth.acme-mcp.example.com/oauth2/token", "audience": "https://auth.acme-mcp.example.com", "resource": "https://mcp.acme.example.com/mcp", "clientAuth": { "method": "client_secret_post", "clientId": "gateway-resource-client", "clientSecret": "$env(RESOURCE_AS_CLIENT_SECRET)" } } } } } }

Both clientAuth blocks also accept client_secret_basic and private_key_jwt. Use private_key_jwt for providers such as Okta that require a signed client assertion for token exchange; it takes privateKeyPem, an optional keyId, and an algorithm (default RS256). Source secrets and private keys from environment variables with $env(VAR_NAME). When resource is omitted, the gateway defaults it to the upstream MCP server URL from the route handler's rewritePattern.

Route Shape

Publish both MCP transport methods as one Zuplo multi-method operation using get,post. POST is the stateless Streamable HTTP route that forwards to the upstream. GET uses the same route and returns 405 Method Not Allowed with Allow: POST from McpProxyHandler before upstream dispatch.

Code
{ "/mcp/linear": { "get,post": { "operationId": "linearMcp", "x-zuplo-route": { "policies": { "inbound": ["auth0-managed-oauth", "mcp-token-exchange-linear"] }, "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpProxyHandler", "options": { "rewritePattern": "https://mcp.linear.app/mcp" } } } } } }

Troubleshooting Upstream OAuth

Gateway authorization errors fall into three buckets:

BucketWhat it meansWhat to fix
Gateway configurationThe route or policy options are invalid before the gateway can contact the upstream.Fix policies.json or routes.oas.json. The error should name the broken entry.
Upstream OAuth setupThe upstream MCP server requires provider/admin setup that the gateway cannot complete automatically.Configure the provider app, allowlist redirect URIs, add required scopes, or contact the provider to approve the client.
Upstream service responseThe upstream server returned its own error page or OAuth error.Treat the upstream response as the source of truth and fix the upstream URL, allowlist, account region, or provider configuration.

For browser-based OAuth failures, the gateway error page shows a user-friendly message and visible developer details. Developer details include the gateway error code, request id, and the underlying reason so screenshots are useful in support tickets.

If the upstream returns an HTML error response, such as an edge firewall 403 Access Denied page, the gateway displays that upstream HTML response on the error page instead of replacing it with only a generic gateway message. This usually means the upstream URL is not publicly reachable from the gateway or the provider has not allowed this client/network/account to access the MCP endpoint.

Common examples:

  • Provider requires app approval or allowlisting: the upstream may reject DCR/CIMD or only allow registered clients. Configure a provider OAuth app or contact the provider to approve the client.
  • Provider requires explicit scopes: add the provider-required scopes to scopes. Do not rely on gateway inference when the provider does not publish the required values.
  • Wrong or private upstream URL: if a direct probe of the upstream MCP URL returns an HTML 403, 404, or branded provider error before OAuth discovery, fix the rewritePattern/metadata URL or provider access. The gateway cannot make a private or blocked upstream public.
  • No upstream auth: omit this policy for anonymous MCP servers. A public upstream should route through McpProxyHandler without token exchange.

Read more about how policies work

Edit this page
Last modified on July 25, 2026
MCP WorkOS OAuthRate Limiting
On this page
  • Configuration
    • Policy Configuration
    • Policy Options
  • Using the Policy
  • Overview
  • Configuration
  • Scope Selection
  • Consent Prompt Override
  • ID-JAG Token Exchange (Cross-App Access)
  • Route Shape
  • Troubleshooting Upstream OAuth
JSON
JSON
JSON
JSON