Skip to main content
FrontMCP runs on Cloudflare Workers via the Web-standard fetch handler — clients connect to https://your-worker.workers.dev/mcp over the MCP Streamable HTTP transport. There is no Express and no Node req/res shim on the Worker; the native Request is routed straight into the MCP transport. There are two deployment paths, and they have different maturity: Start with the decorator path. It’s the one that’s verified end-to-end.

Path 1 — Deploy a FrontMCP app (decorator build)

This is the proven path. You write a normal @FrontMcp app and frontmcp build --target cloudflare compiles it into an ES Module Worker.
1

Write the server

Keep tool/resource/prompt code worker-safe: no node:fs, timers, Math.random(), or Date.now() at module top level. Inside execute() / read() is fine — that runs in a request context. --target cloudflare rejects sqlite/redis storage config at build time (no native modules / Node net on Workers).
2

Add frontmcp.config.js

wrangler.name is the deployed Worker name; the build writes it into wrangler.toml.
3

Build

This emits dist/cloudflare/index.js (an ES Module Worker) and (re)writes the top-level keys of wrangler.toml:
nodejs_compat with compatibility_date >= 2024-09-23 is required — the SDK runtime still uses some Node builtins behind that flag; without it the Worker won’t load. The build always emits both.
4

Test locally in workerd, then deploy

5

Verify

Point any MCP client at https://<your-worker-url>/mcp (Streamable HTTP).
frontmcp build --target cloudflare regenerates wrangler.toml’s four top-level keys on every build (alwaysWriteConfig). It does not emit [[kv_namespaces]], [[durable_objects]], [[r2_buckets]], [[d1_databases]], or [triggers], and the generated entry passes only the Request (not env) to the handler — so the decorator-build path can’t wire CF bindings. For bindings + the auto-update Cron, use the managed-edge path below with a hand-maintained wrangler.toml.

Endpoint path, CORS & SSE — all config-driven

The worker’s transport is driven by the standard http + transport config — the same fields the Express host reads — so one config behaves identically on either adapter. There are no edge-specific knobs. The worker serves MCP at exactly one path: http.entryPath (the worker root / when unset). It does not guess a / + /mcp set — pick the path that matches how you expose the worker. Cloudflare routes never strip the path before it reaches the worker:
  • CORS comes from http.cors (false to disable, { origin, credentials, maxAge } to configure). A function origin isn’t supported on the worker — use a static origin (true / string / string[]).
  • SSE is derived from the transport protocol: streaming is on when Streamable HTTP is enabled and JSON-buffering is off (true under legacy/modern, false under stateless-api). Server→client GET streams are always honored.
  • A trailing slash is normalized (/mcp/ matches /mcp); /healthz + /readyz always answer a liveness 200 regardless of entryPath.
The decorator-build path (@FrontMcp({ http, transport }) + frontmcp build --target cloudflare) and the @frontmcp/edge createEdgeMcp({ http, transport }) path read the same config, so the endpoint path / CORS / SSE behave identically on both.

Path 2 — Managed auto-updating edge (@frontmcp/edge)

The @frontmcp/edge package runs FrontMCP from a plain config object — no decorator, no frontmcp build step — and adds managed mode: it pulls a signed skilled-OpenAPI bundle from a SaaS endpoint, caches it in KV, and refreshes it on a Cron Trigger, so the server’s capabilities update without a redeploy.
createEdgeMcp returns { fetch, scheduled }. The fetch handler serves MCP; the scheduled handler is the Cron Trigger entrypoint that pulls a fresh bundle and hot-swaps it. Both come from the same module export.

Required wrangler.toml (hand-maintained)

The managed path is bundled by wrangler (not frontmcp build), so you own wrangler.toml:

How the cache + refresh behave

  • Boot: the first request lazily builds the scope and pulls the bundle. If the pull fails, it falls back to the last-good bundle in KV (validated against the bundle schema) so a SaaS outage doesn’t kill the server.
  • Refresh: the Cron Trigger invokes scheduled(), which pulls a fresh bundle, persists it to KV, and hot-swaps the live skill/tool registries — emitting notifications/*/list_changed.
  • Single-flight: boot and Cron refresh are mutually exclusive (no double-pull on cold start).
  • No source attached → loud failure: if the bundle source can’t be constructed, scheduled() throws so the Cron run is reported as failed rather than silently succeeding with a stale bundle.
Managed mode requires the optional peer @frontmcp/plugin-skilled-openapi. And see Current status — the full managed bundle does not yet boot on workerd.

Storage on the edge

Cloudflare KV backs the generic FrontMCP StorageAdapter, so factory-based stores (sessions, elicitation, cache) can run on Workers:
KV is eventually-consistent and is not Redis. The adapter is honest about what KV can’t do — these throw StorageNotSupportedError pointing at the (forthcoming) Durable Object path:
  • Atomic counters (incr/decr/incrBy) — no atomic increment on KV.
  • Conditional writes (ifNotExists/ifExists) — no compare-and-set.
  • TTL introspection (ttl) — KV doesn’t expose remaining TTL.
keys() runs over the prefix-scoped, paginated list() API with client-side glob filtering, and the 60-second minimum expirationTtl is enforced. delete()’s “existed” flag and expire()’s re-put are best-effort under eventual consistency.

Stateful sessions & notifications (Durable Objects)

A stateless worker can’t support the Streamable HTTP standalone GET notification stream: each request is a fresh isolate/transport, so a tools/call’s notifications have no path back to the client’s open GET stream (it closes immediately). The fix is a Durable Object — one instance per Mcp-Session-Id holds a persistent MCP server + session-bound transport, so the GET stream stays open and server→client notifications reach it. It runs the same http:request flow as the stateless path — only the transport persists. Enable it with sessions and export the DO class createEdgeMcp returns:
The worker routes a request to its session DO by Mcp-Session-Id (minting one on initialize); the router falls back to stateless handling if the binding isn’t present. Without sessions, the worker stays stateless (request/response tools work; no server push).
MCP_SESSION_SECRET is required on a production isolate (NODE_ENV=production) — the flow’s session:verify stage encrypts session IDs with it. createEdgeMcp bridges Worker env vars/secrets into process.env so FrontMCP’s config resolution sees them.

Low-level: createWebFetchHandler() (custom runtimes — Deno, Bun, custom workers)

createEdgeMcp (above) is the recommended path. Under it sits the SDK’s runtime-agnostic Web-standard transport, exported for advanced use — wiring FrontMCP into Deno, Bun, or a hand-rolled Worker where you control the export default { fetch } yourself. It turns a Scope into a (request: Request) => Promise<Response> handler.

Types

Unlike the Express/Node host (@FrontMcp({ http }) + serve), this handler has no server to start — the runtime owns the listener and just hands each Request to it. It runs the same http:request flow (auth, quota, audit, routing) as the Express adapter, so behavior is identical across runtimes; only the transport translation differs. FrontMcp.fetch() and createEdgeMcp are thin wrappers that build a scope and call this.

Current status & limitations

FrontMCP-on-Cloudflare is v1.3 in progress. What works vs what’s still missing: Works today (verified on real Cloudflare):
  • The decorator build path (frontmcp build --target cloudflare) boots on real workerd and serves MCP over /mcp with a /healthz liveness probe.
  • createEdgeMcp also deploys + serves on real Cloudflare — bundled with esbuild + a small set of stubs (see the next bullet). Verified end-to-end (initialize / tools/list / tools/call). The KV last-good cache + the Cron scheduled refresh wiring are unit/integration-tested.
  • The worker runs the real http:request flow — the same hookable pipeline every transport runs: session:verify (auth), router, audit, metrics + plugin/user hooks all execute on the worker. The MCP response is produced by the SDK’s web-standard Streamable HTTP transport (WebStandardStreamableHTTPServerTransport) — which is the standard transport (the Node StreamableHTTPServerTransport is a thin req/res wrapper over it). Configure transparent auth and the flow’s checkAuthorization stage returns 401 + WWW-Authenticate on the worker, just like Node.
  • Stateful sessions via a Durable Object (sessions: {} + the bound SessionDurableObject): the Streamable HTTP standalone GET notification stream stays open across requests and requests for a session route back to the same DO — so server→client notifications work. Verified live.
  • CloudflareKvStorageAdapter (KV only, with the honest limits above).
Deploying createEdgeMcp today needs two things:
  • It must run with serve: false (now the default inside createEdgeMcp) so the scope doesn’t construct the Node Express host.
  • Three Node-only transports that are statically bundled but never used on the edge must be stubbed in your bundler: express (pulls node:tty), raw-body (pulls safer-buffer, which crashes at module-eval on workerd), and cross-spawn (pulls node:child_process, via the MCP stdio client). See cloudflare-sandbox/build-edge.mjs for a working esbuild config.
The miniflare-based local e2e is stricter than production — it rejects node:http2/node:fs that real Cloudflare nodejs_compat actually provides. So the edge package runs on real Cloudflare even though the local managed e2e is skipped. The worker-conditioned SDK build (swapping those Node-only transports for the browser variants the SDK + protocol already ship) is the roadmap item that removes the manual stubs.
Known gaps (roadmap):
  • Stateless mode has no server push. Without sessions, the worker is stateless and the standalone GET notification stream can’t deliver server→client notifications (each request is a fresh isolate) — enable sessions (the Durable Object) for that.
  • DO session eviction. A Durable Object is evicted when idle; an in-flight session then needs to re-initialize. WebSocket-hibernation-style persistence is a follow-up.
  • KV only. No R2 (blobs) or D1 (relational) stores, and no @frontmcp/adapters/cloudflare subpath yet.
  • No worker observability sink, no deploy CLI / GitHub Action / push webhook, and the declarative frontmcp.deploy.yaml manifest is schema-only (no runtime consumer yet).

Deployment targets

All build targets (node / cloudflare / vercel / browser).

Skills-Only Deployment

The skilled-OpenAPI model managed mode pulls.

Deploy manifest reference

The frontmcp.deploy.yaml schema (forward-looking).

Production build

Bundling + production hardening.