ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Getting Started
Concepts
API Management
AI Gateway
MCP Gateway
    Overview
    Getting Started
      How it works
      Connect MCP clients
      Authentication
      Cross App Access
      Configuration
        Set up the gatewayMulti-upstreamLocal developmentCapability filteringCurate toolsCurate tools (in code)McpProxyHandlerCompatibility dates
      Policies
      Observability
      ReferenceServer RegistryTroubleshooting
    MCP Server
    Developer Portal
    Development
    Deploying & Source Control
    Analytics
    Observability
    Networking & Infrastructure
    Account Management
    Programming API
    Build with AI
    Zuplo CLI
    Migration Guides
    Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
    powered by Zuplo
    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:

    Code
    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 also serves the read-only MCP Server Registry by default. See the McpGatewayPlugin reference for all configuration options. The plugin is 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:

    Code
    { "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)", }, }, }

    Match your environment variable names to the $env(...) references in policies.json.

    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.

    Attach the same MCP OAuth policy to every MCP route in the project. Attaching different OAuth policies causes a configuration error.

    3. Define one token-exchange policy per upstream

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

    Code
    { "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:

    Code
    { "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, or npm run dev if the project's dev script runs that command. Then send an unauthenticated request to the route:

    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 attached 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.
    • ZuploMcpSdk — programmatically control MCP tool results from custom handler modules.
    • Compatibility dates — why 2026-03-01 is required and what older dates break.
    • Local development — dev-loop, loopback URLs, the /__zuplo/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 September 21, 2026
    Configuration referenceMulti-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