Content
# Workers MCP Server
> **Talk to your Cloudflare Workers from Claude Desktop!**
## NOTE: This has now been superseded by the [Workers MCP]([https://github.com/geelen/workers-mcp](https://github.com/cloudflare/workers-mcp)) package. Go there instead.
This is a proof-of-concept of writing a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) Server in a Cloudflare Worker. This gives you a way to extend Claude Desktop (among other MCP clients) by invoking functions using Cloudflare Worker's [new RPC syntax](https://blog.cloudflare.com/javascript-native-rpc/), which gives you access to any Cloudflare or third-party binding.
You write worker code that looks like this:
```ts
export class ExampleWorkerMCP extends WorkerEntrypoint<Env> {
/**
* Generates a random number. This is extra random because it had to travel all the way to
* your nearest Cloudflare PoP to be calculated which... something something lava lamps?
*
* @return {string} A message containing a super duper random number
* */
async getRandomNumber() {
return `Your random number is ${Math.random()}`
}
}
```
And, using the provided MCP proxy, your Claude Desktop can see & invoke these messages:

> <sub>Yes, I know that `Math.random()` works the same on a Worker as it does on your local machine, but don't tell Claude</sub> 🤫
## Neat! How do I play?
1. **Download Claude Desktop** https://claude.ai/download
2. **Clone this repo.**
3. **`pnpm install`**
4. **Check `wrangler.json`**<br/>The current demo uses both the [Email Routing](https://developers.cloudflare.com/email-routing/) API and [Browser Rendering](https://developers.cloudflare.com/browser-rendering/. If you don't have access to these, or they're not enabled, comment out the relevant sections in `wrangler.json` or your deploy will fail.
5. **`pnpm deploy:worker`**<br/>This takes your `src/index.ts` file and generate `dist/docs.json` from it, then deploys it using Wrangler.
6. **`npx workers-mcp secret generate && npx workers-mcp secret upload`**<br/>This generates a secret in `.dev.vars` and uploads it using `wrangler secret put`. You only need to this once.
7. **`npx workers-mcp install <server-alias> <worker-url>`**
8. **Restart Claude Desktop** You have to do this pretty often, but you _definitely_ have to do it after running the install step above.
To iterate on your server, do the following:
1. Make your change to `src/index.ts`
2. **`pnpm deploy:worker`**
3. (usually) **Restart Claude Desktop**.<br/>You have to do this whenever you add/remove/change your methods or any of the documentation (Claude doesn't detect changes while it's running). But if you're just updating the code within a method then `pnpm deploy:worker` is enough.
---
## NOTE: Specifics below are now outdated
These are replaced with relevant sections in [Workers MCP](https://github.com/geelen/workers-mcp) package.
---
## How it works
Separately to your MCP code inside `src/index.ts`, there are three pieces required to make this work:
### 1. Docs generation: `scripts/generate-docs.ts`
The [MCP Specification](https://spec.modelcontextprotocol.io/specification/server/tools/#listing-tools) separates the `tools/list` and `tools/call` operations into separate steps, and most MCP servers have naturally followed suit and separated their schema definition from the implementation. However, combining them provides a much better DX for the author.
I'm using [ts-blank-space](https://github.com/bloomberg/ts-blank-space) and [jsdoc-api](https://www.npmjs.com/package/jsdoc-api) to parse the TS and emit the schema, slightly tweaked. This gives you LLM-friendly documentation at build time:
```ts
/**
* Send a text or HTML email to an arbitrary recipient.
*
* @param {string} recipient - The email address of the recipient.
* @param {string} subject - The subject of the email.
* @param {string} contentType - The content type of the email. Can be text/plain or text/html
* @param {string} body - The body of the email. Must match the provided contentType parameter
* @return {Promise<string>} A success message.
* @throws {Error} If the email fails to send, or if that destination email address hasn't been verified.
*/
async sendEmail(recipient: string, subject: string, contentType: string, body: string) {
// ...
}
```
```json
{
"ExampleWorkerMCP": {
"exported_as": "ExampleWorkerMCP",
"description": null,
"methods": [
{
"name": "sendEmail",
"description": "Send a text or HTML email to an arbitrary recipient.",
"params": [
{
"description": "The email address of the recipient.",
"name": "recipient",
"type": "string"
},
{
"description": "The subject of the email.",
"name": "subject",
"type": "string"
},
{
"description": "The content type of the email. Can be text/plain or text/html",
"name": "contentType",
"type": "string"
},
{
"description": "The body of the email. Must match the provided contentType parameter",
"name": "body",
"type": "string"
}
],
"returns": {
"description": "A success message.",
"type": "Promise.<string>"
}
}
]
}
}
```
This list of methods is very similar to the required MCP format for `tools/list`, but also gives us a list of the `WorkerEntrypoint` exports names to look up our service bindings later.
To iterate on your docs, run `pnpm generate:docs:watch` and you'll see the output change as you tweak your JSDoc in your `src/index.ts` (you'll need [watchexec](https://github.com/watchexec/watchexec) installed).
## 2. Public HTTP handler: `lib/WorkerMCP.ts`
Since our `WorkerEntrypoint` is not directly accessible, we need something that defines a default export with a `fetch()` handler. This is what `lib/WorkerMCP.ts` does.
This exposes a single endpoint, `/rpc`, which takes a JSON payload of `{ method: string, args?: any[] }`, then calls that method on your `WorkerEntrypoint`.
### 3. Local MCP proxy: `scripts/local-proxy.ts`
This file uses the `@modelcontextprotocol/sdk` library to build up a normal, local MCP server. This responds to `tools/list` by producing the data from `docs.json` for the specified entrypoint.
On `tools/call`, a `.fetch` call is made to the remote worker on the `/rpc` route, providing a `Bearer` token with the contents of `generated/.shared-secret`. The responses are then piped back to Claude.
Calling `pnpm install:claude <server-alias> <worker-url>` adds a sever definition that points to this file in your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"<server-alias>": {
"command": "<absolute-path-to>/node",
"args": [
"<project-dir>/node_modules/tsx/dist/cli.mjs",
"<project-dir>/scripts/local-proxy.ts",
"<server-alias>",
"<worker-url>",
"<entrypoint-name>"
]
}
}
}
```
In this way you can install as many of these as you like, as long as they each have a distinct `<server-alias>`.
## Limitations
There are lots. This pizza is straight out of the oven. You may well burn your mouth.
1. `docs.json` is only generated from `src/index.ts`. It doesn't currently crawl imports like a bundler, because no bundler I could find preserved comments in-place in order for me to run the docs generator afterwards.
2. Documentation generation only works for class exports. It can, at least, parse `class X {}; export { X as Y }`, but in general most people do `export default class X {}` anyway so this is fine for now.
3. The local proxy <-> remote proxy communication doesn't follow any particular RPC spec, but it probably should.
4. Error handling, non-text return values, streaming responses, etc have not really been thought through but do sorta work.
5. No `wrangler dev` support yet, but `wrangler dev --remote` should be possible so you don't have to deploy so often
6. Following on from the above, the spec includes a [`notifications/tools/list_changed` notification](https://spec.modelcontextprotocol.io/specification/server/tools/#list-changed-notification) that should trigger Claude to refresh its list of the tools available, meaning fewer restarts of Claude Desktop. But I haven't implemented that yet.
7. The docs parsing doesn't yet use TS types to either augment or replace the need for `@param` blocks in the JSDoc
8. The doc generation might be completely superfluous if someone was using a validator like zod or a schema library like typebox. However, I wanted build-time docs generation (i.e. static extraction) and wanted to be as generic as possible, so JSDoc will do for now.
## Future ideas
Obviously, having Claude Desktop talk directly to the Worker would be ideal. Also, `wrangler dev --remote` support would be great: you could iterate on your worker without redeploying, but still access your production bindings.
The docs generator needs to be extracted into a library so we can publish changes, as it needs to grow in scope to be really useful, and likely incorporate other sources of data (`d.ts` files, zod schemas, etc).
## Feedback & Contributions
Give it a try! Then, raise an issue or send a PR . This is all very new, so it could really go in a lot of different directions. We'd love to hear from you!