ZuploZuplo
LoginStart for Free
  • 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
    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
MCP Gateway

How the MCP Gateway works

The MCP Gateway is a set of policies and a route handler that run inside any Zuplo project. A single deployment hosts any number of public MCP routes, 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 route 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.

Zuplo MCP Gateway
OAuth endpoints
MCP route
MCP Client
Identity Provider
Upstream MCP Server
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.

The flow in detail, for the first call from a new client to an OAuth-protected upstream:

  1. The client POSTs to the MCP route with no token.
  2. The gateway returns 401 with WWW-Authenticate: Bearer resource_metadata=....
  3. The client fetches the Protected Resource Metadata document and discovers the gateway's authorization server.
  4. The client fetches the AS metadata, registers via DCR (or uses a CIMD client ID), and starts the authorization flow with PKCE and a resource parameter.
  5. The gateway redirects the user's browser to the configured identity provider for login.
  6. After login, the gateway renders a consent page that lists every upstream the route requires.
  7. The user completes upstream OAuth for each required upstream — the gateway stores per-user tokens encrypted at rest.
  8. The user approves consent. The gateway redirects the client back with an authorization code.
  9. The client exchanges the code at /oauth/token and receives an access token scoped to mcp:tools.
  10. The client POSTs to the MCP route with the bearer token. The gateway validates the token, attaches the user's upstream credential, and proxies to the upstream MCP server.

Once tokens are issued and the upstream connection exists, only step 10 runs on subsequent calls.

Three details that come up during debugging:

  • The resource parameter (RFC 8707) is required on /oauth/authorize and /oauth/token. The gateway rejects tokens whose audience doesn't match the route they're being used against.
  • The consent screen lists the upstream the route depends on with a Connect button. The user can't approve the grant until the upstream is connected.
  • The upstream OAuth flow runs once per (user, upstream) pair. Subsequent requests reuse the stored credential. If an upstream returns a 401 mid-call, the gateway refreshes and retries once before propagating the error.

Two OAuth surfaces

The gateway plays two OAuth roles simultaneously. The inbound role — as an OAuth server for MCP clients — and the outbound role — as an OAuth client to each upstream — use different policies and different credential stores.

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 resource parameter is required on every authorization and token request.
  • RFC 6750 Bearer tokens — the gateway issues opaque tokens carried in Authorization: Bearer headers.

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 MCP route. The route binding is a deliberate blast-radius constraint: a token issued for /mcp/linear-v1 can't be replayed against /mcp/stripe-v1, so a stolen token from one upstream stays confined to that upstream.

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 the client_id. If the upstream doesn't support CIMD, the gateway falls back to RFC 7591 Dynamic Client Registration.
  • clientRegistration: { mode: "manual" } — supply a pre-registered clientId and clientSecret (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 MCP route uses the Streamable HTTP transport defined in the MCP spec. The gateway accepts POST requests only:

  • POST on the route path carries the JSON-RPC payload.
  • GET on the same path returns 405 Method Not Allowed with Allow: POST. The gateway doesn't open SSE streams for server-initiated messages.

Route paths are whatever you set in routes.oas.json — /mcp/<provider>-v<n> is the recommended convention (and what the dogfood gateway uses), but any path the OpenAPI router accepts works.

The gateway is stateless. It does not maintain MCP sessions, doesn't track subscriptions, and doesn't emit server-initiated notifications. Statelessness is what lets the gateway run on Zuplo's edge runtime and scale horizontally without session affinity — any node can serve any request. Stateful MCP features (long-running subscriptions, server-initiated sampling) aren't supported through the gateway today.

Configuration model

The MCP Gateway is configured the same way as the rest of a Zuplo project: an OpenAPI route file, a policy library, and a runtime plugin registration. Every project that uses the gateway has the same shape:

PieceLives inPurpose
compatibilityDate >= 2026-03-01zuplo.jsoncUnlocks MCP Gateway features. Required.
McpGatewayPluginmodules/zuplo.runtime.tsRegisters the OAuth metadata, authorization endpoints, consent page, and upstream connect callbacks.
One MCP OAuth policyconfig/policies.jsonAuthenticates inbound MCP requests against your identity provider. One per project — pick the wrapper for your IdP (Auth0, Cognito, Clerk, Entra, Google, Keycloak, Logto, Okta, OneLogin, Ping, WorkOS) or mcp-oauth-inbound for any other OIDC provider.
One mcp-token-exchange-inbound policy per upstreamconfig/policies.jsonResolves the user's upstream credential and attaches it as the upstream Authorization header. Omit for non-OAuth upstreams.
Optional mcp-capability-filter-inbound policyconfig/policies.jsonCurates the tools, prompts, resources, and resource templates the route exposes.
One route per upstreamconfig/routes.oas.jsonUses McpProxyHandler with the upstream URL as rewritePattern. Attaches the OAuth policy + token exchange policy.

A minimal route looks like this:

config/routes.oas.json
"/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"] } } } }

The plugin registration:

modules/zuplo.runtime.ts
import { RuntimeExtensions } from "@zuplo/runtime"; import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addPlugin(new McpGatewayPlugin()); }

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 an MCP route, the policies run in this order:

  1. MCP OAuth policy — one of the IdP-specific wrappers (mcp-auth0-oauth-inbound, mcp-okta-oauth-inbound, mcp-entra-oauth-inbound, and so on) or the generic mcp-oauth-inbound. Validates the gateway-issued bearer token, asserts audience binding and scope.
  2. 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.
  3. Capability filter policy (mcp-capability-filter-inbound, optional) — filters the upstream's tools/list, prompts/list, resources/list, and resources/templates/list responses, and blocks calls to hidden capabilities with MethodNotFound.

The handler — McpProxyHandler — runs after the policies, forwards the request to the upstream URL, and emits capability analytics events.

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/list caching. 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-inbound policy to those routes if needed.

Related

  • Set up an MCP Gateway — the how-to that puts this conceptual model into practice.
  • Quickstart — add the MCP Gateway plugin to a Zuplo project and front your first upstream.
  • Authentication overview — the two OAuth layers expanded with the full standards table.
  • Reference — the full URL catalog, default TTLs, compatibility date, and OAuth metadata extensions.
  • Troubleshooting — the gotchas that catch most people the first time.
Edit this page
Last modified on May 27, 2026
Quickstart (Local Dev)Overview
On this page
  • Request lifecycle
  • Two OAuth surfaces
    • Downstream — gateway as OAuth 2.1 server
    • Upstream — gateway as OAuth client
  • Transport — Streamable HTTP, POST only
  • Configuration model
    • Inbound policy chain
  • What the gateway does not do
  • Related
JSON
TypeScript