Local development
Use zuplo dev to run the MCP Gateway locally on port 9000 with hot reload.
For local testing, you can sign in without setting up an identity provider.
Start the gateway
Start the development server from the project root:
Code
If the project has the standard dev script, npm run dev runs the same
command.
The gateway listens on port 9000. Each MCP route in routes.oas.json becomes
reachable there — for example http://127.0.0.1:9000/mcp/linear-v1.
Use one loopback origin everywhere
Use either http://localhost:9000 or http://127.0.0.1:9000 consistently for
local gateway URLs. The gateway treats them as different origins.
Use the same hostname in these settings:
- The gateway URL in your MCP client configuration.
browserLogin.urlwhen using the local dev-login endpoint.- The gateway callback and allowed-origin URLs registered with your identity provider, when testing with one.
Mixing hostnames can cause oauth_callback_mismatch during local login or a
callback URL error from your identity provider. The examples on this page use
127.0.0.1.
Bypass your IdP with /__zuplo/oauth/dev-login
The local /__zuplo/oauth/dev-login endpoint signs you in as dev-browser-user
without an identity provider.
To use it, set browserLogin.url to the dev-login URL when configuring the
OAuth policy:
Code
When browserLogin.url points at /__zuplo/oauth/dev-login, the
browserLogin.tokenUrl, browserLogin.clientId, and
browserLogin.clientSecret options aren't required. The consent page renders
normally.
The policy requires oidc.issuer and oidc.jwksUrl, but dev-login doesn't use
them. Keep the placeholder values in this example for local testing. The /jwks
URL is a placeholder, not an endpoint the gateway serves.
Use an OIDC identity provider in deployed environments. The dev-login endpoint
only accepts requests from loopback origins and returns 403 Forbidden for
other origins.
Configure local and deployed environments
Attach the same MCP OAuth policy to every MCP route. To use dev-login locally
and an identity provider when deployed, configure a generic mcp-oauth-inbound
policy with environment variables:
Code
For local development, set these values:
Code
Leave BROWSER_LOGIN_TOKEN_URL, BROWSER_LOGIN_CLIENT_ID, and
BROWSER_LOGIN_CLIENT_SECRET unset locally. In each deployed environment, set
all six variables to your identity provider's values.
Provider-specific policies such as mcp-auth0-oauth-inbound use the provider's
login page and don't support dev-login. See
Define one OAuth policy.
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.
Match your environment variable names to the $env(...) references in
policies.json.
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
/__zuplo/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
Some MCP client connect attempts can leave the local dev server 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:
Code
Then have the MCP client reconnect. A restart doesn't force a re-consent — your upstream tokens are still stored.
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.jsonc. - Multi-upstream pattern — one project, many upstreams.
- Connect MCP clients — wire each client to the local or deployed gateway URL.