18 KiB
MCP Cloudflare Worker Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Deploy the Autumn MCP endpoint currently served through apps/leaf as a dedicated Cloudflare Worker while preserving the existing /mcp and OAuth protected-resource contract.
Architecture: Add a new apps/mcp-worker Worker that imports @autumn/mcp, exposes GET /.well-known/oauth-protected-resource/mcp, ALL /mcp, and GET /health, and calls the Autumn API through fetch. Keep server as the public proxy/control plane initially, then switch MCP_SERVER_URL / CHAT_SERVER_URL to the Worker once live smoke tests pass.
Tech Stack: Cloudflare Workers, Wrangler JSONC, Hono, Mastra MCP, @modelcontextprotocol/sdk Streamable HTTP, Bun workspace scripts, Autumn OAuth issuer from server.
Global Constraints
- Do not migrate all of
apps/leaf; the Worker scope is only hosted MCP. - Keep
packages/mcpusable byapps/leaftests and evals. - Do not hardcode secrets in
wrangler.jsonc; usewrangler secret put. - Generate Worker env types with Wrangler instead of hand-writing binding types.
- Use Cloudflare-compatible crypto and schema validation; avoid
node:fs,node:http,ioredis, andprocess.envin Worker request paths. - Keep
server/src/routers/chatProxyRouter.tsbehavior intact until the new Worker endpoint is live. - Validate locally, dry-run bundle, then deploy and smoke test the real
/mcpendpoint.
File Structure
- Create
apps/mcp-worker/package.json: Worker package scripts and dependencies. - Create
apps/mcp-worker/wrangler.jsonc: Worker name, entrypoint, compatibility date,nodejs_compat, non-secret vars, observability, optional custom route. - Create
apps/mcp-worker/src/index.ts: Workerfetchentry, Hono app, route registration. - Create
apps/mcp-worker/src/env.ts: parse Cloudflare bindings and vars fromenv, notprocess.env. - Create
apps/mcp-worker/src/auth.ts: Worker-native copy/extraction of the current MCP auth flow fromapps/leaf/src/mcp/auth. - Create
apps/mcp-worker/src/protectedResourceMetadata.ts: Worker route for OAuth protected resource metadata. - Create
apps/mcp-worker/src/mcpHandler.ts: Worker-native HTTP handler for/mcp. - Create
apps/mcp-worker/tests/mcp-worker.test.ts: Worker route/auth smoke tests usingapp.requestorunstable_dev. - Modify
package.json: add workspaceapps/mcp-worker; add root scripts such asmcp:cf:dev,mcp:cf:check,mcp:cf:deploy. - Modify
packages/mcp/src/server/server.ts: accept optional Cloudflare JSON schema validator and resource/instruction inputs. - Modify
packages/mcp/src/resources-v2/index.tsandpackages/mcp/src/resources-v2/mcpInstructions.ts: replace Worker runtime file reads with injectable/generated static text. - Modify
packages/mcp/src/agent/pending-actions.ts: abstract pending-action storage so Worker can use KV or Durable Object instead of Redis. - Modify
packages/mcp/src/analytics/analyticsSink.ts,packages/mcp/src/agent/axiom.ts, and related utilities: replace directprocess.envreads with injectable config. - Modify
server/wrangler.jsonconly after Worker deploy: setCHAT_SERVER_URLandMCP_SERVER_URLto the deployed Worker origin, or route/mcpdirectly to the Worker custom domain.
Task 1: Worker Runtime Audit Gate
Files:
- Read:
packages/mcp/src/** - Read:
apps/leaf/src/mcp/** - Create:
apps/mcp-worker/tests/runtime-compatibility.test.ts
Interfaces:
-
Produces: a test that fails while Worker-incompatible imports remain in the Worker bundle.
-
Step 1: Add a static compatibility test
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const workerReachableFiles = [
"packages/mcp/src/server/server.ts",
"packages/mcp/src/resources-v2/index.ts",
"packages/mcp/src/resources-v2/mcpInstructions.ts",
"packages/mcp/src/agent/pending-actions.ts",
"packages/mcp/src/analytics/analyticsSink.ts",
"packages/mcp/src/agent/axiom.ts",
];
describe("mcp Worker runtime compatibility", () => {
test("Worker bundle does not depend on Node-only runtime APIs", () => {
const forbidden = [/from "node:fs"/, /from "node:http"/, /from "ioredis"/, /process\.env/];
const offenders = workerReachableFiles.flatMap((file) => {
const text = readFileSync(join(process.cwd(), file), "utf8");
return forbidden
.filter((pattern) => pattern.test(text))
.map((pattern) => `${file}: ${pattern}`);
});
expect(offenders).toEqual([]);
});
});
- Step 2: Run the failing gate
Run: cd /Volumes/sker/resources/coding/comi-logic/autumn && bun test apps/mcp-worker/tests/runtime-compatibility.test.ts
Expected: FAIL before compatibility refactors, naming the current node:fs, ioredis, and process.env offenders.
- Step 3: Commit after the test exists
git add apps/mcp-worker/tests/runtime-compatibility.test.ts
git commit -m "test: add mcp worker runtime compatibility gate"
Task 2: Make @autumn/mcp Configurable For Workers
Files:
- Modify:
packages/mcp/src/server/server.ts - Modify:
packages/mcp/src/resources-v2/index.ts - Modify:
packages/mcp/src/resources-v2/mcpInstructions.ts - Modify:
packages/mcp/src/analytics/analyticsSink.ts - Modify:
packages/mcp/src/agent/axiom.ts - Test:
packages/mcp/tests/unit/mcp-server/agent/server.test.ts
Interfaces:
-
Produces:
createAutumnOperationsMCPServer(options?: AutumnMcpServerOptions). -
Produces:
AutumnMcpRuntimeConfigwithaxiomToken,axiomOrgId,analyticsDataset,nodeEnv, andpendingActionStore. -
Step 1: Introduce options
Add an exported type:
export type AutumnMcpServerOptions = {
resources?: MCPServerResources;
instructions?: string;
jsonSchemaValidator?: ConstructorParameters<typeof MCPServer>[0]["jsonSchemaValidator"];
runtime?: AutumnMcpRuntimeConfig;
};
Update createAutumnOperationsMCPServer to pass:
jsonSchemaValidator: options?.jsonSchemaValidator,
instructions: options?.instructions ?? autumnMcpInstructions,
resources: options?.resources ?? autumnMcpResources,
- Step 2: Replace direct runtime env reads
Move process.env.* reads behind a runtime config getter. For Node callers, default from process.env; for Worker callers, pass the config from apps/mcp-worker/src/env.ts.
- Step 3: Keep Node resource behavior
Keep the current disk-based development behavior for apps/leaf, but add a second export:
export const createStaticAutumnMcpResources = (docs: AutumnMcpResourceDoc[]): MCPServerResources => ({
listResources: async () => docs.map((doc) => ({
uri: doc.uri,
name: doc.name,
title: doc.title,
description: doc.description,
mimeType: "text/markdown",
size: doc.text.length,
annotations: { audience: doc.audience, priority: doc.priority },
})),
getResourceContent: async ({ uri }) => {
const doc = docs.find((entry) => entry.uri === uri);
if (!doc) throw new Error(`Unknown Autumn MCP resource: ${uri}`);
return { text: doc.text };
},
});
- Step 4: Run package tests
Run: cd /Volumes/sker/resources/coding/comi-logic/autumn && bun -F @autumn/mcp test
Expected: PASS.
- Step 5: Re-run compatibility gate
Run: cd /Volumes/sker/resources/coding/comi-logic/autumn && bun test apps/mcp-worker/tests/runtime-compatibility.test.ts
Expected: PASS after Worker-incompatible imports are isolated from the Worker path.
Task 3: Generate Static MCP Resource Bundle
Files:
- Create:
packages/mcp/scripts/build-worker-resources.ts - Create:
packages/mcp/src/resources-v2/generated/workerResources.ts - Modify:
packages/mcp/package.json
Interfaces:
-
Produces:
workerAutumnMcpInstructions: string. -
Produces:
workerAutumnMcpResourceDocs: AutumnMcpResourceDoc[]. -
Step 1: Add generator
The generator imports existing compile helpers in Bun/Node, reads markdown, and writes a TypeScript module that exports plain strings/objects.
- Step 2: Add script
In packages/mcp/package.json:
"build:worker-resources": "bun scripts/build-worker-resources.ts"
- Step 3: Validate generated file
Run: cd /Volumes/sker/resources/coding/comi-logic/autumn && bun -F @autumn/mcp build:worker-resources && bun -F @autumn/mcp ts
Expected: generated file compiles and contains no node:fs import.
Task 4: Add Dedicated Cloudflare Worker App
Files:
- Create:
apps/mcp-worker/package.json - Create:
apps/mcp-worker/wrangler.jsonc - Create:
apps/mcp-worker/src/index.ts - Create:
apps/mcp-worker/src/env.ts - Modify:
package.json
Interfaces:
-
Produces: Worker routes
GET /health,GET /.well-known/oauth-protected-resource/mcp, andALL /mcp. -
Consumes:
createAutumnOperationsMCPServer,createStaticAutumnMcpResources,workerAutumnMcpResourceDocs,workerAutumnMcpInstructions. -
Step 1: Add package scripts
Use scripts:
{
"name": "@autumn/mcp-worker",
"private": true,
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"types": "wrangler types",
"check": "bun run types && tsc --noEmit",
"test": "bun test tests"
}
}
- Step 2: Add Worker config
wrangler.jsonc should include:
{
"name": "autumn-mcp",
"main": "src/index.ts",
"compatibility_date": "2026-07-14",
"compatibility_flags": ["nodejs_compat"],
"observability": { "enabled": true, "head_sampling_rate": 1 },
"vars": {
"BETTER_AUTH_URL": "https://api.useautumn.com",
"MCP_SERVER_URL": "https://mcp.useautumn.com",
"MCP_OAUTH_ENVIRONMENT": "sandbox",
"AUTUMN_API_URL": "https://api.useautumn.com"
}
}
Secrets to set with Wrangler: AXIOM_TOKEN, AXIOM_ORG_ID, and any future secret-only MCP config.
- Step 3: Add Worker entry
src/index.ts creates a Hono app and exports:
export default {
fetch: app.fetch,
} satisfies ExportedHandler<Env>;
- Step 4: Register workspace
Add apps/mcp-worker to root package.json workspaces and root scripts:
"mcp:cf:dev": "bun -F @autumn/mcp-worker dev",
"mcp:cf:check": "bun -F @autumn/mcp-worker check",
"mcp:cf:deploy": "bun -F @autumn/mcp-worker deploy"
Task 5: Port Auth And Protected Resource Metadata
Files:
- Create:
apps/mcp-worker/src/auth.ts - Create:
apps/mcp-worker/src/protectedResourceMetadata.ts - Test:
apps/mcp-worker/tests/auth.test.ts
Interfaces:
-
Produces:
buildAuthForWorkerRequest(request, env): Promise<AutumnMcpAuth>. -
Produces:
getWorkerProtectedResourceMetadata(env): object. -
Step 1: Port from leaf without Node types
Copy the behavior from apps/leaf/src/mcp/auth/resolveRequestAuth.ts, but use Web Request, Headers, and crypto.subtle.digest or crypto.randomUUID where needed.
- Step 2: Preserve responses
Unauthenticated /mcp must return 401 with WWW-Authenticate pointing to /.well-known/oauth-protected-resource/mcp.
- Step 3: Test static key and OAuth token paths
Run: cd /Volumes/sker/resources/coding/comi-logic/autumn && bun -F @autumn/mcp-worker test
Expected: PASS for secret-key, OAuth bearer prefix, missing bearer, invalid env, and protected-resource metadata.
Task 6: Implement Worker MCP HTTP Handler
Files:
- Create:
apps/mcp-worker/src/mcpHandler.ts - Modify:
apps/mcp-worker/src/index.ts - Test:
apps/mcp-worker/tests/mcp-worker.test.ts
Interfaces:
-
Produces:
handleMcpRequest(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>. -
Step 1: Build per-request auth
Call buildAuthForWorkerRequest, then pass the auth to Mastra via extra.authInfo / RequestContext in the same shape expected by packages/mcp/src/server/auth/auth.ts.
- Step 2: Use Cloudflare validator
Create the server with:
import { CfWorkerJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/cfworker";
const server = createAutumnOperationsMCPServer({
jsonSchemaValidator: new CfWorkerJsonSchemaValidator(),
instructions: workerAutumnMcpInstructions,
resources: createStaticAutumnMcpResources(workerAutumnMcpResourceDocs),
runtime,
});
- Step 3: Use stateless/serverless HTTP
Prefer Mastra startHTTP serverless mode if it accepts Web Request/Response in practice. If the current type/runtime still requires Node IncomingMessage/ServerResponse, replace the transport layer with the underlying @modelcontextprotocol/sdk/server/streamableHttp Web-compatible transport, keeping existing tools/resources unchanged.
- Step 4: Test MCP initialize
Send JSON-RPC:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0.1"}}}
Expected: HTTP 200 and MCP server info for Autumn MCP.
Task 7: Replace Redis Pending Actions
Files:
- Modify:
packages/mcp/src/agent/pending-actions.ts - Create:
apps/mcp-worker/src/pendingActionStore.ts - Modify:
apps/mcp-worker/wrangler.jsonc - Test:
packages/mcp/tests/unit/mcp-server/agent/pending-actions.test.ts
Interfaces:
-
Produces:
PendingActionStorewithcreate,claimLatest,getLatest,clear. -
Step 1: Abstract storage
Keep Redis as the Node default for leaf. Add a Worker store implementation backed by KV for the 15-minute pending action TTL.
- Step 2: Add KV binding
Add to wrangler.jsonc:
"kv_namespaces": [
{ "binding": "MCP_PENDING_ACTIONS", "id": "<created-with-wrangler>" }
]
- Step 3: Validate claim semantics
Because KV is eventually consistent, confirm whether double-claim is acceptable for the current billing confirmation flow. If not acceptable, switch this task to a Durable Object store before production deploy.
Task 8: Verification And Deploy
Files:
- Modify:
server/wrangler.jsoncafter deploy only. - Read:
apps/mcp-worker/wrangler.jsonc
Interfaces:
-
Produces: live Worker endpoint.
-
Step 1: Generate types and check
Run:
cd /Volumes/sker/resources/coding/comi-logic/autumn
bun -F @autumn/mcp-worker check
bun -F @autumn/mcp test
bun -F @autumn/leaf ts
Expected: all pass.
- Step 2: Dry-run bundle
Run:
cd /Volumes/sker/resources/coding/comi-logic/autumn/apps/mcp-worker
bunx wrangler deploy --dry-run --outdir dist-dry-run
Expected: bundle succeeds and does not include node:fs, node:http, or ioredis in the Worker path.
- Step 3: Set secrets
Run:
cd /Volumes/sker/resources/coding/comi-logic/autumn/apps/mcp-worker
bunx wrangler secret put AXIOM_TOKEN
bunx wrangler secret put AXIOM_ORG_ID
Expected: Wrangler confirms each secret was uploaded.
- Step 4: Deploy
Run:
cd /Volumes/sker/resources/coding/comi-logic/autumn
bun run mcp:cf:deploy
Expected: Wrangler returns a deployed Worker version and route.
- Step 5: Live smoke
Run:
curl -i https://<mcp-worker-host>/health
curl -i https://<mcp-worker-host>/.well-known/oauth-protected-resource/mcp
curl -i https://<mcp-worker-host>/mcp
Expected: /health returns 200, metadata returns 200 JSON, unauthenticated /mcp returns 401 with WWW-Authenticate.
Task 9: Cut Over Existing Autumn Server Proxy
Files:
- Modify:
server/wrangler.jsonc - Optionally modify: deployment environment vars for
server
Interfaces:
-
Consumes: deployed Worker origin.
-
Produces: existing
server/mcpproxy routes target the Cloudflare Worker instead of localhost/leaf. -
Step 1: Change target vars
Set:
"CHAT_SERVER_URL": "https://<mcp-worker-host>",
"MCP_SERVER_URL": "https://<mcp-worker-host>"
If Slack remains on leaf, do not reuse CHAT_SERVER_URL; split server/src/routers/chatProxyRouter.ts into CHAT_SERVER_URL for Slack and MCP_UPSTREAM_URL for MCP before changing production config.
- Step 2: Deploy server
Run:
cd /Volumes/sker/resources/coding/comi-logic/autumn/server
bun run cf:deploy
Expected: deploy succeeds.
- Step 3: Smoke through existing public origin
Run:
curl -i https://autumn-api.bowong.cc/.well-known/oauth-protected-resource/mcp
curl -i https://autumn-api.bowong.cc/mcp
Expected: metadata reflects the intended MCP resource URL, and unauthenticated /mcp returns the correct OAuth challenge.
Task 10: Production Acceptance
Files:
- Test only unless smoke reveals defects.
Interfaces:
-
Produces: a deployed, externally reachable MCP endpoint for Claude/Codex-style MCP clients.
-
Step 1: Run JSON-RPC initialize against live endpoint
Use a bearer test credential or OAuth token and post an initialize request to /mcp.
Expected: server identifies as Autumn MCP.
- Step 2: List tools
Post tools/list.
Expected: core Autumn tools are present: customer, plan, billing, balance, logs, and organization tools.
- Step 3: Run a read-only tool
Call organizationMe or equivalent read-only org tool.
Expected: returns data scoped to the authenticated Autumn org and environment.
- Step 4: Confirm observability
Check Cloudflare Worker logs and Axiom MCP analytics.
Expected: one structured request log and one MCP tool analytics event for the smoke request.
Rollback
- Keep
apps/leafdeployment unchanged until Task 10 passes. - If Worker deploy fails, keep
servervars pointing to current leaf/local target. - If cutover fails after Task 9, restore previous
CHAT_SERVER_URL/MCP_SERVER_URLvalues inserver/wrangler.jsoncand redeployserver. - If billing confirmation double-claim appears during smoke, disable billing write tools in Worker until Pending Actions moves from KV to Durable Object.
Notes From Current Audit
packages/mcp/src/resources-v2/index.tsandmcpInstructions.tsusereadFileSync; this must not run in the Worker bundle.packages/mcp/src/agent/pending-actions.tsusesioredis; Worker needs KV or Durable Object.apps/leaf/src/mcp/handlers/handleMcp.tsdepends on@hono/node-serverrequest/response bindings; it is not Worker-native.@mastra/mcpdocumentsCfWorkerJsonSchemaValidatorfor Cloudflare Workers.serveralready exposes/mcpand protected-resource metadata as a proxy, so cutover can be config-first after the new Worker is live.