# 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.

```json title="config/policies.json"
{
  "name": "my-mcp-token-exchange-inbound-policy",
  "policyType": "mcp-token-exchange-inbound",
  "handler": {
    "export": "McpTokenExchangeInboundPolicy",
    "module": "$import(@zuplo/runtime/mcp-gateway)",
    "options": {
      "displayName": "Linear",
      "authMode": "user-oauth",
      "scopes": [],
      "clientRegistration": {
        "mode": "auto"
      }
    }
  }
}
```

### Policy Configuration

- `name` <code className="text-green-600">&lt;string&gt;</code> - The name of your policy instance. This is used as a reference in your routes.
- `policyType` <code className="text-green-600">&lt;string&gt;</code> - The identifier of the policy. This is used by the Zuplo UI. Value should be `mcp-token-exchange-inbound`.
- `handler.export` <code className="text-green-600">&lt;string&gt;</code> - The name of the exported type. Value should be `McpTokenExchangeInboundPolicy`.
- `handler.module` <code className="text-green-600">&lt;string&gt;</code> - The module containing the policy. Value should be `$import(@zuplo/runtime/mcp-gateway)`.
- `handler.options` <code className="text-green-600">&lt;object&gt;</code> - The options for this policy. [See Policy Options](#policy-options) below.

### Policy Options

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

- `id` <code className="text-green-600">&lt;string&gt;</code> - 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)** <code className="text-green-600">&lt;string&gt;</code> - Display name shown in connect-required responses, audit logs, and the setup UI.
- `summary` <code className="text-green-600">&lt;string&gt;</code> - Optional human-readable summary of the upstream, shown on the consent page.
- `protectedResourceMetadataUrl` <code className="text-green-600">&lt;string&gt;</code> - Optional override for the upstream's OAuth protected-resource metadata URL. Defaults from the route handler's rewritePattern.
- `authMode` **(required)** <code className="text-green-600">&lt;string&gt;</code> - Authentication mode. `user-oauth` performs per-user OAuth federation; `shared-oauth` uses a gateway-wide OAuth grant. Allowed values are `user-oauth`, `shared-oauth`.
- `scopes` <code className="text-green-600">&lt;string[]&gt;</code> - OAuth scopes to request from the upstream (for OAuth modes).
- `scopeDelimiter` <code className="text-green-600">&lt;string&gt;</code> - Delimiter used to join scopes in the OAuth authorization request. Defaults to a single space.
- `clientId` <code className="text-green-600">&lt;string&gt;</code> - OAuth client id when registering manually (for OAuth modes).
- `clientSecret` <code className="text-green-600">&lt;string&gt;</code> - OAuth client secret (for OAuth modes with manual registration). Use `$env(VAR_NAME)` to source from an environment variable.
- `tokenEndpointAuthMethod` <code className="text-green-600">&lt;string&gt;</code> - Token endpoint authentication method (for OAuth modes with manual registration). Allowed values are `client_secret_basic`, `client_secret_post`, `none`.
- `clientRegistration` <code className="text-green-600">&lt;undefined&gt;</code> - OAuth client registration mode. Defaults to `auto` (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

```json
{
  "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. 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.

```json
{
  "/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:

| 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 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](/articles/policies)
