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 takes one server from frontmcp build to a connected client, step by step.
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, 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. |
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 |
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 |
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 |
lambda | dist/lambda/handler.cjs, one bundle that exports handler | An AWS Lambda handler | 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 |
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 |
mcpb | dist/mcpb/<name>-<version>.mcpb, a ZIP archive | Installed by a desktop client | Connecting MCP clients |
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 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.
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 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. |
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() 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.)
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 has the details.
| Entry point | Chosen by | Used by | Answers |
|---|---|---|---|
HTTP server (bootstrap()) | 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() | node, with the variable | The same, on a socket file. See Serving on a Unix socket. |
| stdio | FRONTMCP_STDIO=1, or runStdio() | node and cli, with --stdio | One client, on stdin and stdout |
Node request handler (createHandler()) | 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()) | FRONTMCP_SERVERLESS=1 and FRONTMCP_WORKER=1 | cloudflare | MCP at one path, fixed health answers, no sessions |
In-process (createDirect(), connect()) | Your code | Calls from your own process, no transport |
Caveats
frontmcp createpins 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
nodeandclistart scripts refuse Node older than 22, or the range innodeVersioninfrontmcp.config. - The
sdkbundle is your entry file, so importing a bundle built from a file with@FrontMcpstarts the HTTP server. Point--entryat a file that exports your apps or configuration instead. - The
browserbundle is your code only; it imports@frontmcp/sdkandreflect-metadatafor your web app's bundler to resolve. It wasn't run for this page. FrontMCP's browser build serves one request at a time. - The generated
ci/Dockerfile(since 1.8.7),ci/template.yaml(since 1.8.7) andvercel.json(since 1.8.6) match what the builds write. Since 1.9.2 the Dockerfile runs as thenodeuser with a health check (on/healthsince 1.9.3), and the Lambda template setsNODE_ENVand asks forMCP_SESSION_SECRET: see Node.js and Docker and AWS Lambda. - The HTTP server that the
nodeanddistributedbuilds start handlesSIGTERMandSIGINT: it finishes requests in flight and exits0(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
npx frontmcp build --target node[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:
PORT=8080 node dist/node/help-desk.bundle.js
curl http://127.0.0.1:8080/healthz{"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 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:
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" } },
});[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_ENVwhen they start, and the platform's setting decides: run them withNODE_ENV=production, and test older clients withMCP_SESSION_SECRETset. Vercel and Lambda 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]saysNODE_ENV = "production", as thewrangler.tomloffrontmcp createdoes: see 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.
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.
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.
[--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.
[--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.
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 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, 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.