Local development
The MCP Gateway runs the same way locally as any Zuplo project — zuplo dev,
port 9000, hot reload on file changes. A few details are specific to the
gateway: the gateway prefers 127.0.0.1 over localhost, OAuth login can be
short-circuited entirely in dev, and the local workerd worker needs a full
restart after some MCP client connect attempts.
Start the gateway
From the project root:
Code
The gateway listens at http://127.0.0.1:9000. Each MCP route in
routes.oas.json becomes reachable at that origin — for example
http://127.0.0.1:9000/mcp/linear-v1.
Prefer 127.0.0.1 over localhost
OAuth metadata, callback URLs, and the in-dev login shortcut all key off the
request origin. The runtime tolerates other loopback aliases — localhost,
::1, [::1] — but the gateway prefers 127.0.0.1 to avoid mixed
loopback-family behavior in the local runtime.
When configuring an MCP client locally, use 127.0.0.1:
Code
The same applies to any callback or redirect URI you configure with an identity provider for local testing.
Bypass your IdP with /oauth/dev-login
Setting up a real OIDC provider for local development is friction — you'd have
to register a localhost callback, manage test users, and so on. The gateway
exposes a loopback-only shortcut that synthesizes a fixed dev-browser-user
subject and skips the IdP round-trip entirely.
To use it, set browserLogin.url to the dev-login URL when configuring the
OAuth policy:
Code
When browserLogin.url points at /oauth/dev-login, the
browserLogin.tokenUrl, browserLogin.clientId, and
browserLogin.clientSecret options aren't required. The gateway internally
synthesizes the IdP callback with a dev-browser-user subject and renders the
consent page normally.
The /oauth/dev-login route returns 403 Forbidden for any request that
doesn't arrive over loopback. It's not a security risk to leave configured for
production, but it's also not useful — production deployments should use a real
OIDC provider via
mcp-auth0-oauth-inbound or
mcp-oauth-inbound.
A common pattern is keeping two OAuth policies in the project — one for
production (Auth0 or generic OIDC) and one for local dev — and selecting between
them in routes.oas.json based on the environment.
Environment variables
When the OAuth policy reads from $env(...) references, define the values in a
.env file at the project root:
Code
.env is read at zuplo dev startup. Restart the dev server after adding or
changing an environment variable — the runtime caches the parsed values per
process.
Never commit .env to source control. Instead, check in a .env.example (or
env.example) that documents which variables are required and an
empty/placeholder value for each.
Adding the gateway to a local MCP client
Once zuplo dev is running and the route is reachable, add the gateway URL to
your MCP client config the same way you'd add any other remote MCP server. For
example, with Claude Desktop:
Code
The client triggers the gateway's OAuth flow on first connect. With
/oauth/dev-login configured, the browser tab opens, lands on the consent page
without any IdP login, and you connect each upstream through its normal browser
OAuth flow. Subsequent calls reuse the issued tokens until they expire.
See Connect MCP clients for client-specific snippets and the connect URL format.
When zuplo dev crashes after a connect attempt
The local Zuplo runtime is workerd, and some MCP client connect attempts can
leave the worker in a state where hot reload no longer recovers it. If the dev
server stops responding after an MCP client connects — particularly after
browser OAuth callbacks finish — fully restart zuplo dev rather than relying
on the file-watcher to re-bundle.
In practice:
Code
Then have the MCP client reconnect. The encrypted upstream tokens are stored against your subject id, so a restart doesn't force a re-consent.
This is a known dev-only quirk and doesn't affect deployed gateways.
Verifying the gateway is up
Two quick checks that don't require an MCP client:
Fetch the well-known OAuth metadata for a route. The path follows the
route's operationId:
Code
A correct response is JSON with resource, authorization_servers,
bearer_methods_supported, and scopes_supported fields.
Send a POST without a token. The gateway should return 401 with a
WWW-Authenticate header pointing at the Protected Resource Metadata URL:
Code
If you see the 401 plus the challenge, the OAuth policy is wired up correctly. The next call from a real client will then start the OAuth dance.
Next steps
McpProxyHandlerreference — the route handler the gateway uses for proxying.- Compatibility dates — pin
2026-03-01inzuplo.jsoncso the upstream-401 retry hook works. - Multi-upstream pattern — one project, many upstreams.
- Connect MCP clients — wire each client to the local or deployed gateway URL.