Deploy an MCP Server on Cloudflare Workers
Deploy a Keycard-protected MCP server on Cloudflare Workers with JWT verification, token exchange, and isolate-safe caching
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 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.
How It Works
Section titled “How It Works”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]
- AI agent sends a request with a Keycard JWT
- Worker verifies the JWT against Keycard’s JWKS endpoint
- Worker exchanges the JWT for an upstream API token via Keycard STS
- Worker calls the external API with the exchanged token
The createKeycardWorker() wrapper handles steps 1-2 automatically. You handle 3-4 using IsolateSafeTokenCache.
Prerequisites
Section titled “Prerequisites”- Cloudflare account with wrangler CLI installed
- Keycard account with a configured zone and identity provider
- Completed the Build an MCP Server guide (for Keycard Console concepts)
Keycard Console Setup
Section titled “Keycard Console Setup”-
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.
-
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.devCredential Provider Zone Provider Scopes mcp:tools -
Add the upstream API as a dependency
Go to the resource details → Dependencies tab → Connect Resource → select your upstream API resource.
-
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
Section titled “Implementation”Install dependencies:
npm install @keycardai/cloudflare @keycardai/oauth @modelcontextprotocol/sdknpm install -D @cloudflare/workers-types typescript wranglerWorker Entry Point
Section titled “Worker Entry Point”createKeycardWorker() handles OAuth metadata, CORS, and JWT verification. Your handler only runs for authenticated requests:
import { createKeycardWorker, IsolateSafeTokenCache, resolveCredential } from "@keycardai/cloudflare";import { TokenExchangeClient } from "@keycardai/oauth/tokenExchange";
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 sharedlet 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. // ... },});Wrangler Config
Section titled “Wrangler Config”{ "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
Section titled “Credential Modes”You can authenticate your Worker with Keycard using either method:
Option A: Client Credentials
Section titled “Option A: Client Credentials”Store the client ID and secret from Keycard Console:
wrangler secret put KEYCARD_CLIENT_IDwrangler secret put KEYCARD_CLIENT_SECRETOption B: Web Identity (no client secret)
Section titled “Option B: Web Identity (no client secret)”Generate a private key — the Worker serves its public key automatically at /.well-known/jwks.json:
openssl genrsa 2048 | wrangler secret put KEYCARD_PRIVATE_KEYRegister 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
Section titled “Deploy and Test”-
Deploy
Terminal window wrangler deploy -
Set secrets (Option A or B from above)
-
Connect from Cursor/Claude Desktop
Add to your MCP settings:
{"mcpServers": {"my-worker": {"url": "https://your-worker.your-subdomain.workers.dev/mcp"}}} -
Verify in Audit Logs
Check Keycard Console Audit Logs for
credentials:issueevents showing the identity chain (user + application).
Full Example
Section titled “Full Example”The complete working example — with MCP tool registration, token exchange, and both credential modes — is in the TypeScript SDK:
Troubleshooting
Section titled “Troubleshooting”401 on every request
Section titled “401 on every request”- Verify
KEYCARD_ISSUERmatches your zone URL exactly (includinghttps://) - Check that your Worker’s URL matches the resource identifier in Keycard Console
Token exchange fails
Section titled “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”
Section titled “”Missing Keycard credentials in env””- Set either
KEYCARD_CLIENT_ID+KEYCARD_CLIENT_SECRETorKEYCARD_PRIVATE_KEYviawrangler secret put