# Directory Structure ``` ├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode │ └── settings.json ├── package-lock.json ├── package.json ├── README.md ├── seq-diagram.png ├── src │ └── index.ts ├── test │ ├── index.spec.ts │ └── tsconfig.json ├── tsconfig.json ├── vitest.config.mts ├── worker-configuration.d.ts └── wrangler.jsonc ``` # Files -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- ``` { "printWidth": 140, "singleQuote": true, "semi": true, "useTabs": true } ``` -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- ``` # http://editorconfig.org root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Logs logs _.log npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data pids _.pid _.seed \*.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage \*.lcov # nyc test coverage .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ # TypeScript cache \*.tsbuildinfo # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional stylelint cache .stylelintcache # Microbundle cache .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history .node_repl_history # Output of 'npm pack' \*.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variable files .env .env.development.local .env.test.local .env.production.local .env.local # parcel-bundler cache (https://parceljs.org/) .cache .parcel-cache # Next.js build output .next out # Nuxt.js build / generate output .nuxt dist # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js # https://nextjs.org/blog/next-9-1#public-directory-support # public # vuepress build output .vuepress/dist # vuepress v2.x temp and cache directory .temp .cache # Docusaurus cache and generated files .docusaurus # Serverless directories .serverless/ # FuseBox cache .fusebox/ # DynamoDB Local files .dynamodb/ # TernJS port file .tern-port # Stores VSCode versions used for testing VSCode extensions .vscode-test # yarn v2 .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.\* # wrangler project .dev.vars .wrangler/ ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # workers-mcp-clerk Talk to a Cloudflare Worker from Claude Desktop proxying Clerk protected API Routes or server actions. ## How it works For every application that uses Clerk for authentication, you can create a Cloudflare Worker that acts as an MPC Server and impersonates a Clerk user.  ## How to use 1. Deploy and install the Cloudflare MCP server to Claude Desktop using the instructions [here](https://github.com/cloudflare/workers-mcp). 2. Open Claude Desktop and type "Say hello to [email protected]" 3. For demo purposes, the Cloudflare Worker will impersonate the Clerk user, return with a greeting and a user JWT. In a real scenario, the JWT will be used to request Clerk protected API Route or server actions. ``` -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- ```json { "files.associations": { "wrangler.json": "jsonc" } } ``` -------------------------------------------------------------------------------- /worker-configuration.d.ts: -------------------------------------------------------------------------------- ```typescript // Generated by Wrangler // After adding bindings to `wrangler.jsonc`, regenerate this interface via `npm run cf-typegen` interface Env { } ``` -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- ```json { "extends": "../tsconfig.json", "compilerOptions": { "types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"] }, "include": ["./**/*.ts", "../worker-configuration.d.ts"], "exclude": [] } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "my-mcp-worker", "version": "0.0.0", "private": true, "scripts": { "deploy": "workers-mcp docgen src/index.ts && wrangler deploy", "dev": "workers-mcp docgen src/index.ts && wrangler dev", "start": "wrangler dev", "test": "vitest", "cf-typegen": "wrangler types" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.6.4", "@cloudflare/workers-types": "^4.20250214.0", "typescript": "^5.5.2", "vitest": "~2.1.9", "wrangler": "^3.109.1" }, "dependencies": { "@clerk/backend": "^1.24.2", "workers-mcp": "^0.0.13" } } ``` -------------------------------------------------------------------------------- /test/index.spec.ts: -------------------------------------------------------------------------------- ```typescript // test/index.spec.ts import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; import { describe, it, expect } from 'vitest'; import worker from '../src/index'; // For now, you'll need to do something like this to get a correctly-typed // `Request` to pass to `worker.fetch()`. const IncomingRequest = Request<unknown, IncomingRequestCfProperties>; describe('Hello World worker', () => { it('responds with Hello World! (unit style)', async () => { const request = new IncomingRequest('http://example.com'); // Create an empty context to pass to `worker.fetch()`. const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions await waitOnExecutionContext(ctx); expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); }); it('responds with Hello World! (integration style)', async () => { const response = await SELF.fetch('https://example.com'); expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`); }); }); ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2021", /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "lib": ["es2021"], /* Specify what JSX code is generated. */ "jsx": "react-jsx", /* Specify what module code is generated. */ "module": "es2022", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "Bundler", /* Specify type package names to be included without being referenced in a source file. */ "types": [ "@cloudflare/workers-types/2023-07-01" ], /* Enable importing .json files */ "resolveJsonModule": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ "allowJs": true, /* Enable error reporting in type-checked JavaScript files. */ "checkJs": false, /* Disable emitting files from a compilation. */ "noEmit": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ "isolatedModules": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "allowSyntheticDefaultImports": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true, /* Enable all strict type-checking options. */ "strict": true, /* Skip type checking all .d.ts files. */ "skipLibCheck": true }, "exclude": ["test"], "include": ["worker-configuration.d.ts", "src/**/*.ts"] } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript import { WorkerEntrypoint } from 'cloudflare:workers'; import { ProxyToSelf } from 'workers-mcp'; import { createClerkClient, type User } from "@clerk/backend"; const BASIC_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; interface Env { CLERK_SECRET_KEY: string; } export default class MyWorker extends WorkerEntrypoint<Env> { /** * A warm, friendly greeting from your new MCP server. * @param emailOrUserId {string} The email or userId of the person to greet. * @return {string} The greeting message. */ async sayHello(email: string) { const jwt = await this.impersonate(email); // Use this JWT to make API calls to a Clerk protected API route. // For Demo purposes, we'll just log the JWT in the greeting message. return `Hello from an MCP Worker, ${email}! Your JWT is ${jwt}`; } /** * @ignore */ async fetch(request: Request): Promise<Response> { // ProxyToSelf handles MCP protocol compliance. return new ProxyToSelf(this).fetch(request); } /** * @ignore */ private async impersonate(emailOrUserId: string) { const user = await this.getUser(emailOrUserId); if (!user) { throw new Error(`User ${emailOrUserId} not found`); } // TODO: Open a PR to add this to @clerk/backend const actorTokenResponse = await fetch('https://api.clerk.com/v1/actor_tokens', { method: 'POST', headers: { Authorization: `Bearer ${this.getClerkSecretKey()}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: user.id, expires_in_seconds: 600, actor: { // TODO: Make this configurable sub: 'My Cloudflare MCP Server', }, }), }); if (!actorTokenResponse.ok) { throw new Error(`Failed to create actor token: ${actorTokenResponse.statusText}`); } const { token: ticket, url } = (await actorTokenResponse.json()) as { token: string; url: string }; const clerkFrontendAPI = new URL(url).origin; // TODO: Open a PR to add this to @clerk/backend const signInResponse = await fetch(`${clerkFrontendAPI}/v1/client/sign_ins?__clerk_api_version=2024-10-01`, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ strategy: 'ticket', ticket, }).toString(), }); if (!signInResponse.ok) { throw new Error(`Failed to sign in: ${signInResponse.statusText}`); } const { client } = (await signInResponse.json()) as { client: { last_active_session_id: string; sessions: { id: string; last_active_token: { jwt: string } }[] }; }; const jwt = client.sessions.find((s) => s.id === client.last_active_session_id)?.last_active_token.jwt; if (!jwt) { throw new Error('Failed to get JWT'); } return jwt; } /** * @ignore */ private async getUser(emailOrUserId: string): Promise<User> { console.log("Getting user: ", emailOrUserId, this.getClerkSecretKey()); const clerk = createClerkClient({ secretKey: this.getClerkSecretKey() }); try { if ((emailOrUserId || '').startsWith('user_')) { return await clerk.users.getUser(emailOrUserId); } if (BASIC_EMAIL_REGEX.test(emailOrUserId || '')) { const users = await clerk.users.getUserList({ emailAddress: [emailOrUserId], orderBy: '-last_sign_in_at', }); return users.data[0]; } } catch (error) { console.error("Error getting user: ", error); } throw new Error(`Invalid user ID or email: ${emailOrUserId}. Please provider a user ID or email address.`); } /** * @ignore */ private getClerkSecretKey(): string { if (!this.env.CLERK_SECRET_KEY) { throw new Error('CLERK_SECRET_KEY is not set'); } return this.env.CLERK_SECRET_KEY; } } ```