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
    Configuration
      Set up the gatewayMulti-upstreamLocal developmentCapability filteringCurate toolsCurate tools (in code)McpProxyHandlerCompatibility dates
    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
Configuration

Set up an MCP Gateway

To turn any Zuplo project into an MCP Gateway, configure four things in source control: the runtime plugin in modules/zuplo.runtime.ts, one MCP OAuth policy in config/policies.json, one mcp-token-exchange-inbound policy per OAuth-protected upstream, and one route per upstream in config/routes.oas.json. This guide walks through each piece for a single-upstream gateway.

For the conceptual model — what each piece does and why the pieces are split the way they are — see How the MCP Gateway works.

1. Register the MCP Gateway plugin

Add a modules/zuplo.runtime.ts file that registers McpGatewayPlugin:

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 plugin registers the OAuth metadata, authorization endpoints, consent page, and upstream connect callbacks the gateway needs. It's a no-op when no MCP-related policy is present, so adding it to projects that don't yet use the gateway has zero runtime cost.

2. Define one OAuth policy

The OAuth policy authenticates inbound MCP requests against your identity provider. Pick the first-class wrapper for your IdP — the provider catalog lists every supported IdP. The Auth0 case looks like this:

config/policies.json
{ "name": "auth0-managed-oauth", "policyType": "mcp-auth0-oauth-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpAuth0OAuthInboundPolicy", "options": { "auth0Domain": "$env(AUTH0_DOMAIN)", "clientId": "$env(AUTH0_CLIENT_ID)", "clientSecret": "$env(AUTH0_CLIENT_SECRET)", }, }, }

Each wrapper takes a small set of provider-specific options (a domain, a tenant ID, a subdomain, and so on) and derives the OIDC URLs from them. For IdPs without a dedicated wrapper — Ory Hydra, Authentik, FusionAuth, PingFederate, a custom OIDC server — use the generic mcp-oauth-inbound policy. See Configuring a generic OIDC provider for the worked example.

A project can have only one MCP OAuth policy. The gateway rejects any configuration with two, regardless of variant. The same policy is attached to every MCP route in the project — every route authenticates against the same identity provider.

3. Define one token-exchange policy per upstream

Each OAuth-protected upstream gets its own mcp-token-exchange-inbound policy:

config/policies.json
{ "name": "mcp-token-exchange-linear", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Linear", "protectedResourceMetadataUrl": "https://mcp.linear.app/.well-known/oauth-protected-resource", "authMode": "user-oauth", "scopes": [], "clientRegistration": { "mode": "auto" }, }, }, }

Name each policy mcp-token-exchange-<id>. The id after the prefix identifies the upstream in analytics and connect URLs. Changing the id strands any existing user-to-upstream connections, so pick it once and keep it.

For per-mode reference and worked examples per provider, see Connect a gateway to an upstream OAuth provider.

4. Define one route per upstream

Each upstream gets a route in routes.oas.json. The handler points at the upstream URL; the inbound policy chain attaches the OAuth policy followed by the matching token exchange policy:

config/routes.oas.json
{ "openapi": "3.1.0", "info": { "title": "MCP Gateway", "version": "0.1.0" }, "paths": { "/mcp/linear-v1": { "get,post": { "operationId": "linear-mcp-server", "summary": "Linear MCP Proxy", "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 path is yours to choose — /mcp/<provider>-v<n> is the recommended convention because it makes the path self-describing and reserves room for versioned upgrades, but the gateway works with any path the OpenAPI router accepts.

get,post is Zuplo's multi-method shorthand. The handler rejects GET with 405 Method Not Allowed because the gateway only speaks stateless Streamable HTTP over POST — see McpProxyHandler for the full handler reference.

Every MCP route must set operationId. Across the project, no two MCP routes can share an operationId or a path, and no two mcp-token-exchange-* policies can share an upstream id. If operationId is missing or duplicated, the gateway returns a configuration error on the first matching request.

Verify the gateway is wired up

Start the project with zuplo dev and the gateway is reachable at http://127.0.0.1:9000/mcp/linear-v1. A quick sanity check is to send an unauthenticated POST:

TerminalCode
curl -i -X POST http://127.0.0.1:9000/mcp/linear-v1 \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

The gateway should return 401 Unauthorized with a WWW-Authenticate header that points at the Protected Resource Metadata URL. If you see that, the OAuth policy is wired up correctly. See Local development for the dev-loop specifics, including the loopback-only login shortcut that skips your IdP during development.

Add more upstreams

The pattern is the same for each additional upstream: one MCP OAuth policy stays shared across the project, and one mcp-token-exchange-* policy and one route get added per new upstream MCP server. Per-user state is keyed by (subjectId, upstreamServerId), so each user maintains independent connections to each upstream they consent to.

For a worked example with two upstreams and the full file layout, see Add multiple upstream MCP servers.

Related

  • McpProxyHandler reference — every option and every behavior of the route handler.
  • Compatibility dates — why 2026-03-01 is required and what older dates break.
  • Local development — dev-loop, loopback URLs, the /oauth/dev-login shortcut, and the workerd restart quirk.
  • Add multiple upstream MCP servers — one project, many upstream MCP servers.
  • Curate the tools an upstream exposes — restrict and re-project the tools, prompts, and resources a route exposes.
Edit this page
Last modified on May 29, 2026
Manual OAuth testingMulti-upstream
On this page
  • 1. Register the MCP Gateway plugin
  • 2. Define one OAuth policy
  • 3. Define one token-exchange policy per upstream
  • 4. Define one route per upstream
  • Verify the gateway is wired up
  • Add more upstreams
  • Related
TypeScript
JSON
JSON
JSON