Configuration

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:

TerminalCode
zuplo dev

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.url when 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:

JSONCode
// config/policies.json { "name": "dev-oauth", "policyType": "mcp-oauth-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpOAuthInboundPolicy", "options": { // Required placeholders for local dev-login. "oidc": { "issuer": "http://127.0.0.1:9000", "jwksUrl": "http://127.0.0.1:9000/jwks", }, "browserLogin": { "url": "http://127.0.0.1:9000/__zuplo/oauth/dev-login", }, }, }, }

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:

JSONCode
// config/policies.json "options": { "oidc": { "issuer": "$env(OIDC_ISSUER)", "jwksUrl": "$env(OIDC_JWKS_URL)", }, "browserLogin": { "url": "$env(BROWSER_LOGIN_URL)", "tokenUrl": "$env(BROWSER_LOGIN_TOKEN_URL)", "clientId": "$env(BROWSER_LOGIN_CLIENT_ID)", "clientSecret": "$env(BROWSER_LOGIN_CLIENT_SECRET)", }, },

For local development, set these values:

Terminal.env
OIDC_ISSUER=http://127.0.0.1:9000 OIDC_JWKS_URL=http://127.0.0.1:9000/jwks BROWSER_LOGIN_URL=http://127.0.0.1:9000/__zuplo/oauth/dev-login

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:

TerminalCode
# .env # Auth0 wrapper interpolations AUTH0_DOMAIN=your-tenant.us.auth0.com AUTH0_CLIENT_ID=your-auth0-web-app-client-id AUTH0_CLIENT_SECRET=your-auth0-web-app-client-secret # Optional: the audience the gateway requires on issued tokens AUTH0_AUDIENCE=https://mcp-gateway.example.com

.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:

JSONCode
// claude_desktop_config.json { "mcpServers": { "linear-via-zuplo-local": { "url": "http://127.0.0.1:9000/mcp/linear-v1", }, }, }

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:

TerminalCode
# Stop zuplo dev with Ctrl+C # Start it again zuplo dev

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:

TerminalCode
curl http://127.0.0.1:9000/.well-known/oauth-protected-resource/mcp/linear-v1

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:

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}'

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

Last modified on