ZuploZuplo
LoginStart for Free
  • Documentation
  • API Reference
Getting Started
Concepts
API Management
AI Gateway
MCP Gateway
    Overview
    Getting Started
      How it works
      Connect MCP clients
      Authentication
      Cross App Access
      Configuration
        Set up the gatewayMulti-upstreamLocal developmentCapability filteringCurate toolsCurate tools (in code)McpProxyHandlerCompatibility dates
      Policies
      Observability
      ReferenceServer RegistryTroubleshooting
    MCP Server
    Developer Portal
    Development
    Deploying & Source Control
    Analytics
    Observability
    Networking & Infrastructure
    Account Management
    Programming API
    Build with AI
    Zuplo CLI
    Migration Guides
    Platform LimitsVersion Support PolicySecuritySupportTrust & ComplianceChangelog
    powered by Zuplo
    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:

    Code
    // 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:

    Code
    // 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:

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

    Code
    // 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

    • McpProxyHandler reference — the route handler the gateway uses for proxying.
    • Compatibility dates — pin 2026-03-01 in zuplo.jsonc.
    • Multi-upstream pattern — one project, many upstreams.
    • Connect MCP clients — wire each client to the local or deployed gateway URL.
    Edit this page
    Last modified on September 21, 2026
    Multi-upstreamCapability filtering
    On this page
    • Start the gateway
    • Use one loopback origin everywhere
    • Bypass your IdP with /__zuplo/oauth/dev-login
      • Configure local and deployed environments
    • Environment variables
    • Adding the gateway to a local MCP client
    • When zuplo dev crashes after a connect attempt
    • Verifying the gateway is up
    • Next steps
    JSON
    JSON
    JSON