How the MCP Gateway works
The MCP Gateway is a Zuplo feature. A single deployment hosts any number of public MCP routes (one per Virtual MCP), each pointing at a different upstream MCP server. The gateway runs its own OAuth 2.1 authorization server for inbound clients and acts as an OAuth client to each upstream provider.
Request lifecycle
The diagram below shows a first-time call from an MCP client to a Virtual MCP that wires a single OAuth-protected upstream. Once tokens are issued and the upstream connection exists, the gateway skips the OAuth dance and goes straight from the bearer-token check to the upstream proxy.
Code
Notes on the flow:
- The 401 response always includes
WWW-Authenticate: Bearer resource_metadata=...so spec-compliant clients can discover the Protected Resource Metadata document without a fallback probe. - The
resourceparameter (RFC 8707) is mandatory on/oauth/authorizeand/oauth/token. The gateway rejects tokens whose audience doesn't match the canonical URI of the Virtual MCP they're being used against. - The consent screen is server-rendered HTML, served at
/oauth/setup. It lists every upstream the requested Virtual MCP depends on with per- upstream Connect buttons. The user can't approve the gateway grant until every required upstream has been connected. - The upstream OAuth flow runs once per (user, upstream) pair. Subsequent requests reuse the stored encrypted tokens. If an upstream returns a 401 mid-call, the gateway refreshes the upstream token and retries once before propagating the error.
Two OAuth surfaces
The gateway plays two OAuth roles simultaneously, and it's important to keep them straight.
Downstream — gateway as OAuth 2.1 server
The gateway implements the MCP authorization spec from the perspective of a Resource Server and an Authorization Server. MCP clients talk OAuth to the gateway, not to the upstream providers. Standards observed:
- RFC 8414 Authorization Server Metadata and OpenID Connect Discovery 1.0 for AS discovery.
- RFC 9728 Protected Resource Metadata for advertising the AS.
- RFC 7591 Dynamic Client Registration and OAuth Client ID Metadata Documents (CIMD) for client registration. CIMD is the recommended path; DCR is supported for clients that don't speak it.
- RFC 7636 PKCE with S256 required.
- RFC 8707 Resource Indicators — the
resourceparameter is required on every authorization and token request. - RFC 6750 Bearer tokens — the gateway issues opaque tokens carried in
Authorization: Bearerheaders.
The gateway delegates user authentication to a configured OIDC identity provider
(Auth0 through McpAuth0OAuthInboundPolicy or generic OIDC through
McpOAuthInboundPolicy). The provider's tokens never leave the gateway — the
gateway issues its own opaque access tokens, scoped to mcp:tools, and binds
each to one specific Virtual MCP.
Token passthrough is explicitly forbidden by the spec, and the gateway enforces it: inbound auth headers don't leak to the upstream.
Upstream — gateway as OAuth client
For each upstream MCP server that requires OAuth, the gateway acts as a standard OAuth client.
- Per-user OAuth (
authMode: "user-oauth") — every end user goes through a one-time consent. The gateway stores their access and refresh tokens encrypted at rest, keyed by user. Token refresh is automatic. - Shared OAuth (
authMode: "shared-oauth") — one upstream connection shared across every user of the gateway. The connection is established by an administrator through a special connect flow.
Client registration with the upstream supports two modes:
clientRegistration: { mode: "auto" }(the default) — the gateway publishes a per-upstream OAuth Client ID Metadata Document at/.well-known/oauth-client/<connection>and tells the upstream that URL is theclient_id. If the upstream doesn't support CIMD, the gateway falls back to RFC 7591 Dynamic Client Registration.clientRegistration: { mode: "manual" }— supply a pre-registeredclientIdandclientSecret(and optional auth method).
When the gateway needs an upstream connection it doesn't have yet, the gateway returns a JSON-RPC error with a URL to open in a browser. Modern MCP clients pop the browser automatically; older ones surface the URL for the user to open manually.
Transport — Streamable HTTP, POST only
Every Virtual MCP route uses the Streamable HTTP transport defined in the MCP spec. The gateway accepts POST requests only:
POST /v1/mcp/<slug>carries the JSON-RPC payload.GET /v1/mcp/<slug>returns405 Method Not AllowedwithAllow: POST. The gateway doesn't open SSE streams for server-initiated messages.
The gateway is stateless. It does not maintain MCP sessions, doesn't track subscriptions, and doesn't emit server-initiated notifications. Stateful MCP features (long-running subscriptions, server-initiated sampling) aren't supported through the gateway today.
Configuration model
Each Virtual MCP is a route in routes.oas.json that uses McpProxyHandler as
its handler. The route declares one inbound OAuth policy and (when the upstream
needs auth) one token-exchange policy. A single project shares one OAuth policy
across all of its MCP routes.
A minimal route looks like this:
Code
The OAuth and token-exchange policies live in config/policies.json. The plugin
that registers the OAuth and upstream-callback endpoints lives in
modules/zuplo.runtime.ts:
Code
The operationId on each MCP route is more than a label — it identifies the MCP
route and is the virtualServerName in analytics. Changing it strands all
stored tokens and per-user upstream connections.
MCP Gateway features require compatibilityDate >= 2026-03-01 in zuplo.jsonc.
See Compatibility dates.
Inbound policy chain
For each request to a Virtual MCP, the policies run in this order:
- MCP OAuth policy (
mcp-auth0-oauth-inboundormcp-oauth-inbound) — validates the gateway-issued bearer token, asserts audience binding and scope. - MCP token-exchange policy (
mcp-token-exchange-inbound) — resolves the right upstream credential for the authenticated user. If the user hasn't connected this upstream yet, the policy returns a connect-required error. - Capability filter policy (
mcp-capability-filter-inbound, optional) — filters the upstream'stools/list,prompts/list,resources/list, andresources/templates/listresponses, and blocks calls to hidden capabilities withMethodNotFound.
The handler — McpProxyHandler — runs after the policies, forwards the request
to the upstream URL, and emits capability analytics events.
Where the Portal vs. code config each piece
The Portal and code config edit the same underlying configuration. Pick whichever fits how your team wants to manage gateway configuration.
| Surface | Portal | Code config |
|---|---|---|
| Create the gateway project | Yes | No |
| Add an Origin MCP (upstream URL) | Yes | Yes (rewritePattern) |
| Custom headers / API-key auth to upstream | — | Yes (compose SetHeadersInboundPolicy, etc.) |
| Create a Virtual MCP (slug + URL) | Yes | Yes (route definition) |
| Curate which tools the Virtual MCP exposes | Yes (tool picker) | Yes (mcp-capability-filter-inbound) |
| Override tool descriptions / annotations | — | Yes (capability filter projection) |
| Configure the downstream OAuth IdP | Yes (Auth0 onboarding) | Yes (McpAuth0OAuthInboundPolicy / McpOAuthInboundPolicy) |
| Configure per-upstream OAuth | — | Yes (McpTokenExchangeInboundPolicy) |
| Teams and Virtual MCP assignment | Yes | — |
| Analytics dashboard | Yes | Read-only |
Most teams pick one path per project to keep change tracking consistent, but mixing is supported — Portal-managed routes appear alongside hand-written ones.
What the gateway does not do
A few capabilities are intentionally out of scope, at least today:
- No stateful sessions. The gateway doesn't open SSE streams, doesn't track
MCP-Session-Id, and doesn't proxy server-initiated requests. - No
tools/listcaching. Every request goes upstream. If an upstream is slow to list capabilities, callers feel it. - No prompt-injection or PII scanning at the policy level. These belong in a separate inbound policy and can be composed alongside the MCP policies through Zuplo's standard policy model.
- No rate limiting on OAuth endpoints out of the box. Add Zuplo's built-in
rate-limit-inboundpolicy to those routes if needed.
Next steps
- Quickstart — set everything above up in the Portal in ten minutes.
- Reference — the full URL catalog, default TTLs, compatibility date, and OAuth metadata extensions.
- Troubleshooting — the gotchas that catch most people the first time.