frontmcp 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.

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:

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

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

Global options

OptionDescription
-V, --versionPrint the CLI's version and exit.
-h, --helpPrint 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-commandsPrint 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

GroupCommandWhat it does
Getting startedcreate [name]Scaffold a project, or an Nx workspace with --nx.
initCreate tsconfig.json, or add the settings FrontMCP needs to the one you have.
doctorCheck Node, npm, tsconfig.json and the entry file.
DevelopmentdevRun the server with tsx --watch, and tsc --noEmit --watch beside it.
buildBuild for a target: node, cli, sdk, browser, vercel, lambda, cloudflare, distributed or mcpb.
test [patterns...]Run Jest with a generated configuration.
inspectorRun npx @modelcontextprotocol/inspector, pointed at the server frontmcp.config describes.
Process managerstart, stop, restart, status, list, logs, socket, serviceRun a server under a supervisor that restarts it, and write service files for it.
Package managerinstall, uninstall, configureCopy a built server into ~/.frontmcp/apps/.
Skillsskills search, list, install, export, publish, readBrowse and copy the FrontMCP skills catalog.
Othermcpb validate <path>Check a .mcpb archive.
eject-mcp-config <client>Print a client's MCP server entry from frontmcp.config's clients, or merge it into the client's file.
plugin install, uninstall, statusWrite the project out as a plugin for a coding-agent host.
Your own commandsAnything in frontmcp.config's cli.commands.

frontmcp create

frontmcp create [name] [options]
OptionValuesDefaultDescription
namefrontmcp-appThe project's folder and package.json name. It's lowercased, and characters other than letters, digits, ., _ and - become -: "My Desk" makes my-desk.
-y, --yesDon't ask; use the defaults and the flags given.
--target <target>node, vercel, lambda, cloudflarenodeThe 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, nonedockernode 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, pnpmnpmUsed in the generated scripts, Dockerfile and workflows. yarn also writes .yarnrc.yml (nodeLinker: node-modules).
--cicd, --no-cicdonWrite .github/workflows/ci.yml, e2e.yml and deploy.yml.
--skills <bundle>recommended, minimal, full, nonerecommendedCopy skills from the catalog into skills/: 9, 4, all 13, or none.
--nxScaffold an Nx workspace instead, with @frontmcp/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:

PathContents
src/main.ts, src/calc.app.ts, src/tools/add.tool.tsA server with one app and an add tool.
e2e/server.e2e.spec.tsFour tests frontmcp test finds.
frontmcp.config.tsname, entry, one deployment, transport.http on port 3000 at /mcp, env.dev, and one clients entry for eject-mcp-config.
package.jsonScripts 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.jsonWritten by init.
ci/Dockerfile, ci/docker-compose.yml, ci/.env.docker, ci/.env.docker.example, .dockerignorenode 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 has the details.
ci/template.yaml, vercel.json, wrangler.tomllambda 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 fileNotes 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:

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.

frontmcp doctor

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

✅ 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.

✅ 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

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.

OptionDescription
-e, --entry <path>The server's entry file. Default: frontmcp.config's entry, then the usual search. 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-portWhen 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-conflictWhen the port is taken, print which process holds it (lsof).
--stdioAct as a stdio bridge for an MCP client: below.
--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 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.

[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 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

frontmcp build [options]
OptionDescription
-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.
--jscli only: a JavaScript bundle instead of a single executable.
--seamcpb only: add a single-executable binary for this machine's platform (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.
--no-deterministicmcpb 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-onlymcpb only: leave the files in <out>/mcpb/__stage/ and skip the zip.
--no-cleanDon't empty the target's folder first. For node and cli, the intermediate files the build writes there are deleted anyway (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.

TargetWrites to dist/<target>/In 1.9.4
node<name>.bundle.js (your code; packages stay imports), <name> (a shell runner), <name>.manifest.json, install-<name>.shBuilds and runs where node_modules is.
cliThe 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 sourcesBuilds.
browser<name>.browser.mjs and its source mapBuilds.
vercelindex.js and serverless-setup.js, then a handler.cjs bundle, .vercel/output/, and vercel.json in the project when there's noneBuilds, without a warning (in 1.9.2 it stopped on @upstash/redis: Troubleshooting). The bundle starts, answers at /mcp, and reads NODE_ENV when it runs, not when it was built.
lambdaThe same, for LambdaStops 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.
cloudflareindex.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 stayBuilds, with Cloudflare Workers adapter is experimental. It accepts redis: { provider: "vercel-kv" } and still refuses an ioredis-style redis block.
distributedindex.js and serverless-setup.js, which sets FRONTMCP_DEPLOYMENT_MODE=distributedBuilds.
mcpb<name>-<version>.mcpb, a zip with manifest.json, server/index.js and README.mdBuilds, 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 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 has the details.

The node target

frontmcp build --target node
[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 flagDoes
noneServe over HTTP on PORT, or 3000.
--stdioServe over stdin and stdout instead (sets FRONTMCP_STDIO=1).
--version, --help, --print-manifestPrint 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

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".

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 binary runs the server in its own process for each command:

./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:

CommandDoes
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: 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, logsRun the server in the background, on a socket: see the 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 runs.

The cli options and signing in

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

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 },
    },
  ],
});
FieldWhat frontmcp build does with it in 1.9.4
cli.descriptionThe line under Usage: in --help. Default: <name> CLI.
cli.outputDefault"text" or "json": the default of --output.
cli.authRequiredtrue adds the Auth commands below.
cli.excludeTools, cli.oauthAccepted, 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, seaAccepted, 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:

CommandDoes
connect --token <token>Stores the token for a session, default unless --session <name>. Without --token it prints how to use it and exits 1.
loginSigns 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.
logoutDeletes 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(), 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.

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
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
OptionDescription
-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-commandsLeave out the plugin's skills/ or commands/ folder.
--only-mcpOnly 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-runPrint the plan and write nothing.
--statusPrint, 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 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.

The mcpb target

frontmcp build --target mcpb
frontmcp mcpb validate dist/mcpb/help-desk-0.1.0.mcpb
[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:

[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:

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 fieldIn manifest.jsonWithout it
displayNamedisplay_nameNone.
longDescriptionlong_description, and its first line is descriptiondescription is package.json's description.
authorauthor, { 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, keywordsThe same namespackage.json's.
repositoryrepository, as { type, url }; a string gets type: "git"package.json's.
documentation, supportThe same namesNone.
privacyPoliciesprivacy_policiesNone.
iconThe 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.
compatibilitycompatibility: claude_desktop, platforms, runtimesplatforms is all three; runtimes.node is nodeVersion, else >=22.0.0.
userConfiguser_configSee below.
sea{ enabled, mergeFrom }: the same as --sea and --merge-from.
deterministicfalse keeps each file's real date, so every build has another sha256, as with --no-deterministic.
envSet when server/index.js starts, for variables the host doesn't set, like the other targets' env.
includeNodeModulesAccepted, 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 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:

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 } }
    ]
  }
}
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

frontmcp test [patterns...] [options]
OptionDescription
patternsOnly files whose path matches, as Jest's own patterns: frontmcp test tickets.
-i, --runInBandOne file at a time. Default: test.runInBand in frontmcp.config.
-w, --watchRun again when files change.
-v, --verbosePassed 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, --coverageCollect coverage into coverage/, from src/**/*.ts(x).
--no-envDon'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 covers the tests themselves.

[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.

[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 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.

CommandOptionsDescription
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 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, --forceStop 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.
listEvery 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, --backgroundServe 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).
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.
$ 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'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.

$ 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

CommandOptionsDescription
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, --yesAsk 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.

CommandOptionsDescription
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, --categoryRank 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-runSubmit a skill to a public marketplace. --dry-run prints the request instead.

frontmcp mcpb validate

frontmcp mcpb validate dist/mcpb/help-desk-0.1.0.mcpb
[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:

[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

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> fieldUsed for
nameThe server's key. Default: the config's name.
transporthttp or sse: an entry with url and transport. stdio: an entry with command and args.
urlThe URL. Default: http://<transport.http.host or 127.0.0.1>:<transport.http.port><transport.http.path or /mcp>.
command, argsFor stdio. Default npx and ["-y", <name>].
envCopied 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

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 has the fix). The server entry runs <name> serve --stdio, so it expects a cli build installed on the PATH under the project's name. A cli build's own install -p 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).

OptionDescription
--scope <project|user>Default project.
--dir <dir>Another destination root.
--no-skills, --no-commandsLeave out the skills/ or commands/ folder.
--only-mcpOnly the server entry, no plugin folder.
--command <cmd>Another command for the server entry.
--env <name>A variable placeholder to add. Repeatable.
--dry-runPrint 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 lists the fields; Usage 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

CodeWhen
0Success, --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.
1Any 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.
2eject-mcp-config with an unknown client; the node runner with an unknown flag; a usage error or invalid input in a cli build.
The script'sA project command.

What it reads and writes

Path or variableRead 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 lists every field and who reads it.
package.jsonmain for the entry; name and version for build without a config file, and version for a config without one.
tsconfig.jsonbuild 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.localdev, 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.logWritten by dev --stdio, in the project's folder.
FRONTMCP_SQLITE_PATH, FRONTMCP_DAEMON_SOCKETSet for the server by --db and by socket (and start --socket). The SDK reads both.
FRONTMCP_HTTP_ENTRY_PATH, FRONTMCP_* security-header variablesSet 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

npx frontmcp create help-desk --yes
cd help-desk
npm install
npm run dev
[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:

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.

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:

$ 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:

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:

{
  "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

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:

{ "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:

[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:

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:

frontmcp build --target cli
./dist/cli/help-desk add --a 2 --b 3
{"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.

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, build again, and store the user's key once:

./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
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.

Packaging a .mcpb bundle

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

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:

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:

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 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

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:

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 has the rest.

Connecting a client

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

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" } },
  },
});
frontmcp eject-mcp-config cursor
frontmcp eject-mcp-config cursor -o .cursor/mcp.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:

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" }
        ]
      }
    }
  }
}
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);
$ 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:

frontmcp start help-desk --port 4103     # terminal 1
frontmcp list                            # terminal 2
frontmcp logs help-desk -n 20
frontmcp stop help-desk
 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:

$ 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:

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). 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).

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.

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.

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.

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 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.

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.