#
tokens: 3139/50000 11/11 files
lines: off (toggle) GitHub
raw markdown copy
# 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.

![seq-diagram](./seq-diagram.png)

## 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;
  }
 
}
```