ZuploZuplo
LoginSign Up
  • Documentation
  • API Reference
Introduction
Getting Started
    Develop using the Portal
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth4 - Deploy5 - Dynamic Rate LimitingMCP - Quick start
    Develop Locally
      1 - Setup Your Gateway2 - Rate Limiting3 - API Key Auth
Concepts
Development
Policies
Handlers
API Keys
MCP Server
MCP Gateway
    IntroductionBetaQuickstartQuickstart (Local Dev)How it works
    Connect MCP clients
    Authentication
    Configuration
    Observability
    ReferenceTroubleshooting
AI Gateway
Developer Portal
Monetization
Deploying & Source Control
Observability
Networking & Infrastructure
Account Management
Programming API
Build with AI
Zuplo CLI
Migration Guides
Platform LimitsSecuritySupportTrust & ComplianceChangelog
powered by Zudoku
MCP Gateway

MCP Gateway quickstart (Local Dev)

Choose your Development Approach

Select how you'd like to build your gateway. You can switch between approaches at any time.

Portal Development
Local Development

Local Development

Develop and test your gateway locally using the Zuplo CLI. Full control over your environment.

Build a Zuplo MCP Gateway fronting Linear, running locally at http://127.0.0.1:9000/mcp/linear-v1. By the end, Claude Desktop connects over the gateway's per-user OAuth flow and answers "list my open Linear issues" with real results.

Any Zuplo project becomes a gateway by adding a plugin, a couple of policies, and a route. This guide uses Linear as the upstream and the built-in dev-login shortcut for sign-in, so you skip identity-provider setup to try it out. For production, swap in your provider: the gateway wraps Auth0, Okta, Microsoft Entra, Google, Clerk, Cognito, Keycloak, Logto, OneLogin, PingOne, and WorkOS, plus a generic OIDC fallback. See the provider catalog.

Prefer the browser with no local setup? The Portal quickstart reaches the same result through the Zuplo Portal UI.

Prerequisites

  • Node.js 20 or higher.

  • A local Zuplo project. Create an empty one with:

    TerminalCode
    npx create-zuplo-api@latest --empty

    Then cd into the new directory. See create-zuplo-api for other options, or import an existing portal project by connecting it to Git and cloning it.

New projects created with create-zuplo-api ship a recent compatibilityDate, so MCP Gateway features work out of the box. If you're adding the gateway to an older project and the build complains about the compatibility date, see Compatibility dates.

  1. Register the MCP Gateway plugin

    Open modules/zuplo.runtime.ts (create it if it doesn't exist) and register McpGatewayPlugin:

    modules/zuplo.runtime.ts
    import { RuntimeExtensions } from "@zuplo/runtime"; import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway"; export function runtimeInit(runtime: RuntimeExtensions) { runtime.addPlugin(new McpGatewayPlugin()); }

    The plugin registers the OAuth metadata, authorization endpoints, consent page, and upstream connect callbacks the gateway needs.

  2. Add an OAuth policy with the dev-login shortcut

    Setting up a real identity provider for local development is friction. You'd register a loopback callback, manage test users, and so on. The gateway exposes a loopback-only shortcut that skips the IdP round-trip entirely and signs you in as a fixed dev-browser-user.

    Open config/policies.json and add the generic OAuth policy pointed at the dev-login URL:

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

    /oauth/dev-login returns 403 Forbidden for any request that doesn't arrive over loopback, so it's safe to leave configured, but only useful in local dev. Production deployments should use a real OIDC provider through one of the IdP wrappers. A common pattern is keeping two OAuth policies (one for production, one for dev) and selecting between them in routes.oas.json by environment.

    When you do switch to a real provider, its policy reads credentials from $env(...) references. Define those values in a .env file at the project root:

    Terminal.env
    MCP_AUTH0_DOMAIN=your-tenant.us.auth0.com MCP_AUTH0_CLIENT_ID=your-auth0-web-app-client-id MCP_AUTH0_CLIENT_SECRET=your-auth0-web-app-client-secret

    .env is read when npm run dev starts, so restart the dev server after adding or changing a variable. Never commit .env. Check in a .env.example with placeholder values instead. The dev-login shortcut above needs no environment variables, so you can skip this until you wire up a provider.

  3. Add a token-exchange policy for the upstream

    Each OAuth-protected upstream gets its own mcp-token-exchange-inbound policy. It looks up the user's upstream credential and attaches it as the upstream Authorization header. Add this entry to config/policies.json:

    config/policies.json
    { "name": "mcp-token-exchange-linear", "policyType": "mcp-token-exchange-inbound", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpTokenExchangeInboundPolicy", "options": { "displayName": "Linear", "protectedResourceMetadataUrl": "https://mcp.linear.app/.well-known/oauth-protected-resource", "authMode": "user-oauth", "scopes": [], "clientRegistration": { "mode": "auto" } } } }

    authMode: "user-oauth" means each user connects their own Linear account the first time they call the route. clientRegistration: { "mode": "auto" } lets the gateway register itself with Linear's OAuth server on demand, so no upstream client credentials in source control.

  4. Add the route

    Open config/routes.oas.json and add an MCP route. The handler points at Linear's MCP server URL; the inbound policy chain runs the OAuth policy followed by the token-exchange policy:

    config/routes.oas.json
    { "openapi": "3.1.0", "info": { "title": "MCP Gateway", "version": "0.1.0" }, "paths": { "/mcp/linear-v1": { "get,post": { "operationId": "linear-mcp-server", "summary": "Linear MCP Proxy", "x-zuplo-route": { "corsPolicy": "none", "handler": { "module": "$import(@zuplo/runtime/mcp-gateway)", "export": "McpProxyHandler", "options": { "rewritePattern": "https://mcp.linear.app/mcp" } }, "policies": { "inbound": ["dev-oauth", "mcp-token-exchange-linear"] } } } } } }

    operationId is the stable identifier for the route. It appears in analytics and is part of the per-user upstream connection key, so pick it once and don't change it. The path is whatever you set; /mcp/<provider>-v<n> is the convention.

  5. Run the gateway

    From the project root:

    TerminalCode
    npm run dev

    The route is now reachable at http://127.0.0.1:9000/mcp/linear-v1.

    Checkpoint: confirm the OAuth policy is wired up

    Send an unauthenticated POST and expect a 401:

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

    The response should be 401 Unauthorized with a WWW-Authenticate: Bearer header pointing at /.well-known/oauth-protected-resource/mcp/linear-v1. That 401 confirms the OAuth policy is loaded. If you see a 200, 404, or 500 instead, the OAuth policy isn't attached to the route.

    Use 127.0.0.1, not localhost

    OAuth metadata and callback URLs key off the request origin. Other loopback aliases (localhost, ::1) can break OAuth subtly in local dev. See Local development for the full set of local-only details, including the known workerd restart quirk.

  6. Connect Claude Desktop

    Open Claude Desktop, go to Settings → Connectors, scroll to the bottom, and click Add custom connector. Paste http://127.0.0.1:9000/mcp/linear-v1 and click Add.

    Claude Desktop opens the gateway's OAuth flow in a browser:

    1. The dev-login shortcut signs you in without any IdP prompt.
    2. The gateway's consent page lists Linear with a Connect button.
    3. Click Connect, complete Linear's OAuth flow, then click Authorize to finish.

    Checkpoint: Claude is connected

    Back in Claude Desktop, the new connector appears in Settings → Connectors marked as connected. Subsequent requests reuse the tokens the gateway just issued.

    For per-client setup details, see Connect MCP clients.

  7. Test it

    In Claude Desktop, prompt the model with something that requires Linear. "list my open issues" works well. Claude asks for permission to call the tool, then returns results proxied through the gateway.

You now have a working MCP Gateway in front of Linear, running locally: Claude Desktop signs in through the dev-login shortcut, the gateway exchanges that for a per-user Linear token, and every call is proxied through. The same shape (one OAuth policy, one token-exchange policy per upstream, one route per upstream) scales out to as many upstream MCP servers as you want to front.

Deploy to production before sharing

The local gateway on 127.0.0.1 is for development only, and the dev-login shortcut works over loopback alone. Before giving others access, swap in a real identity provider and ship the gateway through the Zuplo Portal. See environments for setting up a production deployment.

Next steps

  • Deploy from the Portal: swap the dev-login shortcut for a real identity provider and ship the gateway through the Zuplo Portal.
  • Local development: the dev-login shortcut in depth, environment variables, and local-only quirks.
  • Connect more clients: Claude Code, Cursor, VS Code, ChatGPT, and any other MCP client.
  • How it works: the request lifecycle and the two OAuth surfaces.
  • Add more upstreams: front several upstream MCP servers from one Zuplo project.
  • Capability filtering: curate the tools, prompts, and resources each route exposes.
Edit this page
Last modified on May 29, 2026
QuickstartHow it works
On this page
  • Prerequisites
  • Next steps
TypeScript
JSON
JSON
JSON