# Getting Started with MCP Gateway - Editor

<WizardSteps build="mcp-gateway" method="local" />

<MethodTabs build="mcp-gateway" method="local" />

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](../../mcp-gateway/auth/overview.mdx#identity-providers).

Prefer the browser with no local setup? The [Portal quickstart](./portal.mdx)
reaches the same result through the Zuplo Portal UI.

## Prerequisites

- [Node.js](https://nodejs.org/en/download) 24 or higher.
- A local Zuplo project. Create one with:

  ```bash
  npx create-zuplo-api@latest my-api-project
  ```

  Then `cd` into the new directory. See
  [`create-zuplo-api`](../../cli/create-zuplo-api.mdx) for other options, or
  [import an existing portal project](../../articles/local-development.mdx#import-your-existing-project)
  by connecting it to Git and cloning it.

:::note

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](../../mcp-gateway/code-config/compatibility-dates.mdx).

:::

## Run your MCP Gateway locally

<Stepper>

1. **Register the MCP Gateway plugin**

   Open `modules/zuplo.runtime.ts` (create it if it doesn't exist) 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.

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:

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

   :::caution

   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](../../mcp-gateway/code-config/local-development.mdx#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`:

   ```json 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, 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:

   ```json 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": ["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:

   ```bash
   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`.

   :::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 confirms the OAuth policy is loaded. If you see a 200, 404, or 500
   instead, the OAuth policy isn't attached to the route.

   :::

   :::caution{title="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](../../mcp-gateway/code-config/local-development.mdx#use-one-loopback-origin-everywhere).

   :::

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:

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

   :::tip{title="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](../../mcp-gateway/connect-clients/overview.mdx).

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.

</Stepper>

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.

:::caution{title="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](../../articles/environments.mdx) for setting up a production
deployment.

:::

## Next steps

- [Deploy from the Portal](./portal.mdx): swap the dev-login shortcut for a real
  identity provider and ship the gateway through the Zuplo Portal.
- [Local development](../../mcp-gateway/code-config/local-development.mdx): the
  dev-login shortcut in depth, environment variables, and local-only quirks.
- [Connect more clients](../../mcp-gateway/connect-clients/overview.mdx): Claude
  Code, Cursor, VS Code, ChatGPT, and any other MCP client.
- [How it works](../../mcp-gateway/how-it-works.mdx): the request lifecycle and
  the two OAuth surfaces.
- [Add more upstreams](../../mcp-gateway/code-config/multi-upstream.mdx): front
  several upstream MCP servers from one Zuplo project.
- [Capability filtering](../../mcp-gateway/capability-filtering.mdx): curate the
  tools, prompts, and resources each route exposes.
