# MCP Gateway quickstart

This guide walks through adding MCP Gateway features to a Zuplo project,
configuring authentication, exposing one upstream MCP server, and connecting
Claude Desktop. 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. The same pattern applies to any other 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`.

   A quick sanity check is to send an unauthenticated POST:

   ```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 gateway should return `401 Unauthorized` with a `WWW-Authenticate` header
   that points at the Protected Resource Metadata URL. If you see that, the
   OAuth policy is wired up correctly.

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.

   Subsequent requests reuse the issued tokens. 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>

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