ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Getting Started
Concepts
API Management
AI Gateway
MCP Gateway
    Overview
    Getting Started
      How it works
      Connect MCP clients
      Authentication
        OverviewUpstream OAuthConnect an upstream OAuth providerConnect an API-key upstream
        Identity providers
        Manual OAuth testing
      Cross App Access
      Configuration
      Policies
      Observability
      ReferenceServer RegistryTroubleshooting
    MCP Server
    Developer Portal
    Development
    Deploying & Source Control
    Analytics
    Observability
    Networking & Infrastructure
    Account Management
    Programming API
    Build with AI
    Zuplo CLI
    Migration Guides
    Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
    powered by Zuplo
    Authentication

    Connect a gateway to an upstream OAuth provider

    When an upstream MCP server requires OAuth — either per user or as a shared service account — attach the mcp-token-exchange-inbound policy to the route. The policy resolves the user's upstream credential and applies it to the upstream request, returns a connect-required error when the user hasn't yet authorized the upstream, and refreshes the credential transparently.

    For the conceptual model behind the policy — the two auth modes, client registration, the consent flow, and connect-required states — see Per-user OAuth to upstream MCP servers.

    Add the token-exchange policy

    1. Declare one mcp-token-exchange-inbound policy per upstream MCP server in config/policies.json:

      Code
      { "name": "mcp-token-exchange-linear", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Linear", "protectedResourceMetadataUrl": "https://mcp.linear.app/.well-known/oauth-protected-resource", "authMode": "user-oauth", "scopes": [], "clientRegistration": { "mode": "auto" }, }, }, }
    2. Attach the policy to the route in config/routes.oas.json, after the inbound MCP OAuth policy:

      Code
      "policies": { "inbound": ["auth0-managed-oauth", "mcp-token-exchange-linear"] }

    Only one MCP token-exchange policy is allowed per route. The route's upstream URL comes from McpProxyHandler's rewritePattern option, not from the policy.

    Compatibility date 2026-03-01

    MCP Gateway features require compatibilityDate >= 2026-03-01 in zuplo.jsonc. See Compatibility dates.

    Pick an auth mode

    Set authMode based on who owns the upstream credential:

    • "user-oauth" — each user has their own per-upstream OAuth connection. This is the default and the right choice for Linear, Notion, Stripe, GitHub, and most SaaS MCP servers.
    • "shared-oauth" — one gateway-wide OAuth grant used by every user. An administrator completes a one-time connection; subsequent user requests reuse the shared credential. Pick shared mode when the upstream uses a service account that represents the organization rather than individual users.

    Pick a client registration mode

    Set clientRegistration based on how the gateway should identify itself to the upstream OAuth provider:

    • { "mode": "auto" } (default) — the gateway publishes a per-upstream OAuth Client ID Metadata Document and tells the upstream that URL is the client ID. If the upstream doesn't accept CIMD, the gateway falls back to RFC 7591 Dynamic Client Registration. No upstream client credentials live in source control.
    • { "mode": "manual", "clientId": "...", "clientSecret": "...", "tokenEndpointAuthMethod": "client_secret_basic" } — pre-registered OAuth app. The gateway uses the clientId directly and authenticates to the upstream token endpoint with the configured method. Pick manual mode when your organization manages OAuth client lifecycle centrally, when the upstream requires an approved client, or when you need to share one OAuth client across multiple routes.

    Use $env(...) for clientSecret so the secret stays out of source control.

    When you register the OAuth app with the upstream provider, register this redirect URI, where <connection-id> is the policy's id (for the example policy above, linear, inferred from the mcp-token-exchange-linear name):

    Code
    https://<gateway-host>/__zuplo/auth/connections/<connection-id>/callback

    This is different from /__zuplo/oauth/callback, which is the callback for the identity provider that signs users in to the gateway itself. Registering that path with the upstream provider fails with redirect_uri_mismatch. Both paths are listed in the MCP Gateway reference.

    Set scopes when the upstream needs them

    When the upstream requires specific scopes that aren't discoverable from MCP metadata, set scopes explicitly:

    Code
    { "options": { "scopes": ["mcp"], }, }

    When scopes is omitted or empty, the gateway falls back through the upstream's most recent WWW-Authenticate challenge, then the scopes_supported array in Protected Resource Metadata, then no scope parameter at all. Microsoft 365, Slack, PostHog, Stripe, Grafana Cloud, and several other providers fall into the bucket where explicit scopes are required.

    Override the Protected Resource Metadata URL

    By default, the gateway derives the upstream PRM URL from the route's rewritePattern:

    Code
    rewritePattern: https://mcp.linear.app/mcp default PRM URL: https://mcp.linear.app/.well-known/oauth-protected-resource/mcp

    When the upstream serves PRM at a non-default path, override it with protectedResourceMetadataUrl. Linear, for example, serves PRM at the origin's root, not under /mcp:

    Code
    { "options": { "displayName": "Linear", "protectedResourceMetadataUrl": "https://mcp.linear.app/.well-known/oauth-protected-resource", "authMode": "user-oauth", "clientRegistration": { "mode": "auto" }, }, }

    When in doubt, look at what the upstream's MCP endpoint returns in its WWW-Authenticate header on an unauthenticated request — the resource_metadata= parameter on that header is the canonical URL.

    Test the connect flow

    After deploying (or restarting zuplo dev):

    1. Connect a test client (the MCP Inspector is the fastest option) to the route as a fresh user.
    2. The first MCP request returns a JSON-RPC connect-required error with an authUrl. Modern MCP clients open the URL automatically; older clients surface it for the user to copy.
    3. Complete the upstream provider's OAuth flow in the browser. The gateway stores the resulting tokens encrypted, keyed by the user's subject ID.
    4. The next MCP request succeeds. Subsequent requests reuse the stored credential transparently.

    For deeper debugging — including a manual curl walkthrough of the OAuth flow — see Manual OAuth testing.

    Worked examples

    Linear (auto registration, PRM override)

    Code
    { "name": "mcp-token-exchange-linear", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Linear", "summary": "Linear MCP upstream, per-user OAuth.", "protectedResourceMetadataUrl": "https://mcp.linear.app/.well-known/oauth-protected-resource", "authMode": "user-oauth", "scopes": [], "clientRegistration": { "mode": "auto" }, }, }, }

    The corresponding route:

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

    Stripe (explicit scope)

    Code
    { "name": "mcp-token-exchange-stripe", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Stripe", "summary": "Stripe MCP upstream, per-user OAuth.", "authMode": "user-oauth", "scopes": ["mcp"], "clientRegistration": { "mode": "auto" }, }, }, }

    Stripe requires the bare mcp scope explicitly. Point the route's rewritePattern at https://mcp.stripe.com — Stripe serves its MCP endpoint at the origin root, not under /mcp. Stripe publishes PRM at the root well-known path, so the default derived PRM URL is correct and no override is needed.

    Notion (PRM override at /mcp path)

    Code
    { "name": "mcp-token-exchange-notion", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Notion", "protectedResourceMetadataUrl": "https://mcp.notion.com/.well-known/oauth-protected-resource/mcp", "authMode": "user-oauth", "scopes": [], "clientRegistration": { "mode": "auto" }, }, }, }

    Non-OAuth upstreams

    mcp-token-exchange-inbound only handles OAuth. For other credential shapes, omit this policy and compose ordinary Zuplo policies alongside McpProxyHandler:

    • API key in a custom header: use set-upstream-api-key-inbound.
    • Static request headers: use SetHeadersInboundPolicy.
    • Anonymous upstream: no upstream credential policy is needed — McpProxyHandler proxies through directly.

    Related

    • Per-user OAuth to upstream MCP servers — the conceptual model behind the policy.
    • McpProxyHandler reference — the route handler the token-exchange policy attaches credentials for.
    • Add multiple upstream MCP servers — apply the same pattern across several upstreams in one project.
    • Manual OAuth testing — drive the upstream OAuth surface with curl for low-level verification.
    Edit this page
    Last modified on September 20, 2026
    Upstream OAuthConnect an API-key upstream
    On this page
    • Add the token-exchange policy
    • Pick an auth mode
    • Pick a client registration mode
    • Set scopes when the upstream needs them
    • Override the Protected Resource Metadata URL
    • Test the connect flow
    • Worked examples
      • Linear (auto registration, PRM override)
      • Stripe (explicit scope)
      • Notion (PRM override at /mcp path)
    • Non-OAuth upstreams
    • Related
    JSON
    JSON
    JSON
    JSON
    JSON
    JSON
    JSON
    JSON