MCP Token Exchange Inbound Policy
Resolve gateway-managed upstream MCP credentials and apply them to the request.
Use this after gateway auth when the upstream requires Zuplo-managed OAuth. 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.
config/policies.json
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 bemcp-token-exchange-inbound.handler.export<string>- The name of the exported type. Value should beMcpTokenExchangeInboundPolicy.handler.module<string>- The module containing the policy. Value should be$import(@zuplo/runtime/mcp-gateway).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.protectedResourceMetadataUrl<string>- Optional override for the upstream's OAuth protected-resource metadata URL. Defaults from the route handler's rewritePattern.authMode(required)<string>- Authentication mode.user-oauthperforms per-user OAuth federation;shared-oauthuses a gateway-wide OAuth grant. Allowed values areuser-oauth,shared-oauth.scopes<string[]>- OAuth scopes to request from the upstream (for OAuth modes).scopeDelimiter<string>- Delimiter used to join scopes in the OAuth authorization request. Defaults to a single space.clientId<string>- OAuth client id when registering manually (for OAuth modes).clientSecret<string>- OAuth client secret (for OAuth modes with manual registration). Use$env(VAR_NAME)to source from an environment variable.tokenEndpointAuthMethod<string>- Token endpoint authentication method (for OAuth modes with manual registration). Allowed values areclient_secret_basic,client_secret_post,none.clientRegistration<undefined>- OAuth client registration mode. Defaults toauto(Dynamic Client Registration).
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 OAuth credentials, such as per-user OAuth or shared OAuth. 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
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:
- The upstream
WWW-Authenticatechallengescopevalue. - The upstream protected resource metadata
scopes_supportedvalue. - No
scopeparameter if the upstream does not advertise one.
Explicit configured scopes always win. 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.
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
Troubleshooting Upstream OAuth
Gateway authorization errors fall into three buckets:
| Bucket | What it means | What to fix |
|---|---|---|
| Gateway configuration | The 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 setup | The 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 response | The 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 therewritePattern/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
McpProxyHandlerwithout token exchange.
Read more about how policies work