Files
cfw-autumn/docs/superpowers/plans/2026-07-14-mcp-cloudflare-worker.md

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/mcp usable by apps/leaf tests and evals.
  • Do not hardcode secrets in wrangler.jsonc; use wrangler 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, and process.env in Worker request paths.
  • Keep server/src/routers/chatProxyRouter.ts behavior intact until the new Worker endpoint is live.
  • Validate locally, dry-run bundle, then deploy and smoke test the real /mcp endpoint.

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: Worker fetch entry, Hono app, route registration.
  • Create apps/mcp-worker/src/env.ts: parse Cloudflare bindings and vars from env, not process.env.
  • Create apps/mcp-worker/src/auth.ts: Worker-native copy/extraction of the current MCP auth flow from apps/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 using app.request or unstable_dev.
  • Modify package.json: add workspace apps/mcp-worker; add root scripts such as mcp: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.ts and packages/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 direct process.env reads with injectable config.
  • Modify server/wrangler.jsonc only after Worker deploy: set CHAT_SERVER_URL and MCP_SERVER_URL to the deployed Worker origin, or route /mcp directly 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: AutumnMcpRuntimeConfig with axiomToken, axiomOrgId, analyticsDataset, nodeEnv, and pendingActionStore.

  • 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, and ALL /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: PendingActionStore with create, 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.jsonc after 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 /mcp proxy 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/leaf deployment unchanged until Task 10 passes.
  • If Worker deploy fails, keep server vars pointing to current leaf/local target.
  • If cutover fails after Task 9, restore previous CHAT_SERVER_URL / MCP_SERVER_URL values in server/wrangler.jsonc and redeploy server.
  • 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.ts and mcpInstructions.ts use readFileSync; this must not run in the Worker bundle.
  • packages/mcp/src/agent/pending-actions.ts uses ioredis; Worker needs KV or Durable Object.
  • apps/leaf/src/mcp/handlers/handleMcp.ts depends on @hono/node-server request/response bindings; it is not Worker-native.
  • @mastra/mcp documents CfWorkerJsonSchemaValidator for Cloudflare Workers.
  • server already exposes /mcp and protected-resource metadata as a proxy, so cutover can be config-first after the new Worker is live.