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
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
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 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
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:
Code
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
McpProxyHandlerreference — every option and every behavior of the route handler.- Compatibility dates — why
2026-03-01is required and what older dates break. - Local development — dev-loop, loopback URLs, the
/oauth/dev-loginshortcut, and theworkerdrestart 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.