# Cloudflare Workers

> Deploy a FrontMCP server as a Cloudflare Worker with frontmcp build --target cloudflare: the entry and wrangler.toml it writes, secrets and bindings, when the Worker is in production, and the storage it takes and refuses.

Source: https://frontmcp.dev/reference/deployment/cloudflare-workers

`frontmcp build --target cloudflare` compiles your server into a Worker: an entry module that hands each request to FrontMCP's web-standard handler, [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler), and a `wrangler.toml` that points at it. `wrangler` bundles the result, runs it locally in the same runtime as Cloudflare with `wrangler dev`, and uploads it with `wrangler deploy`. The Worker keeps no sessions and can't reach Redis over TCP or a SQLite file, so it serves MCP 2026-07-28 clients fully and older clients one request at a time.

```bash
frontmcp build --target cloudflare   # dist/cloudflare/index.js, and main in wrangler.toml
npx wrangler dev                     # locally, in workerd
npx wrangler deploy
```

[`@frontmcp/edge`](https://frontmcp.dev/reference/deployment/edge), new in 1.9, runs a server on Workers without `frontmcp build`: from a plain configuration, bundled by `wrangler` as it is, with sessions kept in a Durable Object and a managed mode that pulls a Skilled OpenAPI bundle. This page is about the `frontmcp build` target.

---

## Reference

### What the build writes

```text
dist/cloudflare/
  main.js, help-desk.app.js, …   your compiled code, as ES modules
  package.json                   { "type": "module" }
  serverless-setup.js            sets FRONTMCP_SERVERLESS, FRONTMCP_WORKER, FRONTMCP_DEPLOYMENT_MODE and FRONTMCP_HTTP_ENTRY_PATH
  index.js                       the Worker's entry
  tools/queue.widget.tsx, …      your tools' *.widget.tsx and *.widget.jsx files, at the path they have under src/
```

The build doesn't bundle anything: `wrangler` does, when it runs or deploys the Worker. It prints `Cloudflare Workers adapter is experimental. See docs for limitations.` on every build.

Since 1.8.6 the build also copies the widget files of tools with a `file` widget, and says `copied 1 widget source file (*.widget.tsx/jsx) to dist/cloudflare`. That doesn't put them in the Worker: `wrangler` bundles what `index.js` imports, nothing imports a widget file, and `wrangler deploy --dry-run --outdir` wrote only `index.js`, so a `file` widget has nothing to read once the Worker is deployed.

`serverless-setup.js` runs before your server's module, so `@FrontMcp` builds a fetch handler instead of listening ([what `@FrontMcp` does on import](https://frontmcp.dev/reference/sdk/frontmcp-instance#what-frontmcp-does-on-import)):

```js dist/cloudflare/serverless-setup.js
// Auto-generated — sets env before the @FrontMcp decorator runs.
process.env.FRONTMCP_SERVERLESS = '1';
process.env.FRONTMCP_DEPLOYMENT_MODE = 'serverless';
process.env.FRONTMCP_WORKER = '1';
process.env.FRONTMCP_HTTP_ENTRY_PATH = "/mcp";
globalThis.FRONTMCP_BUILD_TARGET = globalThis.FRONTMCP_BUILD_TARGET || "cloudflare";
```

The `FRONTMCP_HTTP_ENTRY_PATH` line is `transport.http.path` from `frontmcp.config`, and only appears when it's set. `http.entryPath` in `@FrontMcp` still wins over it. So the Worker serves the path `frontmcp dev` serves, as the Vercel, Lambda and distributed builds do since 1.8.7 ([the endpoint path](https://frontmcp.dev/reference/deployment/production-build#the-endpoint-path)). When `deployments[].server` sets security headers, the build adds `FRONTMCP_HSTS`, `FRONTMCP_CSP_*` and the like to this file ([security headers](https://frontmcp.dev/reference/deployment/security#from-frontmcpconfig)).

`index.js` exports `{ fetch(request, env, ctx) }`. On the first request of an isolate it copies every **string** binding in `env` (your `[vars]` and secrets) into `process.env`, without replacing a value that's already there; then it passes the request, `ctx` and `env` to the handler, which is built on that first request.

### `wrangler.toml`

`frontmcp create --target cloudflare` writes one, followed by comments on the secrets to set:

```text title="wrangler.toml"
name = "help-desk"
main = "dist/cloudflare/index.js"
compatibility_date = "2024-11-11"
compatibility_flags = ["nodejs_compat", "nodejs_compat_populate_process_env"]

[vars]
NODE_ENV = "production"
```

`NODE_ENV = "production"` puts `wrangler dev` in production too ([below](#production-mode-and-secrets)). `2024-11-11` is the earliest date Vercel KV works with ([storage](#storage)); before 1.9.2, `create` and the build wrote `2024-09-23`.

Every build rewrites only what it manages, and keeps your comments, `[vars]`, bindings and other sections:

| Key | What the build does |
| --- | --- |
| `main` | Always sets it to `dist/cloudflare/index.js`, or the same path under `--out-dir`: `--out-dir build-out` gives `build-out/cloudflare/index.js` (since 1.9.2). |
| `name` | Writes it only when the file has none. When it differs from `deployments[].wrangler.name` in `frontmcp.config`, it keeps the file's and warns: `wrangler.toml declares name = "…" but frontmcp.config resolves to "…". Keeping "…"`. |
| `compatibility_date` | Writes it, `2024-11-11`, only when the file has none. |
| `compatibility_flags` | Adds `nodejs_compat`, which FrontMCP needs, and `nodejs_compat_populate_process_env`, which fills `process.env` from `[vars]` and secrets before your module runs; keeps the flags you added. |

`deployments: [{ target: "cloudflare", wrangler: { name, compatibilityDate, compatibilityFlags } }]` in [`frontmcp.config`](https://frontmcp.dev/reference/server/config-files#fields) sets what the build writes.

### Production mode and secrets

FrontMCP reads the Worker's own `NODE_ENV`, from `[vars]` or a secret, first. Without one, it falls back to what `wrangler` writes into the bundle: `"production"` for `wrangler deploy`, `"development"` for `wrangler dev`. So with `NODE_ENV = "production"` in `[vars]`, as `frontmcp create` writes it, the Worker is in [production mode](https://frontmcp.dev/reference/auth/production#production-mode) under `wrangler dev` too; `wrangler dev --var NODE_ENV:development` runs it in development. Under `wrangler dev` 4.147, with `NODE_ENV = "production"` in `[vars]`, a tool that read `this.runtimeContext.env` got `production`, while `process.env.NODE_ENV` in the tool's own code was `development`: `wrangler` replaces that expression in your code as it bundles, so read `this.runtimeContext.env` instead.

Changed in 1.9: before, `NODE_ENV` in `[vars]` changed nothing, so `wrangler dev` was never in production and a deployed Worker always was.

In production the Worker needs:

| Secret | When | Without it |
| --- | --- | --- |
| `MCP_SESSION_SECRET` | For clients on protocol versions before 2026-07-28 | Their `initialize` answers `500` `{"error":"server_misconfigured","code":"SESSION_SECRET_REQUIRED","message":"Set MCP_SESSION_SECRET in the deployment environment (e.g. \`wrangler secret put MCP_SESSION_SECRET\`). …"}`. Clients on MCP 2026-07-28, anonymous or with a static key, are served without it, and `/healthz` answers `200`. |
| `JWT_SECRET` | `local` and `remote` auth | Every request, health checks included, answers `500` with `JWT_SECRET_REQUIRED`: see [production secrets](https://frontmcp.dev/reference/sdk/create-fetch-handler#production-secrets). |

Set them with `wrangler secret put MCP_SESSION_SECRET`, and locally in `.dev.vars`, which `wrangler dev` reads. [Checking production locally](#checking-production-locally) runs `wrangler dev` in production mode.

Changed in 1.8.5: before, a production Worker without `MCP_SESSION_SECRET` answered every anonymous or static-key request with `500 SESSION_SECRET_REQUIRED`, 2026-07-28 ones included.

#### When the server can't be built

A Worker builds the server on its first request. When that fails, every request, health checks included, gets an answer that says so, and later requests try again:

| Cause | Answer |
| --- | --- |
| A configuration fault, like a missing `JWT_SECRET` or an `authorities` rule that names an unknown profile | `500` `{ "error": "server_misconfigured", "code", "message" }`, with the code of the fault, like `JWT_SECRET_REQUIRED` |
| Anything else, like a [remote app](https://frontmcp.dev/reference/server/remote) that can't be reached | `503` `{ "error": "server_unavailable", "code": "SERVER_START_FAILED", "message": "The server failed to start, so it refuses requests. Its log has the cause; a request after the Retry-After delay tries to start it again." }`, with `Retry-After` in seconds |

The retries back off: the build ran again after 1, 2, 4, then 8 seconds while requests kept arriving, and the log has the cause of each failure (`FrontMCP failed to start; requests are refused until a retry succeeds …`). Once the cause is gone, the next retry builds the server and requests are served. Changed in 1.8.5: before, only recognized configuration faults answered; any other failure made `fetch` throw, and every request built the server again at once.

### What the Worker serves

What [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler#what-it-answers) serves: MCP at one path, `/healthz` and `/health` with a fixed `200` `{ "status": "ok", "server", "transport": "web-fetch" }` that runs no checks, `/readyz` when you turn it on ([Health checks](https://frontmcp.dev/reference/deployment/health-and-metrics#what-each-runtime-answers)), and `404` `{ "error": "Not Found", "entryPaths": ["/mcp"] }` for everything else. Its [caveats](https://frontmcp.dev/reference/sdk/create-fetch-handler#caveats) apply: no custom routes, the primary scope only. `createFetchHandler()` serves `/metrics` since 1.9, and a Worker does since 1.9.2: when the `@FrontMcp` source mentions `metrics` and `@frontmcp/observability` is installed, the build imports it into `index.js` and registers it with the SDK. With `metrics: { enabled: true }` and `@frontmcp/observability` and `@opentelemetry/sdk-trace-base` installed, `/metrics` under `wrangler dev` answered `200` with Prometheus text: the counters you create, like `frontmcp_tool_add_calls_total 1` after one call of a tool that counts, and an empty body before any. A Worker has no process to measure, so there are no `frontmcp_process_*` gauges (changed in 1.9.3: in 1.9.2 it listed five, all `0`). Before 1.9.2 such a Worker logged `No such module "@frontmcp/observability"` and answered every request `503` `SERVER_START_FAILED`.

A client on MCP 2026-07-28 loses nothing. An older client gets no session id, and each of its requests is served on its own ([sessions](https://frontmcp.dev/reference/sdk/create-fetch-handler#sessions)).

#### Bindings

String bindings reach your code as `process.env.NAME`, through the flag and through the entry's copy. KV namespaces, D1 databases, R2 buckets and Durable Objects are objects: the entry passes `env` to FrontMCP, and a tool reads it as `this.workerEnv`, the `env` object of the request it's serving:

```ts
const kv = this.workerEnv?.["DESK_KV"] as KVNamespace | undefined;
```

`this.workerEnv` is `undefined` when the request has no `env`: on the Node server, under `createDirect()`, and in a fetch handler that wasn't given one. Under `wrangler dev` with a `[[kv_namespaces]]` binding, a tool wrote a key to the namespace and read it back, and `Object.keys(this.workerEnv)` listed `NODE_ENV`, the other `[vars]` and the binding. Changed in 1.8.7: in 1.8.6 nothing on a tool's `this` exposed `env`, so a tool couldn't reach these bindings through FrontMCP.

### Storage

A Worker can't open a TCP connection to Redis or a SQLite file, and the build refuses a server whose `@FrontMcp` source names either, even behind a condition in the field's value:

```text
Error: [--target cloudflare] config incompatible with Cloudflare Workers:
  - ioredis-style `redis` storage is not supported on --target cloudflare (no Node net). Even an env-gated `redis: process.env.X ? {...} : undefined` still ships the Node-only branch in the worker bundle. Use `redis: { provider: 'vercel-kv' }` (HTTP), or move the redis config behind a build-time `define` so the bundler can dead-code-eliminate it.
```

A `redis` spread in only when a variable is set, `...(process.env.REDIS_URL ? { redis: { url: process.env.REDIS_URL } } : {})`, gets past the build when `REDIS_URL` isn't set where you build. With `REDIS_URL` set in the Worker's environment, every request then answered `503` `SERVER_START_FAILED`, and the log said `ioredis is required for Redis storage adapter`. Keep `redis` out of a Worker's entry.

`redis: { provider: "vercel-kv" }`, which talks to Vercel KV or Upstash over HTTP, is accepted when the source spells the provider out like that, in any entry file. A `redis` whose provider the build can't tell, like `redis: { url: process.env.REDIS_URL }`, is refused with `the build could not evaluate the entry to see which provider it uses`. A Worker with Vercel KV needs `compatibility_date` `2024-11-11` or later, which the build writes since 1.9.2. With an earlier date in `wrangler.toml`, the build warns, `redis: { provider: 'vercel-kv' } needs compatibility_date 2024-11-11 or later on Workers (its Upstash client sends cache: 'no-store', which earlier dates reject), but the Worker runs with 2024-09-23: every storage call will fail. Raise compatibility_date in wrangler.toml.`, and `wrangler dev` logged `Failed to create session store - session persistence disabled` with `Failed to connect to Vercel KV: The 'cache' field on 'RequestInitializerDict' is not implemented.`; with `2024-11-11` it logged `Session store connection validated successfully`. The Worker still keeps no sessions ([what the Worker serves](#what-the-worker-serves)): `redis` is for what else uses it, like a plugin's `"global-store"`. `sqlite` fails the build with a message of its own, and so does `tasks: { enabled: true }` without `tasks.redis`. Background tasks are off on a Worker unless you ask for them.

Changed in 1.9: before, the build accepted `redis: { provider: "vercel-kv" }` only from an entry that imported no other TypeScript file, and a Worker built with it failed to load its session store (`No such module "@frontmcp/auth"`).

So the Worker keeps everything in the isolate's memory, which Cloudflare may discard between requests. That's enough for MCP 2026-07-28, where each request carries what it needs, and for `transparent` or `static` auth. `local` and `remote` auth keep pending sign-ins and refresh tokens in memory by default, so a sign-in whose steps reach different isolates fails. For older clients' sessions on a Worker, [`@frontmcp/edge`](https://frontmcp.dev/reference/deployment/edge#sessions-in-a-durable-object) keeps each one in a Durable Object.

#### Caveats

- The upload for a small server is about 7.7 MB, 1.4 MB compressed, as `wrangler deploy --dry-run` reports it (`Total Upload: 7697.86 KiB / gzip: 1410.18 KiB`).
- `frontmcp build --target cloudflare` doesn't read a `frontmcp.deploy.yaml` file.
- `wrangler dev` was run for this page with `wrangler` 4.149 and FrontMCP 1.9.3 (the spread `redis` again with 1.9.4): metrics and a spread `redis`; and with `wrangler` 4.147 and 1.9.2: the endpoint and its `404`, production mode and the missing secret, `this.runtimeContext.env` and Vercel KV. `wrangler deploy` only with `--dry-run`. Vercel KV was run against a local server that speaks Upstash's REST API. Bindings and the start-failure retries were last run with 1.9.1.

---

## Usage

### Deploying a Worker

In a project made with `frontmcp create --target cloudflare`, which lists `wrangler` in `devDependencies`:

```bash
npx frontmcp build --target cloudflare
```

```text
Cloudflare Workers adapter is experimental. See docs for limitations.
[build] entry: src/main.ts
[build] outDir: dist/cloudflare
[build] target: cloudflare (esnext)
[build] tsconfig.json detected — compiling with project settings
[build] Generating cloudflare deployment files...
  Generated serverless setup at dist/cloudflare/serverless-setup.js
  Generated cloudflare entry at dist/cloudflare/index.js
  Updated wrangler.toml (managed keys only)
Build completed.
Output placed in dist/cloudflare
Server will serve MCP at /mcp
```

```bash
npx wrangler dev --port 8787
curl http://127.0.0.1:8787/healthz
```

```json
{"status":"ok","server":{"name":"help-desk","version":"1.0.0"},"transport":"web-fetch"}
```

A `tools/call` to `http://127.0.0.1:8787/mcp` returns the tool's result, and `/` answers `404` with `"entryPaths":["/mcp"]`. Then set the secret and deploy:

```bash
npx wrangler secret put MCP_SESSION_SECRET   # paste the output of: openssl rand -hex 32
npx wrangler deploy
```

The project's `deploy` script does the build and `wrangler deploy` in one step, and `dev:worker` the build and `wrangler dev`.

### Reading secrets, variables and bindings

A tool reads a Worker's `[vars]` and secrets from `process.env`, and its other bindings, like a KV namespace, from `this.workerEnv`. `worker.ts` below does what the generated entry does, with `createFetchHandler()` in place of the decorated class, and the tests send it `env` the way Cloudflare does:

```ts desk-api.tool.ts active
import { Tool, ToolContext, z } from "@frontmcp/sdk";

@Tool({ name: "desk_api_status", description: "Which help desk API and region this server uses", inputSchema: {} })
export class DeskApiStatus extends ToolContext {
  async execute() {
    return {
      apiUrl: process.env.DESK_API_URL ?? null,
      region: process.env.DESK_REGION ?? null,
      kvInProcessEnv: process.env.DESK_KV !== undefined,
    };
  }
}

type KvNamespace = { get(key: string): Promise<string | null>; put(key: string, value: string): Promise<void> };

@Tool({
  name: "remember_note",
  description: "Keep a note for a ticket in the Worker's KV namespace, and read it back",
  inputSchema: { id: z.string(), note: z.string() },
})
export class RememberNote extends ToolContext {
  async execute({ id, note }: { id: string; note: string }) {
    const kv = this.workerEnv?.["DESK_KV"] as KvNamespace | undefined;
    if (!kv) return { stored: false, note: null };
    await kv.put(`note:${id}`, note);
    return { stored: true, note: await kv.get(`note:${id}`) };
  }
}
```

```ts worker.ts
import { App, FrontMcpInstance } from "@frontmcp/sdk";
import { DeskApiStatus, RememberNote } from "./desk-api.tool";

@App({ id: "help-desk", name: "Help Desk", tools: [DeskApiStatus, RememberNote] })
export class HelpDesk {}

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { entryPath: "/mcp" },
};

const handler = FrontMcpInstance.createFetchHandler(config);

// As in the entry `frontmcp build --target cloudflare` writes: on the first request,
// copy each string binding into process.env, without replacing what's already there.
let envBridged = false;
function bridgeEnvToProcess(env: Record<string, unknown>) {
  if (envBridged || !env) return;
  envBridged = true;
  for (const [key, value] of Object.entries(env)) {
    if (typeof value !== "string") continue;
    if (process.env[key] === undefined) process.env[key] = value;
  }
}

export default {
  async fetch(request: Request, env: Record<string, unknown>, ctx: { waitUntil(promise: Promise<unknown>): void }) {
    bridgeEnvToProcess(env);
    return (await handler)(request, ctx, env);
  },
};
```

```ts worker.test.ts
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import worker, { config } from "./worker";

const ctx = { waitUntil() {} };

function kvNamespace() {
  const data = new Map<string, string>();
  return { get: async (key: string) => data.get(key) ?? null, put: async (key: string, value: string) => void data.set(key, value) };
}

function callTool(name: string, args: Record<string, unknown>) {
  return new Request("https://desk.example.com/mcp", {
    method: "POST",
    headers: { "content-type": "application/json", "mcp-protocol-version": "2026-07-28", "mcp-method": "tools/call", "mcp-name": name },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: 1,
      method: "tools/call",
      params: { name, arguments: args, _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
    }),
  });
}

test("string bindings reach process.env; a value already there wins; objects don't", async () => {
  process.env.DESK_REGION = "eu-west"; // set before the first request, as the populate flag does
  try {
    const first = await worker.fetch(callTool("desk_api_status", {}), { DESK_API_URL: "https://api.desk.example.com", DESK_REGION: "us-east", DESK_KV: kvNamespace() }, ctx);
    expect((await first.json()).result.structuredContent).toEqual({ apiUrl: "https://api.desk.example.com", region: "eu-west", kvInProcessEnv: false });

    // 🚩 Only the first request is copied: a binding that changes later isn't seen
    const later = await worker.fetch(callTool("desk_api_status", {}), { DESK_API_URL: "https://staging.desk.example.com" }, ctx);
    expect((await later.json()).result.structuredContent.apiUrl).toBe("https://api.desk.example.com");
  } finally {
    delete process.env.DESK_API_URL;
    delete process.env.DESK_REGION;
  }
});

test("`this.workerEnv` is the request's `env`, so a tool reaches a KV namespace", async () => {
  const response = await worker.fetch(callTool("remember_note", { id: "T-1", note: "Asked for a screenshot" }), { DESK_KV: kvNamespace() }, ctx);
  expect((await response.json()).result.structuredContent).toEqual({ stored: true, note: "Asked for a screenshot" });
});

test("`this.workerEnv` follows each request, and is undefined without one", async () => {
  const kv = kvNamespace();
  await worker.fetch(callTool("remember_note", { id: "T-2", note: "first" }), { DESK_KV: kv }, ctx);
  expect(await kv.get("note:T-2")).toBe("first");

  const noEnv = await worker.fetch(callTool("remember_note", { id: "T-2", note: "second" }), {}, ctx);
  expect((await noEnv.json()).result.structuredContent).toEqual({ stored: false, note: null });
  expect(await kv.get("note:T-2")).toBe("first");

  const direct = await FrontMcpInstance.createDirect(config);
  try {
    expect((await direct.callTool("remember_note", { id: "T-3", note: "x" })).structuredContent).toEqual({ stored: false, note: null });
  } finally {
    await direct.dispose();
  }
});

test("health checks answer; other paths name the endpoint", async () => {
  const health = await worker.fetch(new Request("https://desk.example.com/healthz"), {}, ctx);
  expect(await health.json()).toEqual({ status: "ok", server: { name: "help-desk", version: "1.0.0" }, transport: "web-fetch" });
  const root = await worker.fetch(new Request("https://desk.example.com/", { method: "POST", body: "{}" }), {}, ctx);
  expect(root.status).toBe(404);
  expect(await root.json()).toEqual({ error: "Not Found", entryPaths: ["/mcp"] });
});
```

The Playground's own **Call** tab calls the tools without the Worker, so `desk_api_status` shows `null`s and `remember_note` stores nothing: nothing copied a binding, and there's no `env`. On Cloudflare, `nodejs_compat_populate_process_env` has already filled `process.env` before the first request, so the entry's copy only adds what the flag didn't.

### Checking production locally

With `NODE_ENV = "production"` in `[vars]`, as `frontmcp create` writes it, `wrangler dev` runs the Worker in production, so a missing secret shows up before you deploy:

```bash
npx wrangler dev
```

Without `.dev.vars`, `/healthz` answers `200`, and so does a 2026-07-28 `tools/call`, but an `initialize` from an older client answers `500` with `SESSION_SECRET_REQUIRED`. With a `.dev.vars` file holding `MCP_SESSION_SECRET=…`, `wrangler dev` lists it as a hidden variable and `initialize` succeeds too. `npx wrangler dev --var NODE_ENV:development` runs it in development instead. (Before 1.9 `[vars]` didn't count, and `--define 'process.env.NODE_ENV:"production"'` was the way to see production locally.)

---

## Troubleshooting

### `[--target cloudflare] config incompatible with Cloudflare Workers`

The `@FrontMcp` source names `sqlite`, or a `redis` that isn't written as `redis: { provider: "vercel-kv" }`, or enables `tasks` without `tasks.redis`. Remove the key from the Worker's configuration, or use Vercel KV. The build reads the source, so hiding the key behind `process.env` doesn't help. See [storage](#storage).

### `{"error":"server_misconfigured","code":"SESSION_SECRET_REQUIRED",…}`

The deployed Worker is in production, has no `MCP_SESSION_SECRET`, and a client on a protocol version before 2026-07-28 sent a request. Run `wrangler secret put MCP_SESSION_SECRET`.

### `503` `{"error":"server_unavailable","code":"SERVER_START_FAILED",…}`, on every path

The server failed to build, for a reason that isn't a configuration fault, like a remote app that couldn't be reached. `wrangler tail`, or the `wrangler dev` output, has the cause. Requests after the `Retry-After` delay try again. See [when the server can't be built](#when-the-server-cant-be-built).

### A missing secret works under `wrangler dev`, then fails once deployed

`wrangler dev` runs in development unless the Worker's `NODE_ENV` is `production`. Put `NODE_ENV = "production"` in `[vars]`, as `frontmcp create` does: see [checking production locally](#checking-production-locally).

### `The 'cache' field on 'RequestInitializerDict' is not implemented`

The Worker uses Vercel KV, and its `compatibility_date` is older than `2024-11-11`, as in a `wrangler.toml` written before 1.9.2. The build warns about it. Raise it in `wrangler.toml`. See [storage](#storage).

### `No such module "@frontmcp/observability"`

The Worker was built before 1.9.2 with `metrics: { enabled: true }`, which it couldn't load, so it didn't start. Build it again with 1.9.3, with `@frontmcp/observability` and `@opentelemetry/sdk-trace-base` installed. See [what the Worker serves](#what-the-worker-serves).

### `{"error":"Not Found","entryPaths":["/mcp"]}`

The request's path isn't the endpoint. `entryPaths` says where it is: `http.entryPath`, else `transport.http.path` from `frontmcp.config`, else `/`. Point the client there.

### `wrangler.toml declares name = "…" but frontmcp.config resolves to "…"`

The two names differ and the build kept the file's. Set `deployments[].wrangler.name` in `frontmcp.config` to the same name, or change the file.

### A tool can't read a KV namespace, D1 database or R2 bucket

Only string bindings reach tools through `process.env`. Read the others from `this.workerEnv`: see [bindings](#bindings).
