ZuploZuplo
LoginSign Up
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
    IntroductionBetaQuickstartQuickstart (Local Dev)How it works
    Connect MCP clients
    Authentication
      OverviewUpstream OAuthConnect an upstream OAuth provider
      Identity providers
      Manual OAuth testing
    Configuration
    Observability
    ReferenceTroubleshooting
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
Authentication

Per-user OAuth to upstream MCP servers

The gateway sits between an MCP client and the upstream MCP servers a team relies on. The inbound OAuth surface is the one MCP clients connect to; the outbound surface is where the gateway authenticates to each upstream on the user's behalf. This page is about the outbound surface — the one the mcp-token-exchange-inbound policy controls.

For the policy steps, options reference, and worked examples, see Connect a gateway to an upstream OAuth provider.

Why the gateway acts as an OAuth client

Modern MCP servers — Linear, Notion, Stripe, GitHub, Grafana Cloud, and many others — are OAuth-protected resources. They expect a Bearer token that represents a specific user (or service identity) granted by their own OAuth authorization server.

When an MCP client connects to a Zuplo MCP Gateway route, it presents the gateway's bearer token. That token authenticates the user to the gateway but isn't valid against the upstream. The spec explicitly forbids forwarding the inbound token to an upstream, so the gateway must mint an independent upstream credential and attach it to the upstream request.

The gateway does that by acting as a standard OAuth client to each upstream — discovering the upstream's authorization server, registering itself as a client, redirecting the user through the upstream's authorization flow, capturing the resulting tokens, and storing them encrypted at rest. On subsequent requests, the gateway resolves the stored credential, refreshes it if necessary, and applies it to the upstream request.

The two auth modes

authMode is the central knob. It decides who owns the upstream credential.

user-oauth

Per-user is the default and the right choice for most upstreams. Each user has their own per-upstream OAuth connection. The first time a user hits the route, the gateway returns a connect-required error; the user completes the upstream provider's OAuth flow in a browser; the gateway stores the resulting tokens encrypted, keyed by the user's subject ID. Subsequent requests from that user are transparent.

This mode is what Linear, Notion, Stripe, GitHub, and most SaaS MCP servers use. It preserves per-user attribution end to end — the upstream sees the specific user making the call, and the gateway's analytics record the same subject ID against every event.

shared-oauth

Shared mode uses a single gateway-wide OAuth grant. There's no per-user connect flow. An administrator completes a one-time connection through the upstream's OAuth provider, and every authenticated user reuses that credential when calling the upstream. If no shared connection exists yet, the gateway returns an admin_connect_required error to let the client know an administrator action is needed.

Shared mode is appropriate when the upstream uses a service account that represents the organization rather than individual users, or when auditing happens at the gateway level (per user) rather than at the upstream (where every call looks like the same service account).

Client registration

The gateway needs to identify itself to the upstream OAuth provider before it can request tokens. The clientRegistration option controls how:

  • CIMD with DCR fallback ({ "mode": "auto" }) — the default. The gateway publishes a per-upstream OAuth Client ID Metadata Document at /.well-known/oauth-client/{connection}?authProfileId=... 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. Auto mode requires nothing from the upstream provider beyond standard MCP authorization spec support and has no client secrets to rotate.
  • Manual — the gateway uses a pre-registered clientId (and optional clientSecret) and authenticates to the upstream token endpoint with a configured method. Manual mode is the right choice when an organization manages OAuth client lifecycle centrally, the upstream provider requires an approved client, or one OAuth client should be shared across multiple routes.

Both modes are first-class. CIMD documents are accessible to the upstream provider over HTTPS — the upstream fetches them as part of its OAuth registration flow. The CIMD URL includes the authProfileId query parameter so the gateway can scope client identity per (upstream, authMode) pair.

How the gateway picks scopes

The gateway needs to know which OAuth scopes to request from the upstream. It considers three sources in order:

  1. An explicit scopes array on the policy. When set, the gateway uses exactly those values on every upstream authorization request.
  2. The scope= value from the upstream's most recent WWW-Authenticate challenge. Used when no explicit scopes are configured.
  3. The scopes_supported array in the upstream's Protected Resource Metadata. Used as the final fallback before falling through to no scope parameter at all.

Explicit scopes always win. Microsoft 365, Slack, PostHog, Stripe, and Grafana Cloud are examples of upstreams that need explicit scopes — their PRM either lists too many scopes or none at all, so deferring to discovery alone isn't enough.

What the user sees

The browser flow runs the first time a user hits an OAuth-protected upstream they haven't connected, and again whenever the upstream revokes the gateway's client. Modern MCP clients implement the URL-elicitation extension and open the URL automatically. Older clients surface the URL as part of the JSON-RPC error message; the user copies it into a browser.

Zuplo Gateway
Upstream connect
/mcp/linear-v1
MCP Client
Linear OAuth
Linear MCP
Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

Each MCP route proxies to exactly one upstream, so the consent page typically shows one upstream to connect. The consent page is part of the gateway and renders automatically whenever a user lands at /oauth/setup mid-flow.

Connect-required states

When the gateway needs the user to act, it returns a JSON-RPC error with a state field that distinguishes the three reasons.

StateMeaningTypical UI message
authenticatingFirst-time connection. User hasn't authorized the upstream yet."Connect to {provider} to continue."
reconsent_requiredExisting connection but the upstream revoked the client or invalidated the refresh token. The user needs to reauthorize."{provider} authorization must be renewed."
admin_connect_requiredauthMode: shared-oauth and no shared connection exists yet. Only an administrator can complete the flow."An administrator must connect {provider} before this service is available."

The full JSON-RPC error payload looks like:

Code
{ "jsonrpc": "2.0", "id": "1", "error": { "code": -32042, "message": "Connect Linear to continue.", "data": { "state": "authenticating", "upstreamServerId": "linear", "operationId": "linear-mcp-server", "authUrl": "https://gateway.example.com/auth/connections/linear/connect?browserTicket=eyJ...&operationId=linear-mcp-server", "nextAction": "redirect", "authProfileId": "linear:user-oauth", }, }, }

The -32042 error code is MCP's URLElicitationRequiredError. Clients that support URL elicitation open authUrl directly; others render the message and let the user open the URL manually.

Refresh and 401 retry

The gateway transparently refreshes the upstream access token from the stored refresh token. When the upstream returns a 401 mid-request — for example, because the upstream's session-bound token expired — the gateway refreshes the upstream credential and retries the upstream fetch once. If the refresh fails or produces another connect-required state, the gateway returns the JSON-RPC connect-required to the client and the user sees the reconsent flow.

Stored refresh tokens stay valid as long as the upstream provider honors them. When an upstream's policy revokes a refresh token — for example, because the user revoked the connection from the upstream's dashboard — the next request surfaces reconsent_required and the user re-authorizes through the same browser flow.

Where the metadata URL comes from

By default, the gateway derives the upstream Protected Resource Metadata 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 (Linear's PRM lives at the origin's root, not under /mcp), the policy's protectedResourceMetadataUrl option overrides the default. The canonical source of truth is the resource_metadata= parameter on the upstream's WWW-Authenticate challenge to an unauthenticated request.

Related

  • Connect a gateway to an upstream OAuth provider — how to attach the policy, pick modes, and verify the connect flow.
  • Authentication overview — the two-layer model and how inbound and outbound OAuth fit together.
  • Manual OAuth testing — verify the gateway's OAuth surface end to end with curl and openssl.
  • Compatibility dates — the 2026-03-01 requirement for upstream 401 retries and other MCP Gateway behaviors.
Edit this page
Last modified on May 27, 2026
OverviewConnect an upstream OAuth provider
On this page
  • Why the gateway acts as an OAuth client
  • The two auth modes
    • user-oauth
    • shared-oauth
  • Client registration
  • How the gateway picks scopes
  • What the user sees
  • Connect-required states
  • Refresh and 401 retry
  • Where the metadata URL comes from
  • Related
JSON