#
tokens: 4479/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
1 | {
2 | 	"printWidth": 140,
3 | 	"singleQuote": true,
4 | 	"semi": true,
5 | 	"useTabs": true
6 | }
7 | 
```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
 1 | # http://editorconfig.org
 2 | root = true
 3 | 
 4 | [*]
 5 | indent_style = tab
 6 | end_of_line = lf
 7 | charset = utf-8
 8 | trim_trailing_whitespace = true
 9 | insert_final_newline = true
10 | 
11 | [*.yml]
12 | indent_style = space
13 | 
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | 
  3 | logs
  4 | _.log
  5 | npm-debug.log_
  6 | yarn-debug.log*
  7 | yarn-error.log*
  8 | lerna-debug.log*
  9 | .pnpm-debug.log*
 10 | 
 11 | # Diagnostic reports (https://nodejs.org/api/report.html)
 12 | 
 13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
 14 | 
 15 | # Runtime data
 16 | 
 17 | pids
 18 | _.pid
 19 | _.seed
 20 | \*.pid.lock
 21 | 
 22 | # Directory for instrumented libs generated by jscoverage/JSCover
 23 | 
 24 | lib-cov
 25 | 
 26 | # Coverage directory used by tools like istanbul
 27 | 
 28 | coverage
 29 | \*.lcov
 30 | 
 31 | # nyc test coverage
 32 | 
 33 | .nyc_output
 34 | 
 35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 36 | 
 37 | .grunt
 38 | 
 39 | # Bower dependency directory (https://bower.io/)
 40 | 
 41 | bower_components
 42 | 
 43 | # node-waf configuration
 44 | 
 45 | .lock-wscript
 46 | 
 47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 48 | 
 49 | build/Release
 50 | 
 51 | # Dependency directories
 52 | 
 53 | node_modules/
 54 | jspm_packages/
 55 | 
 56 | # Snowpack dependency directory (https://snowpack.dev/)
 57 | 
 58 | web_modules/
 59 | 
 60 | # TypeScript cache
 61 | 
 62 | \*.tsbuildinfo
 63 | 
 64 | # Optional npm cache directory
 65 | 
 66 | .npm
 67 | 
 68 | # Optional eslint cache
 69 | 
 70 | .eslintcache
 71 | 
 72 | # Optional stylelint cache
 73 | 
 74 | .stylelintcache
 75 | 
 76 | # Microbundle cache
 77 | 
 78 | .rpt2_cache/
 79 | .rts2_cache_cjs/
 80 | .rts2_cache_es/
 81 | .rts2_cache_umd/
 82 | 
 83 | # Optional REPL history
 84 | 
 85 | .node_repl_history
 86 | 
 87 | # Output of 'npm pack'
 88 | 
 89 | \*.tgz
 90 | 
 91 | # Yarn Integrity file
 92 | 
 93 | .yarn-integrity
 94 | 
 95 | # dotenv environment variable files
 96 | 
 97 | .env
 98 | .env.development.local
 99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 | 
103 | # parcel-bundler cache (https://parceljs.org/)
104 | 
105 | .cache
106 | .parcel-cache
107 | 
108 | # Next.js build output
109 | 
110 | .next
111 | out
112 | 
113 | # Nuxt.js build / generate output
114 | 
115 | .nuxt
116 | dist
117 | 
118 | # Gatsby files
119 | 
120 | .cache/
121 | 
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 | 
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 | 
126 | # public
127 | 
128 | # vuepress build output
129 | 
130 | .vuepress/dist
131 | 
132 | # vuepress v2.x temp and cache directory
133 | 
134 | .temp
135 | .cache
136 | 
137 | # Docusaurus cache and generated files
138 | 
139 | .docusaurus
140 | 
141 | # Serverless directories
142 | 
143 | .serverless/
144 | 
145 | # FuseBox cache
146 | 
147 | .fusebox/
148 | 
149 | # DynamoDB Local files
150 | 
151 | .dynamodb/
152 | 
153 | # TernJS port file
154 | 
155 | .tern-port
156 | 
157 | # Stores VSCode versions used for testing VSCode extensions
158 | 
159 | .vscode-test
160 | 
161 | # yarn v2
162 | 
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 | 
169 | # wrangler project
170 | 
171 | .dev.vars
172 | .wrangler/
173 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # workers-mcp-clerk
 2 | 
 3 | Talk to a Cloudflare Worker from Claude Desktop proxying Clerk protected API Routes or server actions.
 4 | 
 5 | ## How it works
 6 | 
 7 | 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.
 8 | 
 9 | ![seq-diagram](./seq-diagram.png)
10 | 
11 | ## How to use
12 | 
13 | 1. Deploy and install the Cloudflare MCP server to Claude Desktop using the instructions [here](https://github.com/cloudflare/workers-mcp).
14 | 2. Open Claude Desktop and type "Say hello to [email protected]"
15 | 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.
16 | 
```

--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------

```json
1 | {
2 | 	"files.associations": {
3 | 		"wrangler.json": "jsonc"
4 | 	}
5 | }
```

--------------------------------------------------------------------------------
/worker-configuration.d.ts:
--------------------------------------------------------------------------------

```typescript
1 | // Generated by Wrangler
2 | // After adding bindings to `wrangler.jsonc`, regenerate this interface via `npm run cf-typegen`
3 | interface Env {
4 | }
5 | 
```

--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------

```json
1 | {
2 | 	"extends": "../tsconfig.json",
3 | 	"compilerOptions": {
4 | 		"types": ["@cloudflare/workers-types/experimental", "@cloudflare/vitest-pool-workers"]
5 | 	},
6 | 	"include": ["./**/*.ts", "../worker-configuration.d.ts"],
7 | 	"exclude": []
8 | }
9 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "my-mcp-worker",
 3 |   "version": "0.0.0",
 4 |   "private": true,
 5 |   "scripts": {
 6 |     "deploy": "workers-mcp docgen src/index.ts && wrangler deploy",
 7 |     "dev": "workers-mcp docgen src/index.ts && wrangler dev",
 8 |     "start": "wrangler dev",
 9 |     "test": "vitest",
10 |     "cf-typegen": "wrangler types"
11 |   },
12 |   "devDependencies": {
13 |     "@cloudflare/vitest-pool-workers": "^0.6.4",
14 |     "@cloudflare/workers-types": "^4.20250214.0",
15 |     "typescript": "^5.5.2",
16 |     "vitest": "~2.1.9",
17 |     "wrangler": "^3.109.1"
18 |   },
19 |   "dependencies": {
20 |     "@clerk/backend": "^1.24.2",
21 |     "workers-mcp": "^0.0.13"
22 |   }
23 | }
24 | 
```

--------------------------------------------------------------------------------
/test/index.spec.ts:
--------------------------------------------------------------------------------

```typescript
 1 | // test/index.spec.ts
 2 | import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
 3 | import { describe, it, expect } from 'vitest';
 4 | import worker from '../src/index';
 5 | 
 6 | // For now, you'll need to do something like this to get a correctly-typed
 7 | // `Request` to pass to `worker.fetch()`.
 8 | const IncomingRequest = Request<unknown, IncomingRequestCfProperties>;
 9 | 
10 | describe('Hello World worker', () => {
11 | 	it('responds with Hello World! (unit style)', async () => {
12 | 		const request = new IncomingRequest('http://example.com');
13 | 		// Create an empty context to pass to `worker.fetch()`.
14 | 		const ctx = createExecutionContext();
15 | 		const response = await worker.fetch(request, env, ctx);
16 | 		// Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
17 | 		await waitOnExecutionContext(ctx);
18 | 		expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
19 | 	});
20 | 
21 | 	it('responds with Hello World! (integration style)', async () => {
22 | 		const response = await SELF.fetch('https://example.com');
23 | 		expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
24 | 	});
25 | });
26 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"compilerOptions": {
 3 | 		/* Visit https://aka.ms/tsconfig.json to read more about this file */
 4 | 
 5 | 		/* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
 6 | 		"target": "es2021",
 7 | 		/* Specify a set of bundled library declaration files that describe the target runtime environment. */
 8 | 		"lib": ["es2021"],
 9 | 		/* Specify what JSX code is generated. */
10 | 		"jsx": "react-jsx",
11 | 
12 | 		/* Specify what module code is generated. */
13 | 		"module": "es2022",
14 | 		/* Specify how TypeScript looks up a file from a given module specifier. */
15 | 		"moduleResolution": "Bundler",
16 | 		/* Specify type package names to be included without being referenced in a source file. */
17 | 		"types": [
18 | 			"@cloudflare/workers-types/2023-07-01"
19 | 		],
20 | 		/* Enable importing .json files */
21 | 		"resolveJsonModule": true,
22 | 
23 | 		/* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
24 | 		"allowJs": true,
25 | 		/* Enable error reporting in type-checked JavaScript files. */
26 | 		"checkJs": false,
27 | 
28 | 		/* Disable emitting files from a compilation. */
29 | 		"noEmit": true,
30 | 
31 | 		/* Ensure that each file can be safely transpiled without relying on other imports. */
32 | 		"isolatedModules": true,
33 | 		/* Allow 'import x from y' when a module doesn't have a default export. */
34 | 		"allowSyntheticDefaultImports": true,
35 | 		/* Ensure that casing is correct in imports. */
36 | 		"forceConsistentCasingInFileNames": true,
37 | 
38 | 		/* Enable all strict type-checking options. */
39 | 		"strict": true,
40 | 
41 | 		/* Skip type checking all .d.ts files. */
42 | 		"skipLibCheck": true
43 | 	},
44 | 	"exclude": ["test"],
45 | 	"include": ["worker-configuration.d.ts", "src/**/*.ts"]
46 | }
47 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { WorkerEntrypoint } from 'cloudflare:workers';
  2 | import { ProxyToSelf } from 'workers-mcp';
  3 | import { createClerkClient, type User } from "@clerk/backend";
  4 | 
  5 | const BASIC_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  6 | 
  7 | interface Env {
  8 |   CLERK_SECRET_KEY: string;
  9 | } 
 10 | 
 11 | export default class MyWorker extends WorkerEntrypoint<Env> {
 12 |   /**
 13 |    * A warm, friendly greeting from your new MCP server.
 14 |    * @param emailOrUserId {string} The email or userId of the person to greet.
 15 |    * @return {string} The greeting message.
 16 |    */
 17 |   async sayHello(email: string) {
 18 |     const jwt = await this.impersonate(email);
 19 | 
 20 |     // Use this JWT to make API calls to a Clerk protected API route.
 21 |     // For Demo purposes, we'll just log the JWT in the greeting message.
 22 |     return `Hello from an MCP Worker, ${email}! Your JWT is ${jwt}`;
 23 |   }
 24 | 
 25 |   /**
 26 |    * @ignore
 27 |    */
 28 |   async fetch(request: Request): Promise<Response> {
 29 |     // ProxyToSelf handles MCP protocol compliance.
 30 |     return new ProxyToSelf(this).fetch(request);
 31 |   }
 32 | 
 33 |   /**
 34 |    * @ignore
 35 |    */
 36 |   private async impersonate(emailOrUserId: string) {
 37 |     const user = await this.getUser(emailOrUserId);
 38 |   
 39 |     if (!user) {
 40 |       throw new Error(`User ${emailOrUserId} not found`);
 41 |     }
 42 | 
 43 |     // TODO: Open a PR to add this to @clerk/backend
 44 |     const actorTokenResponse = await fetch('https://api.clerk.com/v1/actor_tokens', {
 45 |       method: 'POST',
 46 |       headers: {
 47 |         Authorization: `Bearer ${this.getClerkSecretKey()}`,
 48 |         'Content-Type': 'application/json',
 49 |       },
 50 |       body: JSON.stringify({
 51 |         user_id: user.id,
 52 |         expires_in_seconds: 600,
 53 |         actor: {
 54 |           // TODO: Make this configurable
 55 |           sub: 'My Cloudflare MCP Server',
 56 |         },
 57 |       }),
 58 |     });
 59 |   
 60 |     if (!actorTokenResponse.ok) {
 61 |       throw new Error(`Failed to create actor token: ${actorTokenResponse.statusText}`);
 62 |     }
 63 |   
 64 |     const { token: ticket, url } = (await actorTokenResponse.json()) as { token: string; url: string };
 65 |   
 66 |     const clerkFrontendAPI = new URL(url).origin;
 67 |   
 68 |     // TODO: Open a PR to add this to @clerk/backend
 69 |     const signInResponse = await fetch(`${clerkFrontendAPI}/v1/client/sign_ins?__clerk_api_version=2024-10-01`, {
 70 |       method: 'POST',
 71 |       headers: {
 72 |         'content-type': 'application/x-www-form-urlencoded',
 73 |       },
 74 |       body: new URLSearchParams({
 75 |         strategy: 'ticket',
 76 |         ticket,
 77 |       }).toString(),
 78 |     });
 79 |   
 80 |     if (!signInResponse.ok) {
 81 |       throw new Error(`Failed to sign in: ${signInResponse.statusText}`);
 82 |     }
 83 |   
 84 |     const { client } = (await signInResponse.json()) as {
 85 |       client: { last_active_session_id: string; sessions: { id: string; last_active_token: { jwt: string } }[] };
 86 |     };
 87 |   
 88 |     const jwt = client.sessions.find((s) => s.id === client.last_active_session_id)?.last_active_token.jwt;
 89 |   
 90 |     if (!jwt) {
 91 |       throw new Error('Failed to get JWT');
 92 |     }
 93 |   
 94 |     return jwt;
 95 |   }
 96 | 
 97 |   /**
 98 |    * @ignore
 99 |    */
100 |   private async getUser(emailOrUserId: string): Promise<User> {
101 |     console.log("Getting user: ", emailOrUserId, this.getClerkSecretKey());
102 |     const clerk = createClerkClient({
103 |       secretKey: this.getClerkSecretKey()
104 |     });
105 |     
106 |     try {
107 |       if ((emailOrUserId || '').startsWith('user_')) {
108 |         return await clerk.users.getUser(emailOrUserId);
109 |       }
110 | 
111 |       if (BASIC_EMAIL_REGEX.test(emailOrUserId || '')) {
112 |         const users = await clerk.users.getUserList({
113 |           emailAddress: [emailOrUserId],
114 |           orderBy: '-last_sign_in_at',
115 |         });
116 |       
117 |         return users.data[0];
118 |       }
119 |     } catch (error) {
120 |       console.error("Error getting user: ", error);
121 |     }
122 | 
123 |     throw new Error(`Invalid user ID or email: ${emailOrUserId}. Please provider a user ID or email address.`);
124 |   }
125 |   
126 |   /**
127 |    * @ignore
128 |    */
129 |   private getClerkSecretKey(): string {
130 |     if (!this.env.CLERK_SECRET_KEY) {
131 |       throw new Error('CLERK_SECRET_KEY is not set');
132 |     }
133 |     return this.env.CLERK_SECRET_KEY;
134 |   }
135 |  
136 | }
```