# frontmcp CLI

> Every command and option of the frontmcp command-line tool: create, init, doctor, dev, build and its nine targets, test, inspector, the process and package managers, skills, mcpb, eject-mcp-config, plugin and your own project commands, with their output, exit codes and the files they read.

Source: https://frontmcp.dev/reference/cli

`frontmcp` is FrontMCP's command-line tool. `create` scaffolds a project, `dev` runs it with reload and a type checker, `build` packages it for one of nine targets, `test` runs its end-to-end tests under Jest, and `inspector` opens the MCP Inspector. Around those are smaller groups: a process manager (`start`, `stop`, `logs`, …), a package manager for built servers (`install`, `configure`, `uninstall`), the skills catalog, `.mcpb` bundles, a client-config snippet generator, and commands you define yourself in `frontmcp.config`. Everything on this page was run with `frontmcp@1.9.2`, and `create`, `doctor`, `dev`, `test`, `build` for every target, `start`, `stop`, `list`, `status`, `install` and `mcpb validate` again with `frontmcp@1.9.4`, as were a `cli` build's own `install` and sign-in commands and the `.mcpb` manifest fields.

```bash
frontmcp <command> [options]
npx frontmcp create <name> [options]      # without installing anything
frontmcp --config <path> <command>        # use a frontmcp.config elsewhere
```

---

## Reference

### Installing and running it

`frontmcp create` adds the CLI to the new project's `dependencies`, pinned to its own version (`"frontmcp": "1.9.4"`), and wires the `package.json` scripts to it, so inside a project you run it with `npm run dev`, `npx frontmcp dev` or `./node_modules/.bin/frontmcp dev`. In a project of your own, install it yourself:

```bash
npm install -D frontmcp@1.9.4
npx frontmcp --version
```

```text
1.9.4
```

The package declares Node 24 or later (`engines`). `frontmcp doctor` accepts Node 22.

#### Global options

| Option | Description |
| --- | --- |
| `-V`, `--version` | Print the CLI's version and exit. |
| `-h`, `--help` | Print help. `frontmcp` with no command prints the same help and exits with `1`. |
| `-c`, `--config <path>` | The `frontmcp.config` file to use, instead of searching up from the current folder and instead of `FRONTMCP_CONFIG`. Every command that reads a config honours it, `build` and its nine targets included, before or after the command name: `frontmcp build -t node --config ci/frontmcp.config.json`. |
| `--list-commands` | Print every command, one per line, as `name`, `[built-in]` or `[project]`, and its description, tab-separated. Only before the command name. |

`frontmcp <command> --help` prints a command's options, and so does `--help` after a subcommand: `frontmcp skills install --help`, `frontmcp mcpb validate --help`, `frontmcp plugin install --help`. Before 1.8.7 those printed the top-level help.

### Commands

| Group | Command | What it does |
| --- | --- | --- |
| Getting started | [`create [name]`](#frontmcp-create) | Scaffold a project, or an Nx workspace with `--nx`. |
| | [`init`](#frontmcp-init) | Create `tsconfig.json`, or add the settings FrontMCP needs to the one you have. |
| | [`doctor`](#frontmcp-doctor) | Check Node, npm, `tsconfig.json` and the entry file. |
| Development | [`dev`](#frontmcp-dev) | Run the server with `tsx --watch`, and `tsc --noEmit --watch` beside it. |
| | [`build`](#frontmcp-build) | Build for a target: `node`, `cli`, `sdk`, `browser`, `vercel`, `lambda`, `cloudflare`, `distributed` or `mcpb`. |
| | [`test [patterns...]`](#frontmcp-test) | Run Jest with a generated configuration. |
| | [`inspector`](#frontmcp-inspector) | Run `npx @modelcontextprotocol/inspector`, pointed at the server `frontmcp.config` describes. |
| Process manager | [`start`, `stop`, `restart`, `status`, `list`, `logs`, `socket`, `service`](#process-manager) | Run a server under a supervisor that restarts it, and write service files for it. |
| Package manager | [`install`, `uninstall`, `configure`](#package-manager) | Copy a built server into `~/.frontmcp/apps/`. |
| Skills | [`skills search`, `list`, `install`, `export`, `publish`, `read`](#skills) | Browse and copy the FrontMCP skills catalog. |
| Other | [`mcpb validate <path>`](#frontmcp-mcpb-validate) | Check a `.mcpb` archive. |
| | [`eject-mcp-config <client>`](#frontmcp-eject-mcp-config) | Print a client's MCP server entry from `frontmcp.config`'s `clients`, or merge it into the client's file. |
| | [`plugin install`, `uninstall`, `status`](#frontmcp-plugin) | Write the project out as a plugin for a coding-agent host. |
| | [Your own commands](#project-commands) | Anything in `frontmcp.config`'s `cli.commands`. |

> **Note**
**Changed in 1.9.** What 1.8.7 got wrong here now works: [`dev --stdio`](#dev---stdio) serves a stdio client, the `--sea` binary in a `.mcpb` archive starts, a server added with `install` runs without extra packages, `init` keeps a `tsconfig.json` that has comments, `kill <pid>` of `frontmcp dev` stops its server too, and `dev` and `build` work from a subfolder of the project.

### `frontmcp create`

```bash
frontmcp create [name] [options]
```

| Option | Values | Default | Description |
| --- | --- | --- | --- |
| `name` | | `frontmcp-app` | The project's folder and `package.json` name. It's lowercased, and characters other than letters, digits, `.`, `_` and `-` become `-`: `"My Desk"` makes `my-desk`. |
| `-y`, `--yes` | | | Don't ask; use the defaults and the flags given. |
| `--target <target>` | `node`, `vercel`, `lambda`, `cloudflare` | `node` | The deployment target: which files and scripts are added, and `deployments` in `frontmcp.config.ts`. Any other value stops the command before it writes anything, with `Error: Invalid --target value 'deno'. Expected one of: node, vercel, lambda, cloudflare.` and exit `1`. |
| `--redis <setup>` | `docker`, `existing`, `none` | `docker` | `node` only. `docker` adds a Redis service to `ci/docker-compose.yml`; `existing` adds `REDIS_*` variables to `.env.example` instead; `none` adds neither. |
| `--pm <pm>` | `npm`, `yarn`, `pnpm` | `npm` | Used in the generated scripts, Dockerfile and workflows. `yarn` also writes `.yarnrc.yml` (`nodeLinker: node-modules`). |
| `--cicd`, `--no-cicd` | | on | Write `.github/workflows/ci.yml`, `e2e.yml` and `deploy.yml`. |
| `--skills <bundle>` | `recommended`, `minimal`, `full`, `none` | `recommended` | Copy skills from the catalog into `skills/`: 9, 4, all 13, or none. |
| `--nx` | | | Scaffold an Nx workspace instead, with [`@frontmcp/nx`](https://frontmcp.dev/reference/nx). Only `--pm` applies with it. |

An invalid `--pm`, `--redis` or `--skills` stops the same way. Without `--yes`, `create` asks for what the flags didn't say: the name, standalone or Nx, the target, Redis (for `node`), the package manager and GitHub Actions. When its input isn't a terminal, as in CI or a script, it asks nothing and uses the defaults, as if `--yes` were given. It refuses a folder that exists and isn't empty.

What it writes, for the default `node` target:

| Path | Contents |
| --- | --- |
| `src/main.ts`, `src/calc.app.ts`, `src/tools/add.tool.ts` | A server with one app and an `add` tool. |
| `e2e/server.e2e.spec.ts` | Four tests `frontmcp test` finds. |
| `frontmcp.config.ts` | `name`, `entry`, one deployment, `transport.http` on port `3000` at `/mcp`, `env.dev`, and one `clients` entry for [`eject-mcp-config`](#frontmcp-eject-mcp-config). |
| `package.json` | Scripts `dev`, `build`, `inspect`, `doctor`, `test` and, for `node`, `docker:up`, `docker:down`, `docker:build`, which pass `--env-file ci/.env.docker` to `docker compose`. `lambda` adds `deploy`; `cloudflare` adds `deploy` and `dev:worker`. |
| `tsconfig.json` | Written by [`init`](#frontmcp-init). |
| `ci/Dockerfile`, `ci/docker-compose.yml`, `ci/.env.docker`, `ci/.env.docker.example`, `.dockerignore` | `node` only. The image runs `node dist/node/<name>.bundle.js` as the `node` user, with `NODE_ENV=production`, `FRONTMCP_BIND_ADDRESS=all` and a `HEALTHCHECK` on `/health` (on `/healthz` in 1.9.2). The compose file publishes Redis on `127.0.0.1` only, defaults `NODE_ENV` to `production`, and refuses to start until `MCP_SESSION_SECRET` is set in `ci/.env.docker` (git-ignored; `ci/.env.docker.example` is its committed copy): `required variable MCP_SESSION_SECRET is missing a value`. [Node.js and Docker](https://frontmcp.dev/reference/deployment/node#docker) has the details. |
| `ci/template.yaml`, `vercel.json`, `wrangler.toml` | `lambda` writes `ci/template.yaml` (`CodeUri: ../dist/lambda/`, `Handler: handler.handler`, `NODE_ENV: production`, and `MCP_SESSION_SECRET` from a required `NoEcho` parameter, `McpSessionSecret`, which its description says to generate once and reuse on every deploy) and adds `@codegenie/serverless-express` `^5.0.0` to `dependencies`, which the `lambda` build needs. `vercel` writes `vercel.json` with an `installCommand` and the `buildCommand` `npx frontmcp build --target vercel` (or the `yarn` or `pnpm exec` form for `--pm`; before 1.8.6, `builds` for a `dist/main.js` that nothing writes). `cloudflare` writes `wrangler.toml` with `compatibility_date = "2024-11-11"` and `[vars] NODE_ENV = "production"`. |
| `.env.example`, `.gitignore`, `.nvmrc`, `README.md` | |
| `AGENTS.md` and a second agent-instructions file | Notes for coding agents, listing the skills. |
| `skills/<name>/` | The skills bundle. |

It doesn't install anything: run `npm install` (or your package manager) next. It runs `git init` and commits everything as `Initial commit` when `git` is installed.

The `@frontmcp/*` dependencies are pinned to the CLI's own version, `"1.9.4"`, with no range, so a project keeps the version that made it until you change it. The dev dependencies include Jest `^30.0.0`, `@swc/jest` and `tsx`.

### `frontmcp init`

Creates `tsconfig.json`, or adds what FrontMCP needs to the one that's there. It takes no options.

- **Forced:** `target` becomes `es2021`, `module` `esnext`, `experimentalDecorators` and `emitDecoratorMetadata` `true`, whatever they were.
- **Replaced when it can't go with `module: "esnext"`:** a `moduleResolution` of `node16` or `nodenext` becomes `node`. `bundler`, `node10` and `classic` stay.
- **Added when missing:** `strictFunctionTypes`, `moduleResolution: "node"`, `strict`, `esModuleInterop`, `resolveJsonModule`, `skipLibCheck`, `sourceMap`, `outDir: "dist"`, `rootDir: "src"`, `types: ["node", "@types/jest", "@frontmcp/testing"]` and `include: ["src/**/*"]`. Values you already have win.
- **Always:** `**/*.widget.tsx` and `**/*.widget.jsx` are added to `exclude`.

It reads the file the way TypeScript does, comments and trailing commas included, and edits only the keys it changes: a comment beside `"target": "es2020"` is still there after `target` becomes `es2021`. It prints `tsconfig.json verified and updated (required decorator settings enforced).` for a file it changed, `tsconfig.json verified (required decorator settings already present).` for one it left as it was, and `tsconfig.json not found — creating one in ..` then `Created tsconfig.json with required decorator settings.` for one it wrote new. A file it can't parse is left alone, and `init` exits `1`:

```text
Error: tsconfig.json is not valid JSON: CommaExpected at line 4, column 5. It was left unchanged — fix it, then run "frontmcp init" again.
```

Before 1.9, `init` read the file as strict JSON, took a commented `tsconfig.json` for a missing one, and wrote a new file over it.

> **Pitfall: init adds settings you may not want**
`init` adds `rootDir: "src"` even when your `include` points elsewhere (`include: ["lib/**/*"]` keeps its value, and gets `rootDir: "src"` beside it), and `types` that fail to resolve until `@types/jest` and `@frontmcp/testing` are installed. Review the diff after running it.

### `frontmcp doctor`

Checks the environment and prints one line per check. It takes no options.

```text
✅ Node 24.18.1 (min 22.0.0)
✅ npm 11.16.0 (min 10.0.0)
✅ tsconfig.json found
  ✓ compilerOptions.target = "es2021"
  ✓ compilerOptions.module = "esnext"
  ✓ compilerOptions.emitDecoratorMetadata = true
  ✓ compilerOptions.experimentalDecorators = true
✅ entry detected: src/main.ts

All checks passed. You are ready to go!
```

A failing check prints `❌` or `•`, and the run ends with `Some checks failed. See above for fixes.` and exit code `1`, so a CI step that runs `doctor` fails. Before 1.8.7 the exit code was `0`.

```text
✅ Node 24.18.1 (min 22.0.0)
✅ npm 11.16.0 (min 10.0.0)
✅ tsconfig.json found
  • compilerOptions.target should be "es2021"
  • compilerOptions.module should be "esnext"
  • compilerOptions.emitDecoratorMetadata should be true
  • compilerOptions.experimentalDecorators should be true
  -> Run "frontmcp init" to apply the required settings.
❌ entry not detected — No entry file found.

Some checks failed. See above for fixes.
```

A `tsconfig.json` that doesn't parse fails its check with the place it stopped: `❌ tsconfig.json is not valid JSON: CommaExpected at line 4, column 5 — fix it, then run frontmcp init`. Comments and trailing commas parse.

The entry is found the way every command finds it: `package.json`'s `main` (trying `.ts`, `.tsx`, `.js`, `.mjs`, `.cjs` and `index.*` in it), then `src/main.ts`.

### `frontmcp dev`

```bash
frontmcp dev [options]
```

Starts two processes and keeps them running until Ctrl+C: `tsx --conditions node --watch <entry>`, which restarts the server on every change to a file it imported, and `tsc --noEmit --pretty --watch`, which prints type errors beside it. Type errors don't stop the server: it restarts with the new code anyway. Both are the project's own, from `tsx` and `typescript` in its `node_modules` (`frontmcp create` adds them), run with Node; a project without them gets `npx -y tsx` and `npx -y --package typescript tsc` instead. With no `npx` on the `PATH`, `dev` started both and served.

Changed in 1.9.4: both always ran through `npx -y`, which `dev` couldn't start on Windows. The CLI now starts every command without a shell, and on Windows runs npm and npx as npm's own scripts with Node, so `dev` works there too. This page wasn't run on Windows.

| Option | Description |
| --- | --- |
| `-e`, `--entry <path>` | The server's entry file. Default: `frontmcp.config`'s `entry`, then [the usual search](#frontmcp-doctor). A path that doesn't exist fails with `Entry override not found: <path>`. A path you type is resolved from the folder you type it in. |
| `-p`, `--port <port>` | The port, passed to the server as `PORT`. Default: `transport.http.port`, then `PORT`, then `3000`. |
| `--auto-port` | When the port is taken, use the next free one instead of stopping. A port counts as taken when anything listens on it, on `127.0.0.1` or on every interface (`0.0.0.0`). Before 1.9.2, on macOS, a `0.0.0.0` listener didn't count, and `dev` started anyway on `127.0.0.1`. |
| `--show-conflict` | When the port is taken, print which process holds it (`lsof`). |
| `--stdio` | Act as a stdio bridge for an MCP client: [below](#dev---stdio). |
| `--serve`, `--log-file <path>`, `--buffer-size <n>`, `--reload-deadline-ms <ms>` | Options of the stdio bridge. `--log-file` defaults to `.frontmcp/dev.log` in the project's folder. |

Before starting, `dev` loads `.env` and `.env.local` into the environment, adds `env.shared` and `env.dev` from `frontmcp.config` for the variables that aren't set already, sets `PORT`, and, when `transport.http.path` is set, `FRONTMCP_HTTP_ENTRY_PATH`. It also passes the `server.csp` and `server.headers` of the first deployment that has a `server` block as `FRONTMCP_*` variables, so the dev server sends the same security headers as a build. [Configuration files](https://frontmcp.dev/reference/server/config-files#env-and-the-port-in-frontmcp-dev) has the order in full. An `http.port` or `http.entryPath` written in `@FrontMcp` wins over both variables.

Run from a subfolder of the project, `dev` finds `frontmcp.config` above it and runs from the config's folder, so the config's `entry` and `.env` are found as from the root. It says so first: `[dev] project root: /Users/you/help-desk (frontmcp.config found above /Users/you/help-desk/src)`. With `--config`, it stays in the folder you run in.

```text
[dev] loaded 2 environment variables from .env files
[dev] using entry: src/main.ts
[dev] config: /Users/you/help-desk
[dev] listening on port: 3000
[dev] MCP endpoint path: /mcp
[dev] starting tsx --watch and tsc --noEmit --watch (async type-checker)
hint: press Ctrl+C to stop
```

Ctrl+C stops both processes, and so does signalling the `frontmcp dev` process alone, with `kill <pid>` (`SIGTERM`) or a process manager's stop: `dev` stops the whole process tree it started, waits for it (forcing it after 2 seconds), and exits `0`, and the port is free again. Before 1.9, a `kill` of `frontmcp dev` left the server listening.

#### `dev --stdio`

`frontmcp dev --stdio` is for an MCP client that starts its servers as commands and talks to them over stdin and stdout. It starts the server the way `frontmcp dev` does, with the same config, `.env` and port (`--port`, else `transport.http.port`, else `PORT`; with none of them, `3000` or the next free port after it), and passes each JSON-RPC message between the client and the server's HTTP endpoint, on the session the server gives it. The server's own output goes to `.frontmcp/dev.log`, so stdout carries only MCP messages.

- When a file the server imports changes, the bridge restarts the server and keeps the client connected: the client gets `notifications/tools/list_changed`, and its next `tools/list` sees the new code. State the server kept for the session starts again.
- When the client closes stdin, the bridge stops the server and exits `0`.
- `--serve` runs the server as the bridge's own child and talks to it over a pipe instead of a port. It needs `tsx` in the project (`frontmcp create` adds it); without it, the bridge stops at once with ``Error: `frontmcp dev --stdio --serve` runs a TypeScript entry with the project's tsx, which is not installed.`` and exit `1`.
- A server whose `@FrontMcp` sets `http: { socketPath }` listens on a Unix socket, which the bridge can't reach without `--serve`. The bridge stops it and says why on stderr, ``[frontmcp dev] the server listens on the Unix socket ./dev.sock (http.socketPath), which the HTTP loopback cannot reach; run `frontmcp dev --stdio --serve` to talk to it over a stdio pipe instead``, and each request the client sent gets an error with the same text in its `data`. With `--serve` that server works.

[Usage](#connecting-a-stdio-client-while-you-work) shows a client entry for it. In 1.8.7 the bridge answered `initialize` and failed every request after it with `dev_server_unreachable`.

### `frontmcp build`

```bash
frontmcp build [options]
```

| Option | Description |
| --- | --- |
| `-t`, `--target <target>` | One target. Without it, `build` builds every target in `frontmcp.config`'s `deployments`, in order, and `node` when there's no config file. |
| `-o`, `--out-dir <dir>` | The base folder. Each target writes to `<dir>/<target>/`. Default `dist`, in the project's folder. A deployment's `outDir` wins over it. |
| `-e`, `--entry <path>` | The entry file, resolved from the folder you type it in. It wins over `frontmcp.config`'s `entry` for every target. |
| `--js` | `cli` only: a JavaScript bundle instead of a single executable. |
| `--sea` | `mcpb` only: add a single-executable binary for this machine's platform ([The `mcpb` target](#the-mcpb-target)). |
| `--merge-from <dir>` | `mcpb` only: a folder of single-executable binaries built on other platforms, to add: `<dir>/<platform>/<name>`, like `linux-x64/help-desk`. |
| `--icon <path>` | `mcpb` only: the icon to put in the archive. It wins over the deployment's `icon`, and a path that doesn't exist is skipped without a warning: see [bundle metadata](#bundle-metadata-and-user-settings). |
| `--no-deterministic` | `mcpb` only: don't make the archive reproducible. By default every entry in it is dated 2000-01-01, so the same sources give the same file. |
| `--stage-only` | `mcpb` only: leave the files in `<out>/mcpb/__stage/` and skip the zip. |
| `--no-clean` | Don't empty the target's folder first. For `node` and `cli`, the intermediate files the build writes there are deleted anyway ([Caveats](#caveats)). |

Before each target, `build` empties its folder, unless the folder is the project, contains the project or is outside it, in which case it warns (`skipping clean of … it is the project root`) and skips the cleaning.

`build` finds `frontmcp.config` like every other command: `--config`, then `FRONTMCP_CONFIG`, then the nearest `frontmcp.config.*` in the current folder or above it. A config found above the current folder makes its folder the project's: `build` runs from there (`[build] project root: … (frontmcp.config found above …)`), so its `entry` and `dist` are where they'd be from the root. The name and version in file names and manifests come from its `name` and `version`. A config without `version` uses the project's `package.json` version, and `1.0.0` when there's none; without a config file both come from `package.json`.

#### The targets

Each row was built from a project made by `frontmcp create` at 1.9.4.

| Target | Writes to `dist/<target>/` | In 1.9.4 |
| --- | --- | --- |
| [`node`](#the-node-target) | `<name>.bundle.js` (your code; packages stay imports), `<name>` (a shell runner), `<name>.manifest.json`, `install-<name>.sh` | Builds and runs where `node_modules` is. |
| [`cli`](#the-cli-target) | The same plus `<name>-cli.bundle.js` and `bin-meta.json`, and without `--js` a single executable (`<name>-cli-bin`, about 127 MB on macOS) | Builds, with or without `--js`. |
| `sdk` | `<name>.cjs.js`, `<name>.esm.mjs`, `.d.ts` files and the compiled sources | Builds. |
| `browser` | `<name>.browser.mjs` and its source map | Builds. |
| `vercel` | `index.js` and `serverless-setup.js`, then a `handler.cjs` bundle, `.vercel/output/`, and `vercel.json` in the project when there's none | Builds, without a warning (in 1.9.2 it stopped on `@upstash/redis`: [Troubleshooting](#module-not-found-cant-resolve-upstashredis)). The bundle starts, answers at `/mcp`, and reads `NODE_ENV` when it runs, not when it was built. |
| `lambda` | The same, for Lambda | Stops with `missing required peer dependency @codegenie/serverless-express` until that package is installed (`create --target lambda` adds it), then builds as `vercel` does, with the package bundled into `handler.cjs`. |
| `cloudflare` | `index.js` (a module worker), `serverless-setup.js`, a `package.json` with `"type": "module"` and the compiled sources, and `wrangler.toml` in the project: created the first time, with `main` relative to `--out-dir`, and afterwards only the keys the build owns are updated (`Updated wrangler.toml (managed keys only)`), so your settings and comments stay | Builds, with `Cloudflare Workers adapter is experimental`. It accepts `redis: { provider: "vercel-kv" }` and still refuses an ioredis-style `redis` block. |
| `distributed` | `index.js` and `serverless-setup.js`, which sets `FRONTMCP_DEPLOYMENT_MODE=distributed` | Builds. |
| [`mcpb`](#the-mcpb-target) | `<name>-<version>.mcpb`, a zip with `manifest.json`, `server/index.js` and `README.md` | Builds, validates and runs on its own: `server/index.js` carries the runtime, 10 MB. With `--sea`, the binary in it runs too, and the manifest points a host at it once every architecture of that OS is in the archive ([below](#the-mcpb-target)). |

The adapter targets print where the server will answer: `Server will serve MCP at /mcp`. The path is `http.entryPath` from `@FrontMcp` when the decorator sets one, else `transport.http.path` (a `serverless-setup.js` in the output sets `FRONTMCP_HTTP_ENTRY_PATH` to it), else `/`. To read the decorator, the build bundles the entry with the files it imports, so a `main.ts` that imports `./calc.app`, as the scaffold's does, is read too: with `http: { entryPath: "/api/mcp" }` in it, the line says `/api/mcp`. Before 1.9 the build read the decorator only from an entry that imported none of your other files, and printed the config's path even when the decorator overrode it.

Since 1.8.6, `build` also copies the `*.widget.tsx` and `*.widget.jsx` files of tools with a `file` widget into the output, and says `copied 1 widget source file (*.widget.tsx/jsx) to dist/node`: beside the bundle for `node`, `cli`, `vercel` and `lambda`, in the compiled tree for `cloudflare` and `distributed`, and not at all for `sdk`, `browser` and `mcpb`. [Production build](https://frontmcp.dev/reference/deployment/production-build#widget-sources) has the details.

#### The `node` target

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

```text
[build] cleaned dist/node
[build:exec] Building executable bundle...
[build:exec] name: help-desk
[build:exec] version: 0.1.0
[build:exec] entry: src/main.ts
[build:exec] compiling TypeScript...
[build:exec] TypeScript compiled.
[build:exec] bundling with esbuild...
[build:exec] bundle created: dist/node/help-desk.bundle.js (4.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
[build:exec] cleaned 4 intermediate file(s)

Executable build completed.
…
Run the server: ./dist/node/help-desk
```

The runner, `dist/node/<name>`, checks Node's version, loads a `.env` next to itself, and runs the bundle. It takes these flags and no others (anything else starting with `--` exits `2`):

| Runner flag | Does |
| --- | --- |
| none | Serve over HTTP on `PORT`, or `3000`. |
| `--stdio` | Serve over stdin and stdout instead (sets `FRONTMCP_STDIO=1`). |
| `--version`, `--help`, `--print-manifest` | Print and exit. |

The bundle imports `@frontmcp/sdk` and your other packages at run time, so run it where they're installed, or ship `node_modules` with it. When `frontmcp.config` sets `transport.http.path`, the runner sets `FRONTMCP_HTTP_ENTRY_PATH` to it (unless the variable is set already), so a server that `dev` serves at `/mcp` is served at `/mcp` when built too. With no `transport.http.path`, the runner leaves it unset and the server answers at `/`. An `http.entryPath` in `@FrontMcp` wins over the variable.

Since 1.9 the bundle does the same by itself when it's the program Node runs: `PORT=8080 node dist/node/help-desk.bundle.js` answers at `/mcp`, and `node dist/node/help-desk.bundle.js --stdio` serves over stdin and stdout. Its first lines also carry the deployment's `server` block as defaults, for variables the environment doesn't set: `server.csp` and `server.headers` as the `FRONTMCP_*` header variables, `server.http.port` as `PORT`, and `server.http.entryPath` as the path, which wins over `transport.http.path`.

#### The `cli` target

```bash
frontmcp build --target cli
./dist/cli/help-desk --help
```

Without `--js` the build also writes a single executable, `dist/cli/<name>-cli-bin`, which carries Node and the whole bundle, so it runs without `node_modules`. The runner `dist/cli/<name>` starts it. With `--js` you get the JavaScript bundle instead, which needs the project's packages at run time. Before 1.8.7, the build without `--js` failed with `Could not resolve "@frontmcp/observability"`.

```text
Usage: help-desk [options] [command]

help-desk CLI

Options:
  -V, --version        output the version number
  --output <mode>      Output format: text or json (default: "text")
  --verbose            Enable verbose console logging (logs always go to ~/.frontmcp/logs/)
  --log-dir <path>     Directory for log files (default: ~/.frontmcp/logs/)
  -h, --help           display help for command

Tools:
  add [options]        Add two numbers

Resources & Prompts:
  resource             Resource operations
  prompt               Prompt operations

Subscriptions:
  subscribe            Subscribe to updates

System:
  serve [options]      Start the MCP server
  doctor [options]     Check system dependencies and configuration
  install [options]    Install to ~/.frontmcp/ and set up dependencies, OR emit an IDE plugin (use -p)
  uninstall [options]  Remove from ~/.frontmcp/, OR remove an IDE plugin (use -p)
  daemon               Daemon management
```

Each tool is a subcommand, named in kebab-case (`get_ticket` is `get-ticket`), with a flag per input field. A tool whose name is one of the program's own commands gets `-tool` added: see [the `cli` options](#the-cli-options-and-signing-in). The binary runs the server in its own process for each command:

```bash
./dist/cli/help-desk add --a 2 --b 3                 # {"result":5}
./dist/cli/help-desk add --a 2 --b 3 --output json   # the whole CallToolResult
./dist/cli/help-desk add --a x --b 3                 # Error: Invalid tool input, exit 2
```

It exits `0` on success, `2` for a usage error or invalid input (a tool error with code `INVALID_INPUT`), and `1` for any other tool error. It writes a log file to `~/.frontmcp/logs/` each time it runs. The build starts the server once to read its tools, and since 1.8.7 doesn't write there.

An object field takes JSON, `--customer '{"name":"Acme","plan":"pro"}'`, and text that isn't JSON stops with `Invalid JSON for --customer` and exit `1`. An array field takes a value per flag: `--tags login --tags sso` is `["login", "sso"]`. The other groups in `--help` reach the rest of the server:

| Command | Does |
| --- | --- |
| `resource list`, `resource read <uri>` | List the resources, or print one: `resource read skill://index.json`. |
| `template list`, `template <name>` | List the resource templates, or read one, with a flag per variable: `template sep2640-skill-md --skill-path triage-ticket` prints that skill's `SKILL.md`. |
| `prompt list`, `prompt get <name>` | List the prompts, or render one, with a flag per argument: `prompt get summarize_ticket --id T-1`. |
| `skills list`, `skills search [query]`, `skills read <name>`, `skills load <ids...>` | On a server with [skills](https://frontmcp.dev/reference/sdk/skill): the same answers as `skills/list`, `skills/search` and `skills/load`. |
| `subscribe resource <uri>`, `subscribe notification <name>` | Stream updates until Ctrl+C, as `--help` says; not run for this page. |
| `daemon start`, `stop`, `status`, `logs` | Run the server in the background, on a socket: see [the process manager](#process-manager). |

`serve` starts the server itself: `serve --port <n>` over HTTP, answering at `/` (the CLI doesn't read `transport.http.path`), or `serve --stdio` over stdin and stdout. Both were run against the binary. `--stdio` alone does the same as `serve --stdio`, and so does `node dist/cli/<name>-cli.bundle.js --stdio` with `--js`; that bundle is what a server [published to npm](https://frontmcp.dev/reference/deployment/mcp-clients#publishing-a-server-to-npm) runs.

##### The `cli` options and signing in

A `cli` deployment in `frontmcp.config` takes a `cli` block for the program:

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

export default defineConfig({
  name: "help-desk",
  entry: "src/main.ts",
  deployments: [
    {
      target: "cli",
      cli: { description: "Help desk tickets from the terminal", outputDefault: "json", authRequired: true },
    },
  ],
});
```

| Field | What `frontmcp build` does with it in 1.9.4 |
| --- | --- |
| `cli.description` | The line under `Usage:` in `--help`. Default: `<name> CLI`. |
| `cli.outputDefault` | `"text"` or `"json"`: the default of `--output`. |
| `cli.authRequired` | `true` adds the **Auth** commands below. |
| `cli.excludeTools`, `cli.oauth` | Accepted, and not read: every tool still gets a command, and `login` gets no default server or scope. Both work only in the older config shape, a `frontmcp.config.json` or `.js` with a `cli` block at the top and no `deployments`, which `build` reads and the other commands treat as no config at all. |
| `js`, `sea` | Accepted, and not read: `js: true` still built the single executable, and `sea: { enabled: true }` with `--js` didn't. Use `--js`. |

With `authRequired: true`, `--help` has an **Auth** group:

| Command | Does |
| --- | --- |
| `connect --token <token>` | Stores the token for a session, `default` unless `--session <name>`. Without `--token` it prints how to use it and exits `1`. |
| `login` | Signs in with OAuth in the browser (`--no-browser` prints the URL instead), and stores the token. Options: `--server <url>` (or `FRONTMCP_SERVER_URL`), `--scope <scopes>`, `--session <name>`. Without a server it stops with `Server URL required. Use --server <url> or set FRONTMCP_SERVER_URL.` and exit `1`. The sign-in itself wasn't run for this page. |
| `logout` | Deletes the active session's token, or `--session <name>`'s, or every token with `--all`. The sessions stay in `sessions list`. |
| `sessions list`, `sessions switch <name>`, `sessions delete <name>` | List the sessions, marking the active one, choose which one the tool commands use, or delete one with its token. |

Each tool command sends the active session's token to the server as the caller's token, which a tool reads as `this.context.authInfo.token`, to call your help desk's API on the user's behalf. Nothing checks it, and nothing requires one: the program runs the server in its own process, like [`connect()`](https://frontmcp.dev/reference/sdk/connect), so without a token the tools still run, with `authInfo.token` unset, even when the server's `auth` requires a key over HTTP. On macOS the token goes in the Keychain, through the `security` command, under the service `frontmcp.<name>` and the session's name; on other systems, encrypted, in `~/.frontmcp/apps/<name>/credentials/<session>.enc`. The sessions are files in `~/.frontmcp/apps/<name>/sessions/`.

A tool named like one of the program's commands gets `-tool` added, and the build says so: `Tool "login" conflicts with built-in command, mapped to "login-tool"`. The names are `resource`, `template`, `prompt`, `subscribe`, `login`, `logout`, `connect`, `serve`, `daemon`, `doctor`, `install`, `uninstall`, `sessions`, `help`, `version`, `skills`, `job`, `workflow`, `get`, `list` and `read`, whether or not `authRequired` is set.

##### Installing into Claude Code and Codex

The program has an `install` command for the people who run it: `install -p claude` writes a Claude Code plugin for the server, and `install -p codex` adds the server to Codex's `config.toml`. It reads `bin-meta.json` and `_skills/`, which the build writes next to the bundle, and doesn't start the server.

```bash
help-desk install -p claude --dry-run       # print the files it would write
help-desk install -p claude                 # ./.claude/plugins/help-desk/
help-desk install -p claude --scope user    # the same under your home folder
help-desk install --status
help-desk uninstall -p claude
```

```text
help-desk install --status
  claude:    installed v0.1.1 at /Users/you/projects/support/.claude/plugins/help-desk
  codex:     not installed in /Users/you/.codex/config.toml
```

| Option | Description |
| --- | --- |
| `-p`, `--provider <claude\|codex>` | Which host. Repeatable: `-p claude -p codex`. Without it, `install` copies the program into `~/.frontmcp/` instead, which this page doesn't cover. |
| `--scope <project\|user>` | `project` (the default) writes `.claude/plugins/<name>/` in the current folder; `user`, under your home folder. |
| `--dir <dir>` | Another folder to write the plugin in. |
| `--no-skills`, `--no-commands` | Leave out the plugin's `skills/` or `commands/` folder. |
| `--only-mcp` | Only the server: for `claude`, a plugin folder with its `plugin.json` and no `skills/`. |
| `--command <cmd>` | Another program for the server entry, instead of `<name>`. It's taken as one program: `--command "npx -y help-desk-mcp"` writes that whole string as the `command`, which a client can't start. |
| `--env <name>` | Adds `"<name>": "${<name>}"` to the server entry's `env`. Repeatable. |
| `--dry-run` | Print the plan and write nothing. |
| `--status` | Print, for each host, whether the server is installed, and exit `0`. |

`uninstall` takes `-p`, `--scope` and `--dir`, and removes what `install` wrote: the files listed in the plugin's `_meta.frontmcp.managedFiles`, and the Codex block. For the help desk, which has one skill, `-p claude` writes two files:

- `.claude-plugin/plugin.json`: the server's name, version and description, `skills: ["triage-ticket"]`, `_meta.frontmcp`, and `mcpServers: { "help-desk": { "command": "help-desk", "args": ["serve", "--stdio"], "transport": "stdio" } }`.
- `skills/triage-ticket/SKILL.md`: the skill's `name` and `description` as frontmatter, then its instructions.

The server entry runs `<name> serve --stdio`, where `<name>` is `frontmcp.config`'s `name`, so it works where a program by that name is on the `PATH`: a package [published to npm](https://frontmcp.dev/reference/deployment/mcp-clients#publishing-a-server-to-npm) whose `bin` has that name, installed with `npm install -g`. That command was run, and answered over stdio; whether Claude Code loads the plugin wasn't checked for this page.

> **Pitfall: `install -p codex` writes a block Codex refuses**
In 1.9.4 the block is an array entry, `[[mcp_servers]]` with a `name`, and Codex (0.152.1) then refuses its whole `config.toml`: `invalid type: sequence, expected a map` in `mcp_servers`. `frontmcp plugin install --codex` writes the same block. Remove it with `uninstall -p codex`, which takes out only that block, and add the server to `~/.codex/config.toml` as a table instead, which Codex lists:

```text
[mcp_servers.help-desk]
command = "help-desk"
args = ["serve", "--stdio"]
```

#### The `mcpb` target

```bash
frontmcp build --target mcpb
frontmcp mcpb validate dist/mcpb/help-desk-0.1.0.mcpb
```

```text
[build:mcpb] Building MCP Bundle...
[build:mcpb] name: help-desk
[build:mcpb] version: 0.1.0
[build:mcpb] entry: src/main.ts
[build:mcpb] TypeScript compiled
[build:mcpb] bundle: dist/mcpb/help-desk.bundle.js (4.0 KB)
[build:mcpb] extracted: 1 tools, 0 resources, 0 prompts
[build:mcpb] server bundle (runtime inlined): 10.4 MB
[build:mcpb] wrote manifest.json
[build:mcpb] dist/mcpb/help-desk-0.1.0.mcpb (2.0 MB)
[build:mcpb] sha256: 709f82ea…

MCPB build completed.
```

The archive holds `manifest.json`, `server/index.js`, `server/package.json` and `README.md`, with no `node_modules`. Since 1.8.7 `server/index.js` carries `@frontmcp/sdk` and the other runtime packages inside it, so it starts anywhere with Node: unzipped into an empty folder, `FRONTMCP_STDIO=1 node server/index.js` answers `initialize` and `tools/call` over stdin and stdout. The manifest runs it the same way, with `"env": { "FRONTMCP_STDIO": "1" }`. Before 1.8.7 it failed with `Cannot find module 'reflect-metadata'`.

With `--sea`, the archive also holds a single-executable binary for this platform (`bin/darwin-arm64/help-desk`, 126 MB, which makes the archive 43 MB). The binary carries Node and the runtime: unzipped into an empty folder, `FRONTMCP_STDIO=1 bin/darwin-arm64/help-desk` answers `initialize`, `tools/list` and `tools/call`. In 1.8.7 it died with `No such built-in module: reflect-metadata`, and `mcpb validate` now checks the binaries in `bin/` for that too.

Whether a host runs the binary is up to the manifest's `platform_overrides`, which MCPB hosts match by operating system only: `darwin`, `linux` or `win32`. So the build adds an override for an OS only when the archive has a binary for every architecture of it. A lone `--sea` build on a Mac says so, and the manifest keeps `node server/index.js` for every host:

```text
[build:mcpb] darwin gets no platform override: MCPB hosts match platform_overrides by OS only, so every darwin architecture needs a binary (missing darwin-x64; add them with --merge-from). darwin hosts run the Node bundle.
```

Built with `--merge-from` a folder holding `darwin-x64/help-desk`, the archive has both Mac binaries and `bin/darwin/launch`, a shell script that runs `bin/darwin-arm64/help-desk` or `bin/darwin-x64/help-desk` by `uname -m`, and the manifest gets `"platform_overrides": { "darwin": { "command": "${__dirname}/bin/darwin/launch", "args": [], "env": { "FRONTMCP_STDIO": "1" } } }`. The launcher answered over stdio like the binary. Before 1.9.2 the override was keyed `darwin-arm64`, which no host matches, so hosts ran the Node bundle anyway.

##### Bundle metadata and user settings

What a host shows before it installs the archive comes from `manifest.json`. The build fills it from the `mcpb` deployment in `frontmcp.config`, and from `package.json` for what the deployment leaves out:

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

export default defineConfig({
  name: "help-desk",
  entry: "src/main.ts",
  deployments: [
    {
      target: "mcpb",
      displayName: "Help Desk",
      longDescription: "Search and read your team's support tickets.\n\nNeeds an API key from the help desk's settings page.",
      author: { name: "Help Desk Team", email: "desk@example.com" },
      documentation: "https://desk.example.com/docs/mcp",
      support: "https://desk.example.com/support",
      icon: "assets/desk-icon.png",
      keywords: ["tickets", "support"],
      privacyPolicies: ["https://desk.example.com/privacy"],
      compatibility: { claude_desktop: ">=0.10.0", platforms: ["darwin", "win32"] },
    },
  ],
});
```

| Deployment field | In `manifest.json` | Without it |
| --- | --- | --- |
| `displayName` | `display_name` | None. |
| `longDescription` | `long_description`, and its first line is `description` | `description` is `package.json`'s `description`. |
| `author` | `author`, `{ name, email?, url? }` | `package.json`'s `author`; a string like `"Help Desk Team <desk@example.com> (https://desk.example.com)"` becomes the three fields. |
| `license`, `homepage`, `keywords` | The same names | `package.json`'s. |
| `repository` | `repository`, as `{ type, url }`; a string gets `type: "git"` | `package.json`'s. |
| `documentation`, `support` | The same names | None. |
| `privacyPolicies` | `privacy_policies` | None. |
| `icon` | The file is copied into the archive as `icon.png`, and `icon` is `"icon.png"` | `--icon` wins over it. Then `package.json`'s `icon`, `icon.png`, and `assets/icon.png`. A path that doesn't exist is skipped without a warning, and the archive has no icon. |
| `compatibility` | `compatibility`: `claude_desktop`, `platforms`, `runtimes` | `platforms` is all three; `runtimes.node` is `nodeVersion`, else `>=22.0.0`. |
| `userConfig` | `user_config` | See below. |
| `sea` | | `{ enabled, mergeFrom }`: the same as `--sea` and `--merge-from`. |
| `deterministic` | | `false` keeps each file's real date, so every build has another `sha256`, as with `--no-deterministic`. |
| `env` | | Set when `server/index.js` starts, for variables the host doesn't set, like the other targets' `env`. |
| `includeNodeModules` | | Accepted, and not read: the archive has no `node_modules` either way. |

The rest comes from the server, which the build starts once to read it: `tools`, each tool's name and description, with `tools_generated: false`; `resources`, with each one's name, URI, description and MIME type, the skills' `skill://` resources included, with `resources_generated: true`; `prompts`, names and descriptions only, with `prompts_generated: true`, since a prompt's text is made when it's called. Each skill's files go in `server/_skills/`, and the project's `README.md` goes in the archive. `_meta` records the CLI's version (`"dev.frontmcp.generator": "frontmcp@1.9.4"`) and whether the server has skills, jobs and workflows.

`userConfig` declares settings the host asks the user for when they install the archive, each `{ type, title, description?, required?, default?, multiple?, sensitive?, min?, max? }`, with `type` one of `string`, `number`, `boolean`, `directory` or `file`. In 1.9.4 they reach the manifest's `user_config`, but not the server: `mcp_config.env` gets only `FRONTMCP_STDIO`, so the user's answers go nowhere. `mcpb validate` passes such an archive.

The settings that do reach the server come from a `setup.steps` list, the questions [`frontmcp install`](#package-manager) asks. Each step becomes a `user_config` entry and a variable in `mcp_config.env`. `setup` is accepted only in the older config shape, a `frontmcp.config.json` or `.js` without `deployments` and with a `cli`, `sea` or `esbuild` key at the top; the current shape refuses it (`Unrecognized key: "setup"`). That shape has no `mcpb` deployment, so such a bundle takes its metadata from `package.json` only:

```json frontmcp.config.json
{
  "name": "help-desk",
  "entry": "./src/main.ts",
  "esbuild": {},
  "setup": {
    "steps": [
      { "id": "desk-api-url", "prompt": "Help desk API URL", "jsonSchema": { "type": "string", "default": "https://api.desk.example.com" } },
      { "id": "desk-api-key", "prompt": "Help desk API key", "sensitive": true, "jsonSchema": { "type": "string" } },
      { "id": "page-size", "prompt": "Tickets per page", "jsonSchema": { "type": "number", "minimum": 1, "maximum": 100, "default": 20 } }
    ]
  }
}
```

```json manifest.json
{
  "server": {
    "mcp_config": {
      "command": "node",
      "args": ["${__dirname}/server/index.js"],
      "env": {
        "FRONTMCP_STDIO": "1",
        "DESK_API_URL": "${user_config.deskApiUrl}",
        "DESK_API_KEY": "${user_config.deskApiKey}",
        "PAGE_SIZE": "${user_config.pageSize}"
      }
    }
  },
  "user_config": {
    "deskApiUrl": { "type": "string", "title": "Help desk API URL", "default": "https://api.desk.example.com" },
    "deskApiKey": { "type": "string", "title": "Help desk API key", "sensitive": true, "required": true },
    "pageSize": { "type": "number", "title": "Tickets per page", "default": 20, "min": 1, "max": 100 }
  }
}
```

A step's `id` becomes the key in camelCase, and its variable is the step's `env`, else the `id` in capitals with `_` for `-`. `prompt` is the `title`. The type comes from `jsonSchema.type` (`integer` is a `number`, and an `array` sets `multiple`), `minimum` and `maximum` become `min` and `max`, and a step without a `default` is `required`. A `sensitive` step has no `default` in the manifest. A step with `showWhen` or `next` is asked every time, since MCPB has no conditions, and the build warns: `Step "sso" uses showWhen/next — MCPB has no equivalent; rendered unconditionally`. The server reads the answers as ordinary variables, like `process.env.DESK_API_KEY`. No MCPB host was used for this page; the manifests were read from the archives, which `mcpb validate` passed.

#### Caveats

- **`--entry` wins over the config's `entry`** for every target. Before 1.8.7 the config won for `node`, `cli` and `mcpb`.
- **`node` and `cli` delete the intermediate files they wrote** in their folder, after building, with or without `--no-clean`: the compiled `.js` and `.map` files at the top level. The rule is "files written by this build, or changed in the 2 seconds before it started". Older files, `.md` files and subfolders stay. So with `deployments: [{ target: "node", outDir: "." }]` the build skips its cleaning ("it is the project root") and leaves `package.json`, `tsconfig.json` and `frontmcp.config.*` alone, unless you saved one of them a moment before building. Before 1.8.7 it deleted them.
- The compiled sources of `node` and `cli` stay next to the bundle, in subfolders like `dist/node/tools/`.

### `frontmcp test`

```bash
frontmcp test [patterns...] [options]
```

| Option | Description |
| --- | --- |
| `patterns` | Only files whose path matches, as Jest's own patterns: `frontmcp test tickets`. |
| `-i`, `--runInBand` | One file at a time. Default: `test.runInBand` in `frontmcp.config`. |
| `-w`, `--watch` | Run again when files change. |
| `-v`, `--verbose` | Passed to Jest. The generated configuration is verbose anyway. |
| `-t`, `--timeout <ms>` | Each test's timeout. Default `test.timeoutMs`, then `60000`. Only with the generated configuration. |
| `-c`, `--coverage` | Collect coverage into `coverage/`, from `src/**/*.ts(x)`. |
| `--no-env` | Don't load `.env` and `.env.local`. |

It loads `.env` and `.env.local` (unless `--no-env`), adds `env.shared` and `env.test`, and runs the project's Jest with Node (`npx jest` when it isn't installed; before 1.9.4, always `npx jest`) with a configuration it writes to a temporary file: `src/**/*.spec.ts(x)`, `**/__tests__/**/*.spec.ts(x)` and `e2e/**/*.e2e.spec.ts(x)`, compiled with `@swc/jest`, with `@frontmcp/testing`'s matchers set up. It transpiles `jose`, `@noble/hashes` and `@noble/ciphers` (ESM-only packages) by default, and `test.esmPackages` adds more. `@swc/jest` comes from the project, or else from `@frontmcp/testing`, which depends on it since 1.9. A `jest.config.{ts,js,mjs,cjs,json}` in the current folder replaces the generated configuration (`[test] using user Jest config: jest.config.cjs`); one that says `preset: "@frontmcp/testing"` gets the same settings. [`@frontmcp/testing`](https://frontmcp.dev/reference/testing#running-tests-with-frontmcp-test) covers the tests themselves.

```text
[test] running tests in .
[test] config: /Users/you/help-desk
[test] using auto-injected Jest configuration
hint: press Ctrl+C to stop

Test Suites: 1 passed, 1 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        2.269 s
Ran all test suites.
```

With the Jest 30 that `frontmcp create` installs, a passing run prints only that summary, with no line per test. A failing run prints the server's log for the failed test (`[TestFixture] === Server Logs (test failed) ===`), then the failure, and exits `1`, whatever code Jest exited with, after `Error: Jest exited with code 1` and a stack trace.

### `frontmcp inspector`

Runs `npx -y @modelcontextprotocol/inspector`, the latest published version, not a pinned one. It takes no options. With `transport.default: "http"` and a `transport.http.port` in `frontmcp.config`, it passes `--transport http --server-url http://<host>:<port><path>` (host `127.0.0.1` and path `/mcp` by default); with `"stdio"` and `transport.stdio.command`, it passes `--transport stdio -- <command>`. Otherwise you enter the server's address in the Inspector yourself. The Inspector's environment gets `env.shared` and `env.dev`, which win over your shell's. Start `frontmcp dev` first, in another terminal.

```text
[inspector] launching MCP Inspector...
[inspector] config: /Users/you/help-desk
Starting MCP inspector...

MCP Inspector Web is up and running at:
   http://127.0.0.1:6274?MCP_INSPECTOR_API_TOKEN=…
```

The Inspector's page is on port `6274` unless `CLIENT_PORT` says otherwise: `CLIENT_PORT=4400 frontmcp inspector`. The [`inspector` Nx executor](https://frontmcp.dev/reference/nx/executors#inspector) sets it from its `port` option. The Inspector prints more lines than the three above, whatever its latest version adds.

### Process manager

`start` runs a server under a supervisor that restarts it when it exits, after 1 second, then 2, 4 and so on, up to `--max-restarts` times, then gives up (`max restarts (5) reached, giving up`) and exits `0`. It writes the server's PID file to `~/.frontmcp/pids/<name>.pid` and its output to `~/.frontmcp/logs/<name>.log` and `<name>.error.log`. The other commands read those files, so they work from any folder and any terminal.

| Command | Options | Description |
| --- | --- | --- |
| `start <name>` | `-e`, `--entry <path>`, `-p`, `--port <N>`, `-s`, `--socket <path>`, `--db <path>`, `--max-restarts <N>` (default `5`) | Start the server and stay in the foreground: the supervisor lives in this process, and Ctrl+C stops the server. The entry is found in the current folder, as for `dev`, unless `<name>` was added by [`install`](#package-manager) and you gave no `--entry`: then it runs the installed bundle from the install folder, with the `.env` there, even from inside a project of the same name. Loads `.env` and `.env.local`, and, since 1.9, `env.shared` and `env.ship` from the `frontmcp.config` above the entry, for variables not set already. A project's entry answers at `/`: `start` doesn't read `transport.http.path`. An installed bundle answers at the path it was built with. |
| `stop <name>` | `-f`, `--force` | Stop it with `SIGTERM`, or `SIGKILL` with `--force`, from another terminal. The `start` process then exits too. |
| `restart <name>` | | Stop it and start it again. The server comes back, but the new supervisor lives in the `restart` process, which prints `Restarted successfully:` and the server's details and then stays in the foreground the way `start` does. Give it a terminal of its own, or run `stop` and `start`. |
| `status [name]` | | One server's details, or the table `list` prints. |
| `list` | | Every server with a PID file: name, PID, status (`running` or `dead`), port (or `socket`), uptime, restarts. With none: `No managed processes found.` |
| `logs <name>` | `-F`, `--follow`, `-n`, `--lines <N>` (default `50`) | The end of its log. |
| `socket <entry>` | `-s`, `--socket <path>`, `--db <path>`, `-b`, `--background` | Serve on a Unix socket (default `~/.frontmcp/sockets/<folder of the entry>.sock`), not on a TCP port. The server answers at `/`. `--background` starts it detached, prints its PID and returns; the PID is also in `<socket>.pid`, and `SIGTERM` stops it and removes the socket. A socket path is limited to about 100 characters on macOS: a longer one fails with `listen EINVAL`. `--db` sets the SQLite file ([below](#sqlite-for-socket-and-start)). |
| `service <install\|uninstall> <name>` | | Write, or remove, a launchd agent (macOS, `~/Library/LaunchAgents/dev.agentfront.frontmcp.<name>.plist`) or a systemd user unit (Linux) that runs `frontmcp start <name> --entry <entry>` and `--port` when the server has one. The name is required, and so is a PID file: start the server once first. It doesn't load the service; it prints the `launchctl` or `systemctl --user` commands that do. For an app added by `install`, the entry is the installed bundle. |

```text
$ frontmcp start help-desk --port 4103
[pm] starting "help-desk"...
[pm] entry: src/main.ts

Started successfully:

Name:        help-desk
PID:         96400
Supervisor:  96392
Status:      running
Entry:       /Users/you/help-desk/src/main.ts
Port:        4103
Started:     2026-10-05T19:21:54.566Z
Uptime:      0s
Restarts:    0
CLI Version: 1.9.4

hint: test with: curl http://localhost:4103/health
```

`stop` and `restart` exit `1` for a name with no PID file, and `service install` too; `stop` also exits `1` for a server that's `dead` (`Process "help-desk" is not running (stale PID file removed)`). `status` and `logs` print `No process found with name "…"` and exit `0`. While a server crashes and is restarted, `list` shows it as `dead` with its restart count.

A [`cli` build](#the-cli-target)'s `daemon start` writes its own PID file to the same `~/.frontmcp/pids/` folder, with its name, binary and socket, so `list` shows a running daemon with `socket` in the Port column, and `status <name>` its details, with the version the daemon was built with, the project's and not the CLI's, as `CLI Version:`; in 1.9.2 that line was empty. Before 1.9.2 its PID file had no name, and `list` and `status` stopped on it with `TypeError: Cannot read properties of undefined (reading 'replace')`; they now read such a file without crashing.

#### SQLite for `socket` and `start`

`--db <path>` sets `FRONTMCP_SQLITE_PATH` for the server, and `@frontmcp/sdk` reads it: it overrides `sqlite.path` from `@FrontMcp`, and turns on the SQLite session and task stores. That needs `better-sqlite3` and `@frontmcp/storage-sqlite` installed in the project. Without them the server stops at start with `Cannot find module 'better-sqlite3'`, in `~/.frontmcp/logs/<name>.error.log`. `socket` passes the socket as `FRONTMCP_DAEMON_SOCKET`, which the SDK reads too.

```text
$ frontmcp socket src/main.ts --socket /tmp/help-desk.sock
[socket] entry: src/main.ts
[socket] socket: /tmp/help-desk.sock
[socket] starting in foreground mode...
hint: press Ctrl+C to stop
hint: test with: curl --unix-socket /tmp/help-desk.sock http://localhost/health
…
MCP HTTP (Express) on unix:///tmp/help-desk.sock
```

### Package manager

| Command | Options | Description |
| --- | --- | --- |
| `install <source>` | `--registry <url>`, `-y`, `--yes`, `-p`, `--port <N>` | Copy a built server into `~/.frontmcp/apps/<name>/` and record it in `~/.frontmcp/registry.json`. `<source>` is an npm package, a local path (`.`, `./…`), or `github:user/repo`. It looks for a `*.manifest.json` in the source, then in its `dist/`, then in the folders of `dist/`, `dist/node` first, since `start` runs the installed bundle as a server. Without one, and with a `frontmcp.config.*`, it builds the `node` target there first (`no manifest found, building from config...`). It copies the bundle, runner and manifest, then runs `npm install` in the app's folder for `@frontmcp/sdk` and `reflect-metadata` (at the versions the project declares, else the CLI's own; `vectoriadb` and `tslib` come with the SDK), the optional SDK peers the project declares (like `@frontmcp/storage-sqlite` or `@frontmcp/observability`), and any native addons the manifest lists. Runs the manifest's setup questions, if any, unless `--yes`. |
| `configure <name>` | `-y`, `--yes` | Ask the setup questions again and rewrite the app's `.env`. An app with none prints `App "help-desk" has no setup questionnaire.` |
| `uninstall <name>` | | Delete the app's folder and its registry entry. |

`frontmcp install ./help-desk --yes` from the folder above a built project, then `frontmcp start help-desk` from any folder, runs the installed server, which answers at the `transport.http.path` it was built with. `frontmcp install . --yes` from the project's own folder does the same. Before 1.9.2 a bare `.` was read as an npm package name, so `install` packed the project and built it again in a temporary folder. Before 1.9, the installed server died at start-up for want of `vectoriadb`, then `tslib`, and `install` took `dist/cli` over `dist/node` when both were built.

### Skills

The skills catalog is a set of instruction folders (`SKILL.md` and references) for coding agents, the same ones `create --skills` copies into a project. In 1.9.4 it has 13.

| Command | Options | Description |
| --- | --- | --- |
| `skills list` | `-c`, `--category`, `-t`, `--tag`, `-b`, `--bundle <recommended\|minimal\|full>` | The catalog, by category. |
| `skills search <query>` | `-n`, `--limit <count>` (default `10`), `-t`, `--tag`, `-c`, `--category` | Rank skills by how well their name, description and tags match. |
| `skills read <name>[:<file>] [reference]` | `--refs`, `--examples [reference]` | Print a skill, one of its references (`frontmcp skills read create-tool quick-start`), or any file in it. |
| `skills install [name]` | `-p`, `--provider <claude\|codex>` (default: `skills.provider` in `frontmcp.config`, else `claude`), `-d`, `--dir <directory>`, `-a`, `--all`, `-t`, `--tag`, `-c`, `--category`, `--from-entry <path>`, `--from-package <pkg>` | Copy skills into `.<provider>/skills/<name>/`, or `--dir`, and, for the default provider, create or update an instructions file in the current folder that lists them. With no name and no selector, it installs the config's `skills.install` list, else its `skills.bundle` (`"none"` installs nothing); without either it stops with `Please specify a skill name, or use --all, --tag, or --category (or set skills.install / skills.bundle in frontmcp.config).` `--from-entry` and `--from-package` install the `@Skill` entries of a server instead of the catalog's. |
| `skills export` | `-t`, `--target <cursor\|windsurf\|copilot>` (default: `skills.exportTarget` in `frontmcp.config`, else `cursor`), `-n`, `--name <name>`, `-a`, `--all`, `-d`, `--out <directory>` | Turn a skill into an editor rule file, like `.cursor/rules/create-tool.mdc`. |
| `skills publish <name>` | `-t`, `--target <smithery\|glama>`, `--token <token>` (default `SMITHERY_TOKEN` or `GLAMA_TOKEN`), `--repository <url>`, `--dry-run` | Submit a skill to a public marketplace. `--dry-run` prints the request instead. |

### `frontmcp mcpb validate`

```bash
frontmcp mcpb validate dist/mcpb/help-desk-0.1.0.mcpb
```

```text
[mcpb:validate] dist/mcpb/help-desk-0.1.0.mcpb
[mcpb:validate] name=help-desk version=0.1.0
[mcpb:validate] tools=1 prompts=0 user_config=0
[mcpb:validate] archive is valid
```

Checks an archive against the MCPB 0.3 manifest format, that the manifest's entry point is in the archive, and that `${…}` substitutions are declared. Since 1.8.7 it also reads `server/*.js` and flags an archive with no `server/node_modules/` whose server still calls `require()` on `@frontmcp/sdk`, `@frontmcp/di`, `@frontmcp/utils`, `@frontmcp/auth` or `reflect-metadata`, since that server can't start:

```text
[mcpb:validate] server/index.js requires "reflect-metadata" but the archive has no node_modules — the server cannot start. Rebuild with `frontmcp build --target mcpb` so runtime packages are bundled
```

Since 1.9 it reads the single-executable binaries under `bin/` the same way, and flags one that would `require()` those packages, since such a binary dies at start-up. An archive built by 1.9.4, with or without `--sea`, passes.

It exits `1` with `MCPB validation failed (<n> error)` for a file that isn't a valid bundle, and `Cannot open archive: …` for one that isn't a zip.

### `frontmcp eject-mcp-config`

```bash
frontmcp eject-mcp-config <client> [-o <file>] [--dry-run]
```

Prints the entry for your server in an MCP client's configuration, built from `frontmcp.config`'s `clients.<client>`. `<client>` is one of `claude-code`, `claude-desktop`, `cursor`, `windsurf`, `vscode`; any other exits `2`. A client with no entry in `clients` exits `1` (``frontmcp.config has no `clients.cursor` entry``).

| `clients.<client>` field | Used for |
| --- | --- |
| `name` | The server's key. Default: the config's `name`. |
| `transport` | `http` or `sse`: an entry with `url` and `transport`. `stdio`: an entry with `command` and `args`. |
| `url` | The URL. Default: `http://<transport.http.host or 127.0.0.1>:<transport.http.port><transport.http.path or /mcp>`. |
| `command`, `args` | For `stdio`. Default `npx` and `["-y", <name>]`. |
| `env` | Copied into a `stdio` entry, after the config's `env.shared` and `env.ship`, which go there too since 1.9. An `http` or `sse` entry gets no `env`. |

Every client gets the same shape, `{ "mcpServers": { … } }`. With `-o`, the snippet goes into a file:

- Missing folders in the path are created.
- A file that's there is merged, not replaced: its other keys and other servers stay, and the entry with the same name is replaced (`merged into existing config`).
- A file that isn't valid JSON is left untouched, and the command exits `1` with `Existing client config is not valid JSON, refusing to overwrite it`.
- `--dry-run` with `-o` prints what would be written, including the merge, and writes nothing.

Before 1.8.7, `-o` needed the folder to exist and overwrote the whole file.

### `frontmcp plugin`

```bash
frontmcp plugin <install|uninstall|status> (--claude | --codex) [options]
```

Writes the project out for a coding-agent host: `--claude` writes a plugin folder, `<project>/.claude/plugins/<name>/` (or under your home folder with `--scope user`), and `--codex` adds an `mcp_servers` entry to `~/.codex/config.toml`, in a form Codex refuses ([the pitfall](#installing-into-claude-code-and-codex) has the fix). The server entry runs `<name> serve --stdio`, so it expects a [`cli` build](#the-cli-target) installed on the `PATH` under the project's name. A `cli` build's own [`install -p`](#installing-into-claude-code-and-codex) does the same from the installed program, for the people who use it. Without `--claude` or `--codex`, it prints `Specify --claude and/or --codex.` and exits `1` (it was `0` before 1.8.7).

| Option | Description |
| --- | --- |
| `--scope <project\|user>` | Default `project`. |
| `--dir <dir>` | Another destination root. |
| `--no-skills`, `--no-commands` | Leave out the `skills/` or `commands/` folder. |
| `--only-mcp` | Only the server entry, no plugin folder. |
| `--command <cmd>` | Another command for the server entry. |
| `--env <name>` | A variable placeholder to add. Repeatable. |
| `--dry-run` | Print the plan and write nothing. |

`uninstall` removes what `install` wrote; `status` reports each host as installed, outdated or not installed.

### Project commands

Commands listed in `frontmcp.config`'s `cli.commands` become `frontmcp <name>`, listed under **Project Commands** in the help (unless `hidden: true`) and as `[project]` in `--list-commands`. They're read from the current folder only: from a subfolder, `frontmcp seed` is `unknown command 'seed'`. [Configuration files](https://frontmcp.dev/reference/server/config-files#fields) lists the fields; [Usage](#adding-your-own-commands) shows one.

The CLI runs the command's `entry` with Node, through `tsx` for a `.ts` file, from the project's folder. The script gets the positional arguments, then every option as `--flag value` (booleans as `--flag` when set), and the whole call as JSON in `FRONTMCP_PROJECT_COMMAND`: `{ verb, positionals, options, cwd }`. `frontmcp` exits with the script's exit code. An `entry` outside the project is skipped with a warning.

### Exit codes

| Code | When |
| --- | --- |
| `0` | Success, `--help` and `--version`. Also `status` and `logs` of an unknown name, a `start` that gave up after `--max-restarts`, `dev` stopped by Ctrl+C or `kill`, and `dev --stdio` when its stdin closes. |
| `1` | Any error: a failed build or test run, a failed `doctor` check, an `init` that can't parse `tsconfig.json`, a failed `mcpb validate`, a port in use for `dev`, a folder `create` won't use or a value it refuses, a missing argument, an unknown command or option, `plugin` without a host flag, `stop` or `restart` of a name that isn't running. Errors print as `Error: <message>`, usually followed by a stack trace. Bare `frontmcp` also exits `1`. |
| `2` | `eject-mcp-config` with an unknown client; the `node` runner with an unknown flag; a usage error or invalid input in a [`cli` build](#the-cli-target). |
| The script's | A [project command](#project-commands). |

### What it reads and writes

| Path or variable | Read or written by |
| --- | --- |
| `frontmcp.config.{ts,js,json,mjs,cjs}` | Found through `--config`, then `FRONTMCP_CONFIG`, then the nearest folder up, by `dev`, `build`, `test`, `inspector` and `eject-mcp-config`; in the current folder only by project commands. `dev` and `build` run from the folder of a config found above the current one, so its `entry` is relative to that folder; with `--config`, it's relative to the folder you run in. `start` and `socket` read `env.shared` and `env.ship` from the config above the entry. [Configuration files](https://frontmcp.dev/reference/server/config-files#frontmcpconfig) lists every field and who reads it. |
| `package.json` | `main` for the entry; `name` and `version` for `build` without a config file, and `version` for a config without one. |
| `tsconfig.json` | `build` compiles with it when it's there, and passes `--module commonjs` (or `esnext` for `cloudflare`) and `--outDir` on top. Without one, it compiles the entry alone. |
| `.env`, `.env.local` | `dev`, `test`, `start`, `socket`. Real variables win. |
| `~/.frontmcp/` | `pids/`, `logs/`, `sockets/`, `apps/`, `registry.json`: the process and package managers. `logs/` also gets a file for each run of a `cli` build's binary. |
| `.frontmcp/dev.log` | Written by `dev --stdio`, in the project's folder. |
| `FRONTMCP_SQLITE_PATH`, `FRONTMCP_DAEMON_SOCKET` | Set for the server by `--db` and by `socket` (and `start --socket`). The SDK reads both. |
| `FRONTMCP_HTTP_ENTRY_PATH`, `FRONTMCP_*` security-header variables | Set by `dev`, by the `serverless-setup.js` of the `vercel`, `lambda`, `cloudflare` and `distributed` builds, and by the first lines of a `node` bundle; the path also by the `node` runner. The headers come from a deployment's `server.csp` and `server.headers`. Before 1.9 the `node` build didn't carry the headers. |

#### Caveats

- `frontmcp create` writes a comment at the top of `frontmcp.config.ts` about the precedence: a CLI flag, then the config, then the built-in default. Only `FRONTMCP_CONFIG` and the variables in the table above are read from the environment; there's no `FRONTMCP_<NAME>` variable for each field.

---

## Usage

### Starting a project

```bash
npx frontmcp create help-desk --yes
cd help-desk
npm install
npm run dev
```

```text
[create] Creating project in ./help-desk
  Deployment: node
  Redis: docker
  Package manager: npm
  GitHub Actions: Yes

✓ created src/main.ts
✓ created src/calc.app.ts
✓ created src/tools/add.tool.ts
✓ created frontmcp.config.ts
✓ created e2e/server.e2e.spec.ts
  9 skills added (bundle: recommended)
✓ created ci/Dockerfile
✓ created ci/docker-compose.yml
…
✓ Initialized git repository
```

The server answers at `http://localhost:3000/mcp`, and `http://localhost:3000/health` answers `{"status":"ok", …}`. To choose the target and leave out Redis, CI and the skills:

```bash
npx frontmcp create help-desk --target cloudflare --no-cicd --skills none --pm pnpm
```

`frontmcp create help-desk --nx` makes an Nx workspace instead: [Nx plugin](https://frontmcp.dev/reference/nx#creating-a-workspace).

### Running the server while you work

`frontmcp dev` refuses a port that's taken, so two projects don't answer on the same one by accident:

```text
$ frontmcp dev
[dev] Port 3000 is already in use — refusing to start.
       Retry with one of:
         • frontmcp dev --port <other-port>
         • frontmcp dev --auto-port     (pick the next free port automatically)
         • PORT=<other-port> frontmcp dev
       (pass --show-conflict to print which process is holding the port)
```

Pick one, or let `dev` find the next free port:

```bash
frontmcp dev --port 4100
frontmcp dev --auto-port      # [dev] port 3000 is in use; auto-picked 3001
```

To keep a port for a project, set `transport.http.port` in `frontmcp.config.ts` rather than `http.port` in `@FrontMcp`: `dev` respects the config's port and `--port` overrides it, while a port in the code wins over both.

### Connecting a stdio client while you work

A client that starts its servers as commands can run `frontmcp dev --stdio` and get the reloading server. Start it in the project's folder, since that's where `dev` looks for its config and entry:

```json
{
  "mcpServers": {
    "help-desk": {
      "command": "sh",
      "args": ["-c", "cd /Users/you/help-desk && exec ./node_modules/.bin/frontmcp dev --stdio"]
    }
  }
}
```

The client sees the server's tools at once. Save a change to a tool and the client gets `notifications/tools/list_changed`, then the new description on its next `tools/list`, without reconnecting. Anything the server prints goes to `.frontmcp/dev.log`, the place to look when a call fails. Pass `--port` when the config's port is taken by another `frontmcp dev`.

### Building and running a production server

```bash
frontmcp build --target node
PORT=8080 ./dist/node/help-desk
```

Run it from the project, or from a copy that has its `node_modules`. With `transport.http.path` set to `/mcp` in `frontmcp.config.ts`, as `create` writes it, the runner serves the endpoint at `/mcp`, where `dev` served it; with no path in the config it answers at `/`. `node dist/node/help-desk.bundle.js` does the same without the runner. For a stdio client, the same runner serves over stdin and stdout:

```json
{ "mcpServers": { "help-desk": { "command": "/Users/you/help-desk/dist/node/help-desk", "args": ["--stdio"] } } }
```

To build every target in `deployments` at once, run `frontmcp build` without `--target`:

```text
[build] Building 2 target(s) from frontmcp.config: node, cloudflare

[build] ═══ node ═══
…
[build] ═══ cloudflare ═══
…
```

To use another config for one build, say a CI one, pass it with `--config`; every target reads it, and `--entry` still overrides its `entry`:

```bash
frontmcp build --target vercel --config ci/frontmcp.config.json
```

### Shipping the server as a command-line tool

The `cli` target turns every tool into a subcommand, in a single executable that carries Node:

```bash
frontmcp build --target cli
./dist/cli/help-desk add --a 2 --b 3
```

```text
{"result":5}
```

`./dist/cli/help-desk serve` starts the server itself, over HTTP, and `serve --stdio` over stdin and stdout. For a JavaScript bundle that runs where the project's packages are installed, add `--js`: that's the one to [publish to npm](https://frontmcp.dev/reference/deployment/mcp-clients#publishing-a-server-to-npm).

When the help desk's tools call its API as the user, give the program a token to send. Set `authRequired: true` in the deployment's [`cli` block](#the-cli-options-and-signing-in), build again, and store the user's key once:

```bash
./dist/cli/help-desk connect --token "$DESK_API_KEY"
./dist/cli/help-desk sessions list
./dist/cli/help-desk get-ticket --id T-1     # the tool reads the key as this.context.authInfo.token
```

```text
Credentials stored for session: default
  default (active) - last used: 2026-10-10T11:55:16.707Z
```

`connect --session staging --token …` keeps a second key, and `sessions switch staging` makes the tool commands use it. The program's users can also add it to Claude Code or Codex themselves, with [`install -p`](#installing-into-claude-code-and-codex).

### Packaging a `.mcpb` bundle

The `mcpb` target makes a `.mcpb` archive. Build it, then check it:

```bash
frontmcp build --target mcpb
frontmcp mcpb validate dist/mcpb/help-desk-0.1.0.mcpb
```

The archive carries everything the server needs, so the host only needs Node. To see what the host will run, unzip it anywhere and start the entry over stdio:

```bash
unzip -q dist/mcpb/help-desk-0.1.0.mcpb -d /tmp/help-desk-bundle
cd /tmp/help-desk-bundle
FRONTMCP_STDIO=1 node server/index.js
```

To ship a binary that carries Node too, add `--sea`: the archive then also holds one for the platform you build on. Hosts run it instead of `node server/index.js` only when the archive has every architecture of that operating system, so build the others on their machines and add them with `--merge-from`, a folder of `<platform>/<name>` binaries:

```bash
frontmcp build --target mcpb --sea --merge-from ci/binaries   # ci/binaries/darwin-x64/help-desk, built on an Intel Mac
unzip -q dist/mcpb/help-desk-0.1.0.mcpb -d /tmp/help-desk-sea
FRONTMCP_STDIO=1 /tmp/help-desk-sea/bin/darwin/launch
```

`bin/darwin/launch` is the command the manifest gives Mac hosts: it runs the binary for the Mac's architecture.

The name, description, icon and links a host shows come from the `mcpb` deployment in `frontmcp.config`, else from `package.json`: [Bundle metadata and user settings](#bundle-metadata-and-user-settings) lists them. To look at the manifest and the files before they're zipped, build with `--stage-only` and read `dist/mcpb/__stage/`, which then holds `manifest.json`, `server/`, `README.md` and the `icon.png`; no `.mcpb` is written.

### Running the tests

```bash
frontmcp test --runInBand
frontmcp test tickets --timeout 120000   # only files matching "tickets"
```

A project made by `frontmcp create` already lists everything the generated configuration needs in its `devDependencies`. In another project, install `@frontmcp/testing`, `jest` and `tsx`:

```bash
npm install -D @frontmcp/testing jest tsx
```

`@frontmcp/testing` brings `@swc/jest` and `@swc/core` since 1.9, so the generated configuration compiles without them in your `package.json`. A test that starts a `.ts` server runs it with `npx tsx`, which uses the project's `tsx` when it's installed and downloads one when it isn't. [`@frontmcp/testing`](https://frontmcp.dev/reference/testing#running-tests-with-frontmcp-test) has the rest.

### Connecting a client

Describe each client once in `frontmcp.config.ts`, then print or write its entry:

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

export default defineConfig({
  name: "help-desk",
  entry: "src/main.ts",
  deployments: [{ target: "node" }],
  transport: { default: "http", http: { port: 4100, path: "/mcp" } },
  clients: {
    cursor: { transport: "http" },
    windsurf: { transport: "stdio", command: "node", args: ["dist/node/help-desk.bundle.js"], env: { FRONTMCP_STDIO: "1" } },
  },
});
```

```bash
frontmcp eject-mcp-config cursor
frontmcp eject-mcp-config cursor -o .cursor/mcp.json
```

```json
{
  "mcpServers": {
    "help-desk": {
      "url": "http://127.0.0.1:4100/mcp",
      "transport": "http"
    }
  }
}
```

`-o` creates `.cursor/` if it's missing. If `.cursor/mcp.json` already lists other servers, they stay, and only the `help-desk` entry is added or replaced.

### Adding your own commands

A command for the project's own chores, like loading sample data, goes in `cli.commands`:

```json frontmcp.config.json
{
  "name": "help-desk",
  "deployments": [{ "target": "node" }],
  "cli": {
    "commands": {
      "seed": {
        "entry": "scripts/seed.mjs",
        "description": "Load sample tickets",
        "arguments": [{ "name": "count", "required": false }],
        "options": [
          { "flags": "--priority <level>", "description": "Ticket priority", "default": "low" },
          { "flags": "--fail", "description": "Exit with an error" }
        ]
      }
    }
  }
}
```

```js scripts/seed.mjs
const call = JSON.parse(process.env.FRONTMCP_PROJECT_COMMAND);
console.log("argv:", process.argv.slice(2).join(" "));
console.log("options:", JSON.stringify(call.options), call.positionals);
process.exit(call.options.fail ? 3 : 0);
```

```text
$ frontmcp seed 5 --priority high
argv: 5 --priority high
options: {"priority":"high"} [ '5' ]

$ frontmcp seed --fail; echo $?
argv: --priority low --fail
options: {"priority":"low","fail":true} [ null ]
3
```

### Keeping a server running

`frontmcp start` stays in the foreground, so give it a terminal of its own (or a service, below), and manage it from another:

```bash
frontmcp start help-desk --port 4103     # terminal 1
frontmcp list                            # terminal 2
frontmcp logs help-desk -n 20
frontmcp stop help-desk
```

```text
 Name      │ PID   │ Status  │ Port │ Uptime │ Restarts
───────────┼───────┼─────────┼──────┼────────┼──────────
 help-desk │ 96400 │ running │ 4103 │ 15s    │ 0
```

To have the system start it at login, write a service file while it's running, then load it with the command `service install` prints:

```text
$ frontmcp service install help-desk
[service] generating launchd service for "help-desk"...
[service] service file created: /Users/you/Library/LaunchAgents/dev.agentfront.frontmcp.help-desk.plist

To load the service:
  launchctl load /Users/you/Library/LaunchAgents/dev.agentfront.frontmcp.help-desk.plist

To start:
  launchctl start dev.agentfront.frontmcp.help-desk
```

`frontmcp restart help-desk` brings the server back after stopping it, and then stays in its terminal as the supervisor, the way `start` does. To keep the terminal you have, run `stop`, then `start`.

To give the server settings that only the running server needs, like a ticket system's address, put them in `frontmcp.config.ts`'s `env.ship`: `start` passes `env.shared` and `env.ship` to the server, under any variable already set in your environment or `.env`.

---

## Troubleshooting

### `Port 3000 is already in use — refusing to start.`

Another process, often another `frontmcp dev`, holds the port. Pass `--show-conflict` to see which, stop it, or use `--port` or `--auto-port`. A port written in `@FrontMcp({ http: { port } })` wins over all three.

### `dev_server_unreachable` from `frontmcp dev --stdio`

The server the bridge started didn't come up. Its first answer is `dev_reload_deadline`, then every request gets `{"code":-32099,"message":"dev_server_unreachable","data":{"reason":"degraded"}}`. `.frontmcp/dev.log` in the project has `initial-boot-failed` and, above it, the server's own output as `child-stderr` lines, with the error that stopped it.

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

A `vercel` or `lambda` build of 1.9.2, which stopped with `Error: Bundle failed:` and this error, twice (from `node_modules/@frontmcp/utils` and its `esm` folder), in a project without `@upstash/redis`, which is any project `frontmcp create` makes. Upgrade `frontmcp` to 1.9.3, which leaves the package out when it isn't installed, as it does every optional package, or install it, and it's bundled into `handler.cjs`:

```bash
npm install @upstash/redis
```

### `Module not found: Can't resolve '@opentelemetry/api'`

`frontmcp build --target vercel` and `--target lambda` of 1.9.2 printed this warning and finished. It's an optional package the SDK loads lazily inside a `try`, left as a `require()` call in `handler.cjs`; since 1.9.3 the build leaves it out without a warning. An installed optional package is bundled, except `better-sqlite3`, which is always left as a `require()`. (Before 1.8.7 these builds stopped on `Can't resolve '@frontmcp/storage-sqlite'` and others.)

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

The `lambda` target needs that package in the project: `npm install @codegenie/serverless-express`. A project made with `create --target lambda` has it already.

### `Cannot find module 'reflect-metadata'`

A bundle from `node` or `cli --js` was run away from the project's `node_modules`. Those bundles import your packages at run time. Run them in the project, or ship `node_modules` beside them. A `.mcpb` archive, with or without `--sea`, doesn't need them.

### The server answers `404` at `/mcp` (or at `/`)

The path comes from `http.entryPath` in `@FrontMcp`, else from `FRONTMCP_HTTP_ENTRY_PATH`, else it's `/`. `dev`, the `node` runner and bundle, an installed `node` bundle and the adapter builds set the variable from `transport.http.path` (or a deployment's `server.http.entryPath`); `start` and `socket` of a project's entry and a `cli` build's `serve` don't, so they answer at `/`. And an `http.entryPath` in the decorator wins over the config.

### `Entry override not found: ./src/main.ts`

The entry doesn't exist where it was looked for. A path you pass with `--entry` is resolved from the folder you type it in. With `--config`, the config's own `entry` is resolved from the folder you run in, not the config's, so a client that starts `frontmcp dev --stdio --config …` from elsewhere gets this: start it in the project's folder ([Connecting a stdio client](#connecting-a-stdio-client-while-you-work)). Before 1.9, running `build` or `dev` from any subfolder gave this too.

### `No test sources found.`

`frontmcp test` found no `jest.config.*`, `src/`, `__tests__/` or `e2e/` in the current folder. Run it from the project's root. `Module @swc/jest in the transform option was not found` means neither the project nor `@frontmcp/testing` provides `@swc/jest`: install `@frontmcp/testing` ([`@frontmcp/testing`](https://frontmcp.dev/reference/testing#module-swcjest-in-the-transform-option-was-not-found)).

### `Unknown build target: …`

The value of `--target` isn't one of `cli`, `node`, `sdk`, `browser`, `cloudflare`, `vercel`, `lambda`, `distributed`, `mcpb`. An unknown `target` in `deployments` fails earlier, as [`Invalid frontmcp.config`](https://frontmcp.dev/reference/server/config-files#invalid-frontmcpconfig).

### `Refusing to scaffold into non-empty directory: …`

The folder `create` would use already has files. Pick another name, or empty the folder.

### `Invalid --target value 'deno'. Expected one of: node, vercel, lambda, cloudflare.`

`frontmcp create` was given a value it doesn't know for `--target`, `--pm`, `--redis` or `--skills`. It stops before writing anything, so run it again with one of the listed values.

### ``frontmcp.config has no `clients.cursor` entry``

`eject-mcp-config` only prints clients listed in `clients`. Add one, like `cursor: { transport: "http" }`.

### `Existing client config is not valid JSON, refusing to overwrite it`

`eject-mcp-config -o` was pointed at a file that doesn't parse, for example one with comments. It left the file alone. Fix the file, or write the snippet to a new path and merge by hand.

### `Could not find or generate a manifest`

`frontmcp install` found no `*.manifest.json` in the source, its `dist/` or a folder of `dist/`, and no `frontmcp.config.*` to build from. Run `frontmcp build --target node` first, or point at the folder that has the manifest.

### `@frontmcp/sdk skill storage needs the optional peer dependency 'vectoriadb'`

In the error log of a server added with the 1.8.7 `install`, which didn't install `vectoriadb` or `tslib`. Install it again with 1.9.4, or run `npm install vectoriadb tslib` in `~/.frontmcp/apps/<name>/`.

### `No PID file found for "…"`

`service install` needs the server's PID file, which `frontmcp start <name>` writes. Start it once, then install the service.

### `Cannot find module 'better-sqlite3'` with `--db`

`--db` (and `FRONTMCP_SQLITE_PATH`) turns on the SQLite stores, which need `better-sqlite3` and `@frontmcp/storage-sqlite` installed in the project: `npm install better-sqlite3 @frontmcp/storage-sqlite`.

### `listen EINVAL` from `frontmcp socket`

The socket path is too long for the system (about 100 characters on macOS). Use a shorter `--socket`, like `/tmp/help-desk.sock`.

### `invalid type: sequence, expected a map` in `mcp_servers`

Codex can't read `~/.codex/config.toml` after a `cli` build's `install -p codex`, or `frontmcp plugin install --codex`: in 1.9.4 both write `[[mcp_servers]]`, and Codex expects a table per server. Run `<name> uninstall -p codex`, then add `[mcp_servers.<name>]` with `command` and `args` by hand: see [the pitfall](#installing-into-claude-code-and-codex).

### `Could not read bin-meta.json at …`

`install -p` or `uninstall -p` of a `cli` build looks for `bin-meta.json` next to the bundle, and it's not there. The build writes it, with `_skills/`, into `dist/cli/`: ship the whole folder, as `"files": ["dist/cli"]` does in a [package on npm](https://frontmcp.dev/reference/deployment/mcp-clients#publishing-a-server-to-npm).

### `Server URL required. Use --server <url> or set FRONTMCP_SERVER_URL.`

`login` of a `cli` build has no server to sign in to. Pass `--server <url>`, or set `FRONTMCP_SERVER_URL`. In 1.9.4 `oauth.serverUrl` in the deployment's `cli` block doesn't give it a default: [the `cli` options](#the-cli-options-and-signing-in) says why. For a key the user already has, `connect --token` needs no server.

### `Unrecognized key: "setup"`

`Invalid frontmcp.config:` with this line means a config with `deployments` has a `setup` block, which it doesn't accept. In 1.9.4 setup questions work only in the older shape, without `deployments`: see [user settings](#bundle-metadata-and-user-settings).

### A `.mcpb` asks for a setting, and the server never gets it

The setting comes from the `mcpb` deployment's `userConfig`, which goes into the manifest's `user_config` but not into `mcp_config.env`, so the host asks and passes nothing on. Declare it as a setup step instead, which maps it to a variable: see [user settings](#bundle-metadata-and-user-settings).
