# MCP Gateway quickstart

By the end of this guide you'll have a Zuplo MCP Gateway fronting Linear at
`https://<your-gateway>/mcp/linear-v1`, Claude Desktop signed in through Auth0,
and your own Linear account connected through the gateway's per-user OAuth flow.
From there, prompting Claude with "list my open Linear issues" returns real
results proxied through the gateway, and you'll see the call show up in the
project's analytics.

The MCP Gateway isn't a separate project type — every Zuplo project can become
an MCP Gateway by adding a plugin, a couple of policies, and a route. The
example uses Linear as the upstream MCP server and Auth0 as the identity
provider, but the same pattern applies to any upstream that speaks the MCP
authorization spec and any OIDC-compatible identity provider. For a generic OIDC
setup, see [Configuring Okta](./auth/configuring-okta.mdx).

## Prerequisites

- A Zuplo project. Create one from the
  [new project page](https://portal.zuplo.com/+/account/projects/new) if you
  don't have one already.
- An Auth0 tenant with a Regular Web Application configured. The
  [Auth0 setup section in Configuring Auth0](./auth/configuring-auth0.mdx#set-up-the-auth0-tenant)
  covers the dashboard side.
- The `AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, and `AUTH0_CLIENT_SECRET` from your
  Auth0 application.

<Stepper>

1. **Pin the compatibility date**

   MCP Gateway features require `compatibilityDate >= 2026-03-01` in
   `zuplo.jsonc`:

   ```jsonc title="zuplo.jsonc"
   {
     "version": 1,
     "compatibilityDate": "2026-03-01",
   }
   ```

   Existing projects on an older date need to bump it before adding MCP
   features. New projects default to a recent date, so most won't need to change
   anything.

2. **Register the MCP Gateway plugin**

   Add a `modules/zuplo.runtime.ts` file (or edit the existing one) and register
   `McpGatewayPlugin`:

   ```ts title="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.

3. **Add an MCP OAuth policy**

   Open `config/policies.json` and add the Auth0 MCP OAuth policy. It
   authenticates inbound MCP requests against your Auth0 tenant:

   ```jsonc title="config/policies.json"
   {
     "name": "auth0-managed-oauth",
     "policyType": "mcp-auth0-oauth-inbound",
     "handler": {
       "module": "$import(@zuplo/runtime/mcp-gateway)",
       "export": "McpAuth0OAuthInboundPolicy",
       "options": {
         "auth0Domain": "$env(AUTH0_DOMAIN)",
         "clientId": "$env(AUTH0_CLIENT_ID)",
         "clientSecret": "$env(AUTH0_CLIENT_SECRET)",
       },
     },
   }
   ```

   :::caution

   `auth0Domain` is a bare hostname (`my-tenant.us.auth0.com`), not a URL.

   :::

   Set the three environment variables on the project — `AUTH0_DOMAIN` in plain
   config and `AUTH0_CLIENT_ID` / `AUTH0_CLIENT_SECRET` in the secret store.

4. **Add a token-exchange policy for the upstream**

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

   ```jsonc title="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, using
   OAuth Client ID Metadata Documents with a DCR fallback — no upstream client
   credentials need to live in source control.

5. **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 attaches the OAuth policy
   followed by the token exchange policy:

   ```jsonc title="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": ["auth0-managed-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 — pick it once and don't
   change it.

   The path is whatever you set in the route — `/mcp/<provider>-v<n>` is the
   convention, but any path the OpenAPI router accepts works.

6. **Run the gateway**

   Run `zuplo dev` from the project root:

   ```bash
   zuplo dev
   ```

   The route is now reachable at `http://127.0.0.1:9000/mcp/linear-v1`. Deploy
   when you're ready to expose it publicly; the route then lives at
   `https://<your-deployment>/mcp/linear-v1`.

   :::tip{title="Checkpoint: confirm the OAuth policy is wired up"}

   Send an unauthenticated POST and expect a `401`:

   ```bash
   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 is the gateway telling a future MCP client "you need to authenticate
   first" — it confirms the OAuth policy is loaded. If you see a 200, 404, or
   500 instead, the OAuth policy isn't attached to the route.

   :::

7. **Connect Claude Desktop**

   Open Claude Desktop, go to **Settings → Connectors**, scroll to the bottom of
   the list, and click **Add custom connector**. Paste the route URL — for the
   locally-running gateway, that's `http://127.0.0.1:9000/mcp/linear-v1`; for a
   deployed gateway, use the public URL — and click **Add**.

   Claude Desktop opens the gateway's OAuth flow in a browser:
   1. Sign in with Auth0.
   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.

   :::tip{title="Checkpoint: Claude is connected"}

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

   :::

   For per-client setup details, see
   [Connect MCP clients](./connect-clients/overview.mdx).

8. **Test it**

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

   Open the project's
   [Analytics dashboard](https://portal.zuplo.com/+/account/project/analytics)
   and switch to the **MCP** tab to see the call appear in the events timeline,
   the success rate, the top capabilities table, and the user breakdown.

</Stepper>

You now have a working MCP Gateway in front of Linear: Claude Desktop
authenticates against Auth0, the gateway exchanges that for a per-user Linear
token, and every call lands in your analytics. 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.

## Next steps

- [Connect more clients](./connect-clients/overview.mdx) — Claude Code, Cursor,
  VS Code, ChatGPT, and any other MCP client.
- [How it works](./how-it-works.mdx) — the request lifecycle and the two OAuth
  surfaces.
- [Add more upstreams](./code-config/multi-upstream.mdx) — front several
  upstream MCP servers from one Zuplo project.
- [Capability filtering](./capability-filtering.mdx) — curate the tools,
  prompts, and resources each route exposes, including description and
  annotation overrides.
