# Configuring Clerk

The MCP Gateway can use [Clerk](https://clerk.com/) as the identity provider
behind its downstream OAuth flow. The `mcp-clerk-oauth-inbound` policy is a
Clerk-friendly wrapper around the generic `mcp-oauth-inbound` policy: provide
your Clerk Frontend API URL, a client ID, and a client secret, and the policy
derives the OIDC issuer, JWKS URL, and authorize and token URLs for you.

This guide walks through the Clerk dashboard setup, then wires the policy into a
gateway project. Read the [authentication overview](./overview.mdx) first for
the two-layer OAuth model.

## Set up Clerk

The MCP Gateway acts as an OAuth 2.1 authorization server in front of Clerk.
Clerk handles browser login; the gateway issues its own access tokens that bind
to MCP routes.

### Create an OAuth application

1. In the Clerk Dashboard, switch to the instance you want the gateway to use,
   then open **Configure → OAuth Applications**.
2. Click **Add OAuth application**.
3. Give the application a name (for example, `Zuplo MCP Gateway`).
4. Set **Redirect URIs** to `https://<gateway-host>/oauth/callback`. Add
   `http://localhost:9000/oauth/callback` for local development with
   `zuplo dev`.
5. Select the OIDC scopes the gateway needs — `openid`, `profile`, and `email`
   are enough.
6. Click **Save**.

Note the **Client ID** and **Client Secret** from the application's detail page.
You'll wire these into the policy in the next section.

### Find the Frontend API URL

Open **Configure → Domains** in the Clerk Dashboard. The **Frontend API URL** is
shown at the top — it looks like `https://verb-noun-00.clerk.accounts.dev` on
development instances or `https://clerk.example.com` on production instances
with a custom domain. Copy the origin (no trailing path).

## Wire the policy into the gateway

Add the policy to `config/policies.json`:

```json
{
  "name": "clerk-managed-oauth",
  "policyType": "mcp-clerk-oauth-inbound",
  "handler": {
    "module": "$import(@zuplo/runtime/mcp-gateway)",
    "export": "McpClerkOAuthInboundPolicy",
    "options": {
      "frontendApiUrl": "$env(CLERK_FRONTEND_API_URL)",
      "clientId": "$env(CLERK_CLIENT_ID)",
      "clientSecret": "$env(CLERK_CLIENT_SECRET)"
    }
  }
}
```

:::caution

`frontendApiUrl` is the origin only. Don't include a path, query string, or
fragment — the policy fails at boot if any of those are present.

:::

Set the three environment variables in your project's environment configuration.
`CLERK_FRONTEND_API_URL` goes in plain config; the secret values belong in the
project secret store.

Attach the policy to each MCP route in `config/routes.oas.json`:

```jsonc
{
  "paths": {
    "/mcp/linear-v1": {
      "get,post": {
        "operationId": "linear-mcp-server",
        "x-zuplo-route": {
          "corsPolicy": "none",
          "handler": {
            "module": "$import(@zuplo/runtime/mcp-gateway)",
            "export": "McpProxyHandler",
            "options": {
              "rewritePattern": "https://mcp.linear.app/mcp",
            },
          },
          "policies": {
            "inbound": ["clerk-managed-oauth", "mcp-token-exchange-linear"],
          },
        },
      },
    },
  },
}
```

Register the gateway plugin in `modules/zuplo.runtime.ts`:

```ts
import { RuntimeExtensions } from "@zuplo/runtime";
import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway";

export function runtimeInit(runtime: RuntimeExtensions) {
  runtime.addPlugin(new McpGatewayPlugin());
}
```

## What the wrapper derives

| Generic field           | Derived value                            |
| ----------------------- | ---------------------------------------- |
| `oidc.issuer`           | `{frontendApiUrl}`                       |
| `oidc.jwksUrl`          | `{frontendApiUrl}/.well-known/jwks.json` |
| `browserLogin.url`      | `{frontendApiUrl}/oauth/authorize`       |
| `browserLogin.tokenUrl` | `{frontendApiUrl}/oauth/token`           |

## Test the configuration

The fastest sanity check is to connect an MCP client:

1. Open Claude Desktop, Cursor, Claude Code, or another OAuth-aware MCP client.
2. Add a remote MCP server pointing at one of your `/mcp/{slug}` routes.
3. The client should redirect you to Clerk's login page. After login, the
   gateway's consent screen renders. Approve it.
4. The client receives an access token and can call `tools/list`.

If something fails partway through, walk the flow manually using the
[manual OAuth testing guide](./manual-oauth-testing.mdx) — it exercises every
endpoint with `curl` so you can see the raw responses.

## Common issues

- **The policy rejects `frontendApiUrl` at boot.** The value includes a path,
  query string, or fragment. Use only the origin (`https://clerk.example.com`).
- **Browser login redirects but the callback fails.** The
  `https://<gateway-host>/oauth/callback` URL isn't on the OAuth application's
  redirect URIs allow-list in Clerk.

## Related

- [Authentication overview](./overview.mdx)
- [Configuring a generic OIDC provider](./configuring-generic-oidc.mdx)
- [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx)
