# Production build

> frontmcp build and its targets: what each one writes, what it bundles and what it leaves in node_modules, how the result runs, which endpoint path it serves, and when it's in production mode.

Source: https://frontmcp.dev/reference/deployment/production-build

`frontmcp build` turns your server into what a platform runs: a bundle and a start script for a Node process, a request handler for Vercel or AWS Lambda, a Worker for Cloudflare, a library, a browser module, a command-line program, or an MCP Bundle for desktop clients. Every target starts from the same `@FrontMcp` entry file, but each one bundles different things, serves its endpoint at a path chosen in a different way, and decides differently whether it runs in production. This page is the map; the pages after it deploy each target. [Deploying Your Server](https://frontmcp.dev/learn/deploying-your-server) takes one server from `frontmcp build` to a connected client, step by step.

```bash
frontmcp build [--target <target>] [--config <file>] [--out-dir <dir>] [--entry <file>] [--no-clean]
```

---

## Reference

### `frontmcp build`

Run it in the project folder. It compiles your TypeScript with the project's `tsconfig.json`, then does what the target needs.

| Option | Default | What it does |
| --- | --- | --- |
| `-t, --target <target>` | Every entry of `deployments` in [`frontmcp.config`](https://frontmcp.dev/reference/server/config-files#frontmcpconfig), one after the other; `node` without a config | `node`, `distributed`, `vercel`, `lambda`, `cloudflare`, `sdk`, `browser`, `cli` or `mcpb`. |
| `-c, --config <path>` | The `frontmcp.config` found by searching up from the project, or `FRONTMCP_CONFIG` | The config file to read: its `name`, `version`, `entry`, `deployments` and `transport`. Every target honours it since 1.8.7. |
| `-o, --out-dir <dir>` | `dist` | The base folder. Each target writes to `<dir>/<target>/`, unless its `deployments` entry has an `outDir`. |
| `-e, --entry <file>` | `entry` in `frontmcp.config` | The file with your `@FrontMcp` class. |
| `--no-clean` | cleans | Keep what's already in the target's folder. By default the build empties it first, so a deleted source file can't linger in the output. |
| `--js` | | `cli` only: a JavaScript bundle instead of a native executable. |
| `--sea`, `--merge-from <dir>`, `--icon <path>`, `--no-deterministic`, `--stage-only` | | `mcpb` only. See [MCP Bundles](https://frontmcp.dev/reference/deployment/mcp-clients#installing-from-an-mcp-bundle). |

With two entries in `deployments`, like `[{ target: "node" }, { target: "sdk", outDir: "lib" }]`, one `frontmcp build` writes `dist/node/` and `lib/`.

The build's name and version come from `frontmcp.config`. Without a `version` there, the version is the one in `package.json` (changed in 1.8.7: it was `1.0.0`, whatever `package.json` said). That's the version in the bundle's name and manifest; the `info.version` your server reports to clients is the one in `@FrontMcp`.

### Targets

| Target | Writes | Runs as | Page |
| --- | --- | --- | --- |
| `node` | `dist/node/<name>.bundle.js`, a start script `dist/node/<name>`, `<name>.manifest.json` and `install-<name>.sh` | An HTTP server: `node dist/node/<name>.bundle.js` | [Node.js and Docker](https://frontmcp.dev/reference/deployment/node) |
| `distributed` | The compiled files, unbundled, plus `serverless-setup.js`, which sets `FRONTMCP_DEPLOYMENT_MODE=distributed` (and the endpoint path, and the `ha` and `server.cookies` settings of `frontmcp.config`), and `index.js`, which loads it and then your server | An HTTP server on every interface: `node dist/distributed/index.js` | [High availability](https://frontmcp.dev/reference/deployment/high-availability) |
| `vercel` | `dist/vercel/handler.cjs`, one bundle of everything (about 10 MB), and `.vercel/output/` in the project for Vercel's Build Output API; `vercel.json` if there's none | A Node `(req, res)` handler | [Vercel](https://frontmcp.dev/reference/deployment/vercel) |
| `lambda` | `dist/lambda/handler.cjs`, one bundle that exports `handler` | An AWS Lambda handler | [AWS Lambda](https://frontmcp.dev/reference/deployment/aws-lambda) |
| `cloudflare` | `dist/cloudflare/index.js`, a Worker entry, with your compiled files as ES modules; rewrites `main` in `wrangler.toml` | A Worker, bundled by `wrangler` | [Cloudflare Workers](https://frontmcp.dev/reference/deployment/cloudflare-workers) |
| `sdk` | `dist/sdk/<name>.cjs.js`, `<name>.esm.mjs` and type declarations | A library that other code imports | |
| `browser` | `dist/browser/<name>.browser.mjs` | An ES module for a web app's own bundler | |
| `cli` | With `--js`, `dist/cli/<name>-cli.bundle.js` and a start script; without it, a native executable (not built for this page) | A command-line program with a `serve` command, and `--stdio` | [Connecting MCP clients](https://frontmcp.dev/reference/deployment/mcp-clients#starting-the-server-over-stdio) |
| `mcpb` | `dist/mcpb/<name>-<version>.mcpb`, a ZIP archive | Installed by a desktop client | [Connecting MCP clients](https://frontmcp.dev/reference/deployment/mcp-clients#installing-from-an-mcp-bundle) |

### What's in the bundle

| Targets | Bundled | Left as imports, so they must be installed where it runs |
| --- | --- | --- |
| `node`, `cli`, `sdk`, `browser` | Your code, and packages you import that aren't FrontMCP's | `@frontmcp/sdk`, `@frontmcp/di`, `@frontmcp/utils`, `@frontmcp/auth`, `@frontmcp/adapters`, `@frontmcp/lazy-zod`, `reflect-metadata`, the optional `better-sqlite3`, `@frontmcp/storage-sqlite`, `@vercel/kv`, `esbuild`, `@swc/core` and `fsevents`, and `@frontmcp/observability` and `@opentelemetry/sdk-trace-base` when they aren't installed |
| `mcpb` | Everything, FrontMCP included, into `server/index.js` (about 10 MB), so the archive runs without `node_modules` | Only the optional `better-sqlite3`, `@frontmcp/storage-sqlite`, `@vercel/kv`, `esbuild`, `@swc/core` and `fsevents` |
| `vercel`, `lambda` | Everything installed, FrontMCP included, into `handler.cjs`; an optional package that isn't installed, like `@upstash/redis` or a model provider's SDK that [agents](https://frontmcp.dev/reference/sdk/agent) load, stays a `require()` that runs only if your configuration asks for it. The Lambda handler includes `@codegenie/serverless-express` since 1.9. | `better-sqlite3`, always; `esbuild`, `@swc/core` and React, which FrontMCP only loads when you use them |
| `distributed` | Nothing: `tsc` output | Everything in `dependencies` |
| `cloudflare` | Nothing yet: `wrangler dev` and `wrangler deploy` bundle it | Nothing at run time: `wrangler` puts it all in the Worker |

So the `node` bundle of a small server is a few kilobytes, and it only runs next to a `node_modules` folder with your production dependencies, FrontMCP's included. `@frontmcp/sdk` brings what it loads, `vectoriadb` and `tslib` among them, since 1.9: the bundle ran in a folder with only `@frontmcp/sdk` and `reflect-metadata` installed. (In 1.8.7 `vectoriadb` came with the `frontmcp` package, which had to stay in `dependencies`.)

The `vercel` and `lambda` builds bundle what's installed and leave what isn't: in a project made with `frontmcp create`, with nothing else installed but `@codegenie/serverless-express`, which `lambda` needs, both finish without a warning. Changed in 1.9.3: in 1.9.2 they stopped with `Module not found: Can't resolve '@upstash/redis'` until you installed that package, and warned about `@opentelemetry/api`; and `openai` and `@anthropic-ai/sdk` stayed out of `handler.cjs` even when installed.

Install a package your server uses, so it's in the bundle: `@vercel/kv` with `redis: { provider: "vercel-kv" }`, `@frontmcp/observability` and `@opentelemetry/sdk-trace-base` with `metrics`, `openai` or `@anthropic-ai/sdk` for an agent's model. A function that uses one it doesn't have logs it at run time, like `@vercel/kv is required for Vercel KV storage adapter. Install it with: npm install @vercel/kv`.

> **Note**
In 1.8.6 the build stopped at the first optional package that wasn't installed, `Module not found: Can't resolve '@frontmcp/storage-sqlite'`, and the same for `@frontmcp/observability`, `@vercel/kv` and `better-sqlite3`, whether your server used them or not, until you ran `npm install @frontmcp/storage-sqlite @frontmcp/observability @opentelemetry/sdk-trace-base @vercel/kv`. `build --target cli` had the same failure with `@frontmcp/observability`. Neither does now.

### Widget sources

A tool whose widget is a file, like `ui: { template: { file: join(__dirname, "queue.widget.tsx") } }`, reads it when the tool is called, and `tsc` never emits `.widget.tsx` or `.widget.jsx` files. Since 1.8.6, `frontmcp build` copies them into the output and says `copied 1 widget source file (*.widget.tsx/jsx) to dist/node`:

| Targets | Where the widget files land |
| --- | --- |
| `node`, `cli`, `vercel`, `lambda` | Directly in the target's folder, beside the bundle: `dist/node/queue.widget.tsx`. Every module in a bundle resolves `__dirname` to that folder. Two widgets with the same file name can't both be there: the build copies neither and says `not copying queue.widget.tsx: … share the name`. |
| `cloudflare`, `distributed` | In the compiled tree, at the path they have under `src/`: `dist/cloudflare/tools/queue.widget.tsx`. |
| `sdk`, `browser`, `mcpb` | Not copied. |

Other local files that a widget imports aren't copied, and neither are widgets by `install-<name>.sh`, which copies a fixed list of files. Before 1.8.6, a built server failed the widget's first call with a bare `ENOENT` for `dist/…/queue.widget.tsx`.

### Production mode

[Production mode](https://frontmcp.dev/reference/auth/production#production-mode) hides internal errors from callers and requires `MCP_SESSION_SECRET` for clients that keep a session (those before MCP 2026-07-28). Whether a build is in production is decided per target:

| Targets | In production when |
| --- | --- |
| `node`, `distributed`, `cli`, `sdk` | `NODE_ENV=production` when the process starts. |
| `vercel`, `lambda` | When `NODE_ENV=production` as the function starts. Vercel sets it for its functions; AWS Lambda doesn't, so set `NODE_ENV=production` in the function's environment. Without it the function runs in development mode: it makes up secrets, and shows callers the messages of plain errors. |
| `cloudflare` | When `NODE_ENV` in `[vars]` is `production`, which `frontmcp create` writes, under `wrangler dev` too (since 1.9). Without it, when `wrangler deploy` bundles it, and never under `wrangler dev`. See [Cloudflare Workers](https://frontmcp.dev/reference/deployment/cloudflare-workers#production-mode-and-secrets). |

### The endpoint path

Where the MCP endpoint is depends on who starts the server:

| Started by | Serves MCP at |
| --- | --- |
| `frontmcp dev` | `http.entryPath` in `@FrontMcp`, else `transport.http.path` in `frontmcp.config`. A project made with `frontmcp create` has `transport.http.path: "/mcp"`. |
| The `node` build: `node dist/node/<name>.bundle.js`, which is also what a Docker image's `CMD` runs, or its start script `dist/node/<name>` | The same: the bundle and the script set `FRONTMCP_HTTP_ENTRY_PATH` from `transport.http.path` (or a `deployments` entry's `server.http.entryPath`), unless it's already set. |
| A `distributed`, `vercel`, `lambda` or `cloudflare` build | `http.entryPath`, else `transport.http.path`: the build writes it into `serverless-setup.js` as `FRONTMCP_HTTP_ENTRY_PATH`. |
| Anything else: `tsx src/main.ts`, your own `tsc` output, [`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler) in your code | `http.entryPath`, else `FRONTMCP_HTTP_ENTRY_PATH` if you set it, else `/`. `frontmcp.config` isn't read. |

An explicit `http.entryPath` in `@FrontMcp` always wins over `FRONTMCP_HTTP_ENTRY_PATH`. Changed in 1.9: before, `node dist/node/<name>.bundle.js` served `/`, whatever `frontmcp.config` said, so a Docker image answered clients at `/mcp` with `404`. Changed in 1.8.7: before, only `frontmcp dev` and a `cloudflare` build served `transport.http.path`; the `node` start script, `vercel`, `lambda` and `distributed` served `/`.

The `distributed`, `vercel`, `lambda` and `cloudflare` builds end with `Server will serve MCP at <path>`: the `http.entryPath` of your entry file, else `transport.http.path`. Since 1.9 the build reads the entry with the files it imports, so a Cloudflare build with `http: { entryPath: "/api" }` in a `main.ts` that imports its app prints `/api`. (In 1.8.7 it could read the decorator only from an entry that imported no other TypeScript files, and printed `/mcp` there.)

> **Pitfall: A server started without the frontmcp command answers at /**
A project made with `frontmcp create` sets the path in `frontmcp.config` only. `frontmcp dev` and every build serve `/mcp`, but `tsx src/main.ts`, your own `tsc` output or a `createFetchHandler()` in your code serve `/`, and a client pointed at `/mcp` gets `404`. Set the path in the server itself, so every way of starting it agrees:

```ts main.ts
@FrontMcp({
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { entryPath: "/mcp" },
})
export default class Server {}
```

### Ways to run a server

The targets are packaged forms of the entry points FrontMCP has. The environment variables choose between them when the `@FrontMcp` class is imported; [`FrontMcpInstance`](https://frontmcp.dev/reference/sdk/frontmcp-instance#what-frontmcp-does-on-import) has the details.

| Entry point | Chosen by | Used by | Answers |
| --- | --- | --- | --- |
| HTTP server ([`bootstrap()`](https://frontmcp.dev/reference/sdk/frontmcp-instance)) | Importing the class | `node`, `distributed`, `cli serve`, `frontmcp dev` | Streamable HTTP, the older SSE transport, sessions, `/healthz`, `/readyz`, `/metrics` |
| Unix socket | `FRONTMCP_DAEMON_SOCKET=<path>`, `http.socketPath`, or [`runUnixSocket()`](https://frontmcp.dev/reference/sdk/frontmcp-instance#frontmcpinstancerununixsocketoptions) | `node`, with the variable | The same, on a socket file. See [Serving on a Unix socket](https://frontmcp.dev/reference/deployment/node#serving-on-a-unix-socket). |
| stdio | `FRONTMCP_STDIO=1`, or [`runStdio()`](https://frontmcp.dev/reference/sdk/frontmcp-instance#frontmcpinstancerunstdioconfigorclass) | `node` and `cli`, with `--stdio` | One client, on stdin and stdout |
| Node request handler ([`createHandler()`](https://frontmcp.dev/reference/sdk/frontmcp-instance#frontmcpinstancecreatehandlerconfig)) | `FRONTMCP_SERVERLESS=1` | `vercel`, `lambda` | Like the HTTP server. These builds also set `FRONTMCP_DEPLOYMENT_MODE=serverless`, which turns `/readyz` off. |
| Web fetch handler ([`createFetchHandler()`](https://frontmcp.dev/reference/sdk/create-fetch-handler)) | `FRONTMCP_SERVERLESS=1` and `FRONTMCP_WORKER=1` | `cloudflare` | MCP at one path, fixed health answers, no sessions |
| In-process ([`createDirect()`](https://frontmcp.dev/reference/sdk/create), [`connect()`](https://frontmcp.dev/reference/sdk/connect)) | Your code | | Calls from your own process, no transport |

#### Caveats

- `frontmcp create` pins FrontMCP's packages to the CLI's own version (`1.9.3`, exactly). With the 1.8.6 CLI they were `~1.8.6`, so a new project got the newest 1.8 patch that was published.
- The `node` and `cli` start scripts refuse Node older than 22, or the range in `nodeVersion` in `frontmcp.config`.
- The `sdk` bundle is your entry file, so importing a bundle built from a file with `@FrontMcp` starts the HTTP server. Point `--entry` at a file that exports your apps or configuration instead.
- The `browser` bundle is your code only; it imports `@frontmcp/sdk` and `reflect-metadata` for your web app's bundler to resolve. It wasn't run for this page. FrontMCP's browser build serves [one request at a time](https://frontmcp.dev/reference/sdk/create-fetch-handler#in-a-browser).
- The generated `ci/Dockerfile` (since 1.8.7), `ci/template.yaml` (since 1.8.7) and `vercel.json` (since 1.8.6) match what the builds write. Since 1.9.2 the Dockerfile runs as the `node` user with a health check (on `/health` since 1.9.3), and the Lambda template sets `NODE_ENV` and asks for `MCP_SESSION_SECRET`: see [Node.js and Docker](https://frontmcp.dev/reference/deployment/node#docker) and [AWS Lambda](https://frontmcp.dev/reference/deployment/aws-lambda#deploying-with-sam).
- The HTTP server that the `node` and `distributed` builds start handles `SIGTERM` and `SIGINT`: it finishes requests in flight and exits `0` ([stopping](https://frontmcp.dev/reference/deployment/node#stopping)). So does a server on a Unix socket, since 1.9.3.

---

## Usage

Everything on this page runs the CLI, so none of it is in the Playground. Each block was run with `frontmcp` 1.9.3 in a project made with `frontmcp create` (building every target at once, with 1.9.1), and each result shown is what it printed or served.

### Building and running a Node server

```bash
npx frontmcp build --target node
```

```text
[build:exec] bundle created: dist/node/help-desk.bundle.js (6.6 KB)
[build:exec] manifest: dist/node/help-desk.manifest.json
[build:exec] runner: dist/node/help-desk
[build:exec] installer: dist/node/install-help-desk.sh
```

Start it, next to the project's `node_modules`, and ask it for its health:

```bash
PORT=8080 node dist/node/help-desk.bundle.js
curl http://127.0.0.1:8080/healthz
```

```json
{"status":"ok","server":{"name":"help-desk","version":"1.0.0"},"runtime":{"platform":"darwin","runtime":"node","deployment":"standalone","env":"development"},"uptime":2.845}
```

[Node.js and Docker](https://frontmcp.dev/reference/deployment/node) covers running it for real: the start script, environment, images and proxies.

### Building every target at once

List the targets in `frontmcp.config`, and `frontmcp build` builds each of them:

```ts frontmcp.config.ts
import { defineConfig } from "frontmcp";

export default defineConfig({
  name: "help-desk",
  version: "1.2.0",
  entry: "./src/main.ts",
  deployments: [{ target: "node" }, { target: "cloudflare", wrangler: { name: "help-desk" } }],
  transport: { default: "http", http: { port: 3000, path: "/mcp" } },
});
```

```text
[build] Building 2 target(s) from frontmcp.config: node, cloudflare
[build] ═══ node ═══
[build:exec] bundle created: dist/node/help-desk.bundle.js (6.7 KB)
…
[build] ═══ cloudflare ═══
  Updated wrangler.toml (managed keys only)
Server will serve MCP at /mcp
```

`--target` builds one of them, or any other target, whatever the file lists.

### Checking a build before you deploy

Most failures that stop a server from starting show up in a Node process too. Run the bundle, or the handler, the way the platform will, with the platform's environment, and look for `FrontMCP bootstrap complete` (`FrontMCP handler created (serverless mode)` for a Vercel or Lambda handler) and a `200` from `/healthz`, then make one MCP call: a missing secret only shows there. Two differences to allow for:

- Vercel and Lambda bundles read `NODE_ENV` when they start, and the platform's setting decides: run them with `NODE_ENV=production`, and test older clients with `MCP_SESSION_SECRET` set. [Vercel](https://frontmcp.dev/reference/deployment/vercel#trying-the-function-locally) and [Lambda](https://frontmcp.dev/reference/deployment/aws-lambda#calling-the-handler-locally) show how to call them.
- Run a Cloudflare build with `npx wrangler dev`, which uses the same runtime as Cloudflare. It's in production when `[vars]` says `NODE_ENV = "production"`, as the `wrangler.toml` of `frontmcp create` does: see [checking production locally](https://frontmcp.dev/reference/deployment/cloudflare-workers#checking-production-locally).

Checks that run while the server is built, like an `approval` no plugin enforces, fail `createDirect()` too, so a test can catch them without building at all: see [Catching a misconfigured server at startup](https://frontmcp.dev/reference/sdk/create-fetch-handler#catching-a-misconfigured-server-at-startup).

---

## Troubleshooting

### `Module not found: Can't resolve '@frontmcp/storage-sqlite'`

A `vercel` or `lambda` build of 1.8.6 or earlier, which stopped at the first optional package that wasn't installed. From 1.8.7 the build leaves such packages out and finishes. If it still stops, upgrade `frontmcp`. See [what's in the bundle](#whats-in-the-bundle).

### `Module not found: Can't resolve '@upstash/redis'`

A `vercel` or `lambda` build of 1.9.2 in a project without `@upstash/redis`. Upgrade `frontmcp` to 1.9.3, which leaves the package out when it isn't installed, or run `npm install @upstash/redis`. See [what's in the bundle](#whats-in-the-bundle).

### `[--target lambda] missing required peer dependency @codegenie/serverless-express`

The Lambda entry adapts Lambda's events with `@codegenie/serverless-express`, which you install yourself: `npm install @codegenie/serverless-express`. See [AWS Lambda](https://frontmcp.dev/reference/deployment/aws-lambda).

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

The `@FrontMcp` source names `sqlite`, or a `redis` that isn't written as `redis: { provider: "vercel-kv" }`, which Workers can't use, or turns on `tasks` without `tasks.redis`. See [Cloudflare Workers](https://frontmcp.dev/reference/deployment/cloudflare-workers#storage).

### `Cannot find module '@frontmcp/sdk'`, or `Cannot find module 'reflect-metadata'`

The `node`, `cli`, `sdk` and `mcpb` bundles import FrontMCP's packages instead of containing them. Run the bundle where `npm install --omit=dev` has installed your dependencies, or copy `node_modules` next to it, as [the Dockerfile](https://frontmcp.dev/reference/deployment/node#building-a-docker-image) does.

### A client gets `404` at `/mcp` after deploying

The server serves `/`, because the path was only set in `frontmcp.config` and the server was started without the `frontmcp` command, or is a `node` build of 1.8.7 or earlier, which a Docker image runs. Set `http: { entryPath: "/mcp" }` in `@FrontMcp`, [as above](#the-endpoint-path), or set `FRONTMCP_HTTP_ENTRY_PATH=/mcp`, or point the client at the root. A `vercel`, `lambda`, `distributed` or `cloudflare` build of 1.8.6 or earlier also served `/`.

### `Error: Unknown build target: …`

`--target` takes `cli`, `node`, `sdk`, `browser`, `cloudflare`, `vercel`, `lambda`, `distributed` or `mcpb`. There's no `docker` target: build `node` and put it in an image.
