FrontMcpInstance

FrontMcpInstance builds a server from the configuration you give @FrontMcp and runs it. You rarely call it: importing a class decorated with @FrontMcp calls FrontMcpInstance.bootstrap(), which builds the server and starts an HTTP server. Its other static methods run the same configuration in other ways: over stdio, on a Unix socket, as a handler for a serverless platform or a Worker, or in your own process.

await FrontMcpInstance.bootstrap(config)

Reference

FrontMcpInstance.bootstrap(config)

Call bootstrap() to start the HTTP server yourself, for example after connecting to a database. Set serve: false on @FrontMcp, so importing the class doesn't start a second server, and pass the same configuration:

main.ts
import "reflect-metadata";
import { FrontMcp, FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";
import { connectToDatabase } from "./db";

@FrontMcp({ ...config, serve: false })
export default class HelpDeskServer {}

await connectToDatabase();
await FrontMcpInstance.bootstrap(config);

See more examples below.

Starting a server needs a real port and a Node process, so the Playground can't run bootstrap(), createHandler(), runStdio() or runUnixSocket(). What this page says about them was checked by running them in Node.

Parameters

ParameterTypeDescription
configFrontMcpConfigInputThe object you'd pass to @FrontMcp: info, apps and the rest. Not the decorated class: a class fails with a ZodError, Invalid input: expected object, received undefined.

Returns

A promise that resolves once the server is listening, and logs MCP HTTP (Express) on 127.0.0.1:3000. It rejects when the configuration is invalid or the server can't listen, for example listen EADDRINUSE: address already in use 127.0.0.1:3000 when the port is taken.

bootstrap() returns nothing to stop the server with. The process ends it.

What @FrontMcp does on import

When the decorated class is defined, @FrontMcp checks the configuration, records it on the class, and then, depending on the environment:

EnvironmentWhat happens
FRONTMCP_SCHEMA_EXTRACT setNothing more. The configuration is only recorded.
FRONTMCP_STDIO setrunStdio(config): serve one client over stdin and stdout. Nothing, with serve: false.
FRONTMCP_SERVERLESS setcreateHandler(config), or createFetchHandler(config) if FRONTMCP_WORKER is set too. The handler is kept for getServerlessHandler().
None of thosebootstrap(config), unless the configuration has serve: false.

A flag counts as set when its value is 1 or true; yes or on don't count. frontmcp build relies on these: its Vercel and Lambda entry files set FRONTMCP_SERVERLESS=1 before importing your server and export the handler from getServerlessHandlerAsync(), and its Cloudflare entry sets FRONTMCP_WORKER=1 as well.

If starting fails, for example because the port is taken, nothing catches the error: Node reports it and the process exits with code 1.

FRONTMCP_SCHEMA_EXTRACT is for tools that need your configuration without running your server. frontmcp build uses it, and so does this site's Playground, which is why importing an example's @FrontMcp class here never opens a port. Read the recorded configuration with getDecoratorConfig():

import { getDecoratorConfig } from "@frontmcp/sdk";
import HelpDeskServer from "./main";

const config = getDecoratorConfig(HelpDeskServer); // the parsed configuration, or undefined

Static methods

MethodReturnsUse it to
bootstrap(config)Promise<void>Start the HTTP server. See above.
createFetchHandler(config)Promise<(request, ctx?, env?) => Promise<Response>>Serve MCP from a runtime that hands you a web Request: Cloudflare Workers, Deno, Bun, a framework's route. See createFetchHandler().
createHandler(config)Promise<unknown>, an Express appServe MCP from a platform that calls a Node (req, res) handler, like Vercel or AWS Lambda. See below.
createDirect(config, { app }?)Promise<DirectMcpServer>Call tools, resources and prompts from your own code, with no transport. app picks an app's own endpoint, and a workerEnv in config sets the bindings its calls read (since 1.9.4). See create() and createDirect().
runStdio(configOrClass)Promise<void>Serve one local client over stdin and stdout. See below.
runUnixSocket(options)Promise<{ close }>Serve over a Unix socket instead of a TCP port. See below.
createForGraph(config)Promise<FrontMcpInstance>Build the server without serving it, to inspect it. See below.
createForCli(config)Promise<FrontMcpInstance>The same, for the frontmcp CLI's own commands: it skips widget compilation and the event and elicitation stores to start faster.

To connect an MCP client in memory instead, use connect(). It isn't a method of FrontMcpInstance.

Every method except runStdio() needs the configuration object. connect() and runStdio() also accept the decorated class.

FrontMcpInstance.createHandler(config)

Builds the server without listening, and returns the Express app that would have listened, a Node (req, res) handler. Export it from the entry file of a platform that calls one:

api/mcp.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "../src/config";

export default await FrontMcpInstance.createHandler(config);

It serves the same paths as bootstrap(), and works with http.createServer(handler) too. With serve: false in the configuration there's no Express app to return, and the promise resolves to undefined.

FrontMcpInstance.runStdio(configOrClass)

Serves one client over stdin and stdout, for clients that start your server as a subprocess, like Claude Desktop. It opens no port.

stdio.ts
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

FrontMcpInstance.runStdio(config);
  • stdout carries the protocol, so from the moment you call it, runStdio() sends console.log, console.info and console.debug to stderr. A console.log before the call still writes to stdout and breaks the client.
  • The promise resolves once the client is connected, not when it disconnects.
  • On SIGINT or SIGTERM it shuts the server down and exits with code 0, within 5 seconds.
  • Callers over stdio are anonymous: this.auth.isAnonymous is true and this.auth.user.sub is "".

It accepts the decorated class as well as the configuration. The class must be imported with FRONTMCP_STDIO=1 already set, or importing it starts an HTTP server first.

FrontMcpInstance.runUnixSocket(options)

Serves over a Unix socket file instead of a TCP port, so only processes on the same machine that can open the file can connect:

const handle = await FrontMcpInstance.runUnixSocket({ ...config, socketPath: "/tmp/help-desk.sock" });

options is the configuration plus socketPath and an optional sqlite. bootstrap() does the same when the FRONTMCP_DAEMON_SOCKET environment variable names a socket path. On SIGINT or SIGTERM it shuts down like bootstrap(): it deletes the socket file, so new clients can't connect, lets requests in flight finish for up to 5 seconds, and exits with code 0.

handle.close() runs the same shutdown without exiting: it resolves once requests in flight have finished and the socket file is gone. (Before 1.9.3, SIGTERM exited at once and close() only deleted the socket file.)

getServerlessHandler() and getServerlessHandlerAsync()

With FRONTMCP_SERVERLESS=1, @FrontMcp builds a handler instead of listening. Import the decorated class, then get the handler:

import { getServerlessHandlerAsync } from "@frontmcp/sdk";
import "./main"; // defines the @FrontMcp class

const handler = await getServerlessHandlerAsync();

getServerlessHandlerAsync() waits until the handler is ready. getServerlessHandler() returns it at once, or null while it's still being built. Both throw what building threw. Without FRONTMCP_SERVERLESS, getServerlessHandlerAsync() throws Serverless handler is not initialized.

Instance members

bootstrap() builds an instance and starts it. createForGraph() returns one that's built and not started.

MemberDescription
configThe parsed configuration, with defaults filled in.
getConfig()Returns config.
readyA promise that resolves once the server is built: its apps, tools, resources and the rest.
getScopes()The server's scopes: one for the whole server, or one per app with splitByApp: true.
getPrimaryScope()The scope that runStdio() serves, and createDirect() and connect() without an app: the one holding every app that isn't standalone, root. With splitByApp: true, or when every app is standalone, the first scope.
getAppScope(app)The scope an app has of its own, served at <entryPath>/<app id>: each app's with splitByApp: true, a standalone app's otherwise. createDirect() and connect() serve it when given that app. Throws ScopeConfigurationError, No endpoint serves app "…" on its own, for an app without one, naming the apps that have one. New in 1.9.3.
start()Starts the HTTP server of a built instance, then runs the callbacks registered with scope.onServerStarted().
initialize()Builds the server. The constructor calls it and keeps the promise in ready; don't call it again.

The constructor, new FrontMcpInstance(config), takes a configuration that has already been parsed. Use the static methods, which parse it for you.

Ports and the entry path

SettingWhere it comes from
Porthttp.port, else the PORT environment variable, else 3000. PORT is read when the server's options are parsed, so setting process.env.PORT in code before bootstrap() works. (Changed in 1.9: before, it was read when @frontmcp/sdk was first imported, and a later change had no effect.)
Address127.0.0.1 unless http.security.bindAddress or FRONTMCP_BIND_ADDRESS says otherwise. See http.
MCP endpointhttp.entryPath, else the FRONTMCP_HTTP_ENTRY_PATH environment variable, else the root, /.
Health checks/healthz, /health and /readyz, whatever the entry path.
With splitByApp: trueEach app at /<app id> under the entry path. The root answers 404.

frontmcp dev sets FRONTMCP_HTTP_ENTRY_PATH to the path in its own configuration.

Lifecycle and shutdown

bootstrap() goes through these steps:

  1. Parses the configuration, and fails on anything invalid.
  2. Builds the server: its providers, its logger, then its scopes, which build the apps and their tools, resources, prompts, jobs and plugins. This is ready.
  3. Starts the HTTP server and waits until it listens.
  4. Runs every scope's onServerStarted() callbacks, then logs FrontMCP bootstrap complete.

On SIGINT or SIGTERM it shuts the server down: it stops taking new connections, lets requests in flight finish for up to 5 seconds, closes what the server holds (its Redis connections, and a distributed instance's heartbeat), and exits 0, or 1 when a step fails or the whole shutdown takes more than 10 seconds. See stopping a Node server. A server on a Unix socket is the exception: it removes the socket file and exits at once, as described above. (Changed in 1.9.2: before, bootstrap() installed no signal handlers, and SIGTERM ended the process at once.)

Caveats

  • Importing a decorated class starts a server. Keep the configuration in a module of its own, and decorate a class with it only in the entry file, so scripts, tests and workers can import the configuration without starting anything.
  • Only runStdio() and connect() accept the decorated class. The other methods need the configuration object. Pass getDecoratorConfig(Server) if all you have is the class.
  • With splitByApp: true, or a standalone app, only bootstrap(), createHandler() and createFetchHandler() serve every app. runStdio() serves the primary scope only: the apps that aren't standalone, or with splitByApp: true the first app. createDirect() and connect() do too, unless they're given an app, which serves that app's own scope. See Apps, discovery and splitting. (Changed in 1.9.3: before, createDirect() and connect() had no app option. Changed in 1.9.2: createFetchHandler() serves them all; see its entry paths.)
  • Declare one @FrontMcp per process. Each bootstrap() starts another server, and a second one on the same port fails with EADDRINUSE.

Usage

Starting the server after setup

A server whose tools need a database connection, or secrets fetched at startup, shouldn't accept calls before they're ready. Keep the configuration in its own module, turn off serve, and call bootstrap() when you're ready:

config.ts
import { HelpDesk } from "./help-desk.app";

export const config = {
  info: { name: "help-desk", version: "1.0.0" },
  apps: [HelpDesk],
  http: { port: 8080, entryPath: "/mcp" },
};
main.ts
import "reflect-metadata";
import { FrontMcp, FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";
import { connectToDatabase } from "./db";

@FrontMcp({ ...config, serve: false })
export default class HelpDeskServer {}

try {
  await connectToDatabase();
  await FrontMcpInstance.bootstrap(config);
} catch (err) {
  console.error("help-desk didn't start:", err);
  process.exit(1);
}

Clients connect to http://localhost:8080/mcp. This needs a real port, so run it locally.

Reading a server's configuration

@FrontMcp records the configuration on the class, with every default filled in. getDecoratorConfig() reads it back, and it's what to pass to the methods that don't take the class. In the Playground, FRONTMCP_SCHEMA_EXTRACT is set, so defining HelpDeskServer records the configuration and starts nothing; the Playground then serves it with createFetchHandler().

Open
import { FrontMcp } from "@frontmcp/sdk";
import { HelpDesk } from "./help-desk.app";

@FrontMcp({ info: { name: "help-desk", version: "1.0.0" }, apps: [HelpDesk] })
export default class HelpDeskServer {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

Inspecting a server without serving it

createForGraph() builds the whole server, apps, tools and all, and doesn't serve it. Use it to check what a configuration produces: in a test, or in a script that lists every tool. A server has one scope, root, holding every app's tools. With splitByApp: true it has one scope per app, each at its own path, and a standalone app always gets one of its own.

Open
import { test, expect } from "@frontmcp/testing";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { Billing, BillingApi, HelpDesk } from "./apps";

const info = { name: "help-desk", version: "1.0.0" };

function describe(instance: FrontMcpInstance) {
  return instance.getScopes().map((scope) => ({
    id: scope.id,
    path: scope.routeBase,
    tools: scope.tools.getTools().map((tool) => tool.metadata.name),
  }));
}

test("one scope holds every app's tools", async () => {
  const instance = await FrontMcpInstance.createForGraph({ info, apps: [HelpDesk, Billing] });
  expect(describe(instance)).toEqual([{ id: "root", path: "", tools: ["get_ticket", "refund_invoice"] }]);
});

test("with splitByApp, each app is a scope at its own path", async () => {
  const instance = await FrontMcpInstance.createForGraph({ info, apps: [HelpDesk, Billing], splitByApp: true });
  expect(describe(instance)).toEqual([
    { id: "help-desk", path: "/help-desk", tools: ["get_ticket"] },
    { id: "billing", path: "/billing", tools: ["refund_invoice"] },
  ]);
  expect(instance.getPrimaryScope()?.id).toBe("help-desk");
});

test("a standalone app gets a scope of its own; createDirect() serves the primary one, or that one with `app`", async () => {
  const config = { info, apps: [BillingApi, HelpDesk] };
  const instance = await FrontMcpInstance.createForGraph(config);
  expect(describe(instance)).toEqual([
    { id: "billing-api", path: "/billing-api", tools: ["refund_invoice"] },
    { id: "root", path: "", tools: ["get_ticket"] },
  ]);
  expect(instance.getPrimaryScope()?.id).toBe("root");
  expect(instance.getAppScope("billing-api").routeBase).toBe("/billing-api");
  expect(() => instance.getAppScope("help-desk")).toThrow('No endpoint serves app "help-desk" on its own');

  const server = await FrontMcpInstance.createDirect(config);
  const { tools } = await server.listTools();
  await server.dispose();
  expect(tools.map((tool) => tool.name)).toEqual(["get_ticket"]);

  const billing = await FrontMcpInstance.createDirect(config, { app: "billing-api" });
  const billingTools = await billing.listTools();
  await billing.dispose();
  expect(billingTools.tools.map((tool) => tool.name)).toEqual(["refund_invoice"]);
});

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

A scope's registries are described in Scope and registries.

Serving over stdio

Local clients like Claude Desktop and Cursor can start your server as a subprocess and talk to it over stdin and stdout. Give them an entry file that calls runStdio() with the configuration, and never imports the decorated class:

stdio.ts
import "reflect-metadata";
import { FrontMcpInstance } from "@frontmcp/sdk";
import { config } from "./config";

FrontMcpInstance.runStdio(config);
claude_desktop_config.json
{
  "mcpServers": {
    "help-desk": { "command": "node", "args": ["/abs/path/dist/stdio.js"] }
  }
}

Or keep one entry file and set FRONTMCP_STDIO=1 in the client's configuration ("env": { "FRONTMCP_STDIO": "1" }): @FrontMcp then calls runStdio() instead of starting an HTTP server. Anything the server writes to stdout that isn't a protocol message breaks the client, so log to stderr.


Troubleshooting

Invalid input: expected object, received undefined

The error is a ZodError with invalid_union, whose issues are at info and at apps (expected array, received undefined). You passed the decorated class to a method that takes the configuration object: bootstrap(), createFetchHandler(), createDirect() or createForGraph(). The class has no info or apps of its own: they're recorded on it. (Changed in 1.9: before, the issue was expected object, received function.)

// 🚩 The class
await FrontMcpInstance.createDirect(HelpDeskServer);

// ✅ Its configuration
await FrontMcpInstance.createDirect(getDecoratorConfig(HelpDeskServer)!);

Better, export the configuration from a module of its own and pass that. The first Playground shows both.

listen EADDRINUSE: address already in use 127.0.0.1:3000

Something already listens on the port: another copy of your server, or a second bootstrap() in the same process, often because a script imported the decorated class and then called bootstrap() too. Stop the other process, or choose another port with http.port or the PORT environment variable. Set PORT in the environment that starts the process, or in code before the server starts.

A script that imports my server starts listening

Importing a class decorated with @FrontMcp starts a server. Import the configuration module instead, or set serve: false on the decorator and start the server where you want it with bootstrap().

Serverless handler is not initialized

getServerlessHandlerAsync() found no handler, because FRONTMCP_SERVERLESS wasn't set to 1 or true when the decorated class was defined. Without it, @FrontMcp called bootstrap() and started an HTTP server instead. Set the variable in the platform's environment, or skip the decorator and call createHandler(config) or createFetchHandler(config) yourself.

My client gets 404 or Cannot POST /mcp

The MCP endpoint is at the root unless http.entryPath is set. See the @FrontMcp entry.

Only my first app is reachable

The server uses splitByApp: true and is served by runStdio(), or by createDirect() or connect() without an app, which use the first scope only. Pass { app } to createDirect() or connect() for another app's scope, serve the server with bootstrap(), createHandler() or createFetchHandler(), which give each app its path, or turn splitByApp off.