Set up an MCP Gateway
To turn any Zuplo project into an MCP Gateway, configure five things in source
control: the compatibility date in zuplo.jsonc, 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. Pin the compatibility date
MCP Gateway features require compatibilityDate >= 2026-03-01 in zuplo.jsonc:
zuplo.jsonc
New Zuplo projects default to a recent compatibility date, so this only applies to existing projects being upgraded to use the MCP Gateway. See Compatibility dates for details.
2. 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.
3. Define one OAuth policy
The OAuth policy authenticates inbound MCP requests against your identity provider. Pick one of two variants based on the IdP.
For Auth0:
config/policies.json
For any other OIDC provider (Okta, Microsoft Entra ID, Cognito, Keycloak, etc.),
use the generic mcp-oauth-inbound policy with explicit oidc.* and
browserLogin.* options. See Configuring Okta
for a 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.
4. 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.
5. 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.