MCP Gateway

Getting Started with MCP Gateway - Editor

MCP Gateway tutorialLocal with an editor

Connect an MCP client to Linear through your own gateway and call a Linear tool.

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

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.

Run your MCP Gateway locally

  1. Register the MCP Gateway plugin

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

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

    The local dev-login endpoint signs you in as dev-browser-user without an identity provider.

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

    JSONconfig/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/jwks" }, "browserLogin": { "url": "http://127.0.0.1:9000/__zuplo/oauth/dev-login" } } } }

    The policy requires oidc.issuer and oidc.jwksUrl, but dev-login doesn't use them. Keep these placeholder values for this tutorial; the gateway doesn't serve /jwks.

    Before deploying, configure an OIDC identity provider. The dev-login endpoint only accepts requests from loopback origins and returns 403 Forbidden for other origins. See Configure local and deployed environments to use one policy with different environment variables for each environment.

  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:

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

    JSONconfig/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 project's dev script runs zuplo 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 the same hostname for local URLs

    Use 127.0.0.1 in your MCP client URL to match the policy in this tutorial. You can use localhost instead, but update both URLs to avoid a callback mismatch. See Local development.

  6. Connect Claude Desktop

    Use a local stdio bridge so Claude Desktop can reach your local gateway. Native remote connectors connect from Anthropic's servers and cannot reach 127.0.0.1 on your computer.

    In Claude Desktop, open Settings → Developer → Edit Config and add this entry to claude_desktop_config.json, preserving any existing servers:

    JSONclaude_desktop_config.json
    { "mcpServers": { "Zuplo MCP": { "command": "npx", "args": [ "-y", "mcp-remote", "http://127.0.0.1:9000/mcp/linear-v1", "--allow-http" ] } } }

    Save the file and restart Claude Desktop. The bridge opens the gateway's OAuth flow on first connection.

    In the 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 Zuplo MCP server appears under Settings → Developer. Its tools are available in chat. 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