# Deploy an MCP Server on Cloudflare Workers

Cloudflare Workers give you edge-deployed MCP servers with zero infrastructure management. But Workers have a unique constraint: **isolates are reused across requests**, which means naive module-level caches leak tokens between users.

The [`@keycardai/cloudflare`](/sdk/cloudflare/) package handles this. It adapts Keycard's auth primitives to Workers' `fetch(request, env)` model — real JWT verification, OAuth metadata endpoints, and per-user token caching that's isolate-safe by design.

This guide walks through deploying a Worker with delegated API access. The full working example is in the [TypeScript SDK](https://github.com/keycardai/typescript-sdk/tree/main/examples/cloudflare-worker).

> **Tip:** **Building with an AI agent?** Point it at [docs.keycard.ai/llms.txt](/llms.txt) for the full docs index, or [docs.keycard.ai/guides/cloudflare-worker.md](/guides/cloudflare-worker.md) for this guide.

## How It Works

```mermaid
flowchart LR
    A[AI Agent] -->|"Bearer JWT"| B[Your Worker]
    B -->|"Verify JWT"| C[Keycard JWKS]
    B -->|"Exchange token"| D[Keycard STS]
    D -->|"Upstream token"| B
    B -->|"Bearer upstream"| E[External API]
```

1. AI agent sends a request with a Keycard JWT
2. Worker verifies the JWT against Keycard's JWKS endpoint
3. Worker exchanges the JWT for an upstream API token via Keycard STS
4. Worker calls the external API with the exchanged token

The `createKeycardWorker()` wrapper handles steps 1-2 automatically. You handle 3-4 using `IsolateSafeTokenCache`.

## Prerequisites

- **Cloudflare account** with [wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/) installed
- **Keycard account** with a configured zone and identity provider
- Completed the [Build an MCP Server](/guides/mcp-server/) guide (for Keycard Console concepts)

## Keycard Console Setup

1. **Register the upstream API**

   If using the Resource Catalog (GitHub, Google, etc.), add it there. Otherwise create a provider and resource manually for your API.

2. **Register your Worker as a resource**

   Navigate to **Resources** → **Create Resource**:

   | Field | Value |
   | --- | --- |
   | **Resource Name** | My Worker MCP Server |
   | **Resource Identifier** | `https://your-worker.your-subdomain.workers.dev` |
   | **Credential Provider** | Zone Provider |
   | **Scopes** | `mcp:tools` |

3. **Add the upstream API as a dependency**

   Go to the resource details → **Dependencies** tab → **Connect Resource** → select your upstream API resource.

4. **Create application credentials**

   Navigate to **Applications** → **Create Application**:

   | Field | Value |
   | --- | --- |
   | **Provided Resource** | Your Worker MCP Server |
   | **Dependency** | Your upstream API resource |

   Generate **client credentials** and save the Client ID and Client Secret.

## Implementation

Install dependencies:

```bash
npm install @keycardai/cloudflare @keycardai/oauth @modelcontextprotocol/sdk
npm install -D @cloudflare/workers-types typescript wrangler
```

### Worker Entry Point

`createKeycardWorker()` handles OAuth metadata, CORS, and JWT verification. Your handler only runs for authenticated requests:

```typescript
// src/index.ts
interface Env {
  KEYCARD_ISSUER: string;
  KEYCARD_CLIENT_ID?: string;
  KEYCARD_CLIENT_SECRET?: string;
  KEYCARD_PRIVATE_KEY?: string;
  KEYCARD_RESOURCE_URL: string;
}

// Module-level cache is safe — keyed by user identity, not shared
let tokenCache: IsolateSafeTokenCache;

function getCache(env: Env) {
  if (!tokenCache) {
    const credential = resolveCredential(env);
    const client = new TokenExchangeClient(env.KEYCARD_ISSUER, credential.getAuth() ?? undefined);
    tokenCache = new IsolateSafeTokenCache(client, { credential });
  }
  return tokenCache;
}

export default createKeycardWorker<Env>({
  resourceName: "My Worker MCP Server",
  scopesSupported: ["mcp:tools"],
  requiredScopes: ["mcp:tools"],

  async fetch(request, env, ctx, auth) {
    // auth.subject is the verified user identity
    // auth.token is the raw JWT for token exchange
    const cache = getCache(env);
    const upstream = await cache.getToken(auth.subject!, auth.token, env.KEYCARD_RESOURCE_URL);

    // Use upstream.accessToken to call your API
    // Register MCP tools, handle routes, etc.
    // ...
  },
});
```

> **Caution:** **Never cache tokens in a plain module-level variable.** Workers reuse isolates across users — a `let cachedToken = ...` leaks user A's token to user B. `IsolateSafeTokenCache` keys by `${subject}::${resource}` to prevent this.

### Wrangler Config

```jsonc
// wrangler.jsonc
{
  "name": "my-mcp-worker",
  "main": "src/index.ts",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"],
  "vars": {
    "KEYCARD_ISSUER": "https://your-zone-id.keycard.cloud",
    "KEYCARD_RESOURCE_URL": "https://api.github.com"
  }
}
```

## Credential Modes

You can authenticate your Worker with Keycard using either method:

### Option A: Client Credentials

Store the client ID and secret from Keycard Console:

```bash
wrangler secret put KEYCARD_CLIENT_ID
wrangler secret put KEYCARD_CLIENT_SECRET
```

### Option B: Web Identity (no client secret)

Generate a private key — the Worker serves its public key automatically at `/.well-known/jwks.json`:

```bash
openssl genrsa 2048 | wrangler secret put KEYCARD_PRIVATE_KEY
```

Register the Worker's JWKS URL (`https://your-worker.workers.dev/.well-known/jwks.json`) in Keycard Console as the application's public key endpoint.

`createKeycardWorker` auto-detects which mode to use from env.

## Deploy and Test

1. **Deploy**

   ```bash
   wrangler deploy
   ```

2. **Set secrets** (Option A or B from above)

3. **Connect from Cursor/Claude Desktop**

   Add to your MCP settings:

   ```json
   {
     "mcpServers": {
       "my-worker": {
         "url": "https://your-worker.your-subdomain.workers.dev/mcp"
       }
     }
   }
   ```

4. **Verify in Audit Logs**

   Check Keycard Console **Audit Logs** for `credentials:issue` events showing the identity chain (user + application).

## Full Example

The complete working example — with MCP tool registration, token exchange, and both credential modes — is in the TypeScript SDK:

**[examples/cloudflare-worker](https://github.com/keycardai/typescript-sdk/tree/main/examples/cloudflare-worker)**

## Troubleshooting

### 401 on every request

- Verify `KEYCARD_ISSUER` matches your zone URL exactly (including `https://`)
- Check that your Worker's URL matches the resource identifier in Keycard Console

### Token exchange fails

- Verify the upstream API resource is added as a **dependency** on your Application
- Check that client credentials (or private key) are set correctly as Worker secrets
- Ensure the user completed the OAuth consent flow for the upstream API

### "Missing Keycard credentials in env"

- Set either `KEYCARD_CLIENT_ID` + `KEYCARD_CLIENT_SECRET` or `KEYCARD_PRIVATE_KEY` via `wrangler secret put`
