Connecting MCP clients

An MCP client reaches a server in one of two ways. It connects to a URL, for a server that's already running somewhere: a deployed one, or frontmcp dev on your machine. Or it starts the server itself, as a child process, and talks to it over stdin and stdout. FrontMCP serves both from the same build; what matters is the URL's path, the command, and keeping everything but protocol messages off stdout. This page covers each, publishing a server to npm so clients can start it with npx, the snippet the CLI prints for a client's settings, and the MCP Bundle target.

{ "mcpServers": { "help-desk": { "url": "https://desk.example.com/mcp" } } }
{ "mcpServers": { "help-desk": { "command": "/opt/help-desk/dist/node/help-desk", "args": ["--stdio"] } } }

Reference

Ways to connect

Client connectsServer runs asClients per serverPage
To a URL, over Streamable HTTPFrontMCP's Node server, a serverless function or a WorkerManyOver HTTP
By starting a command, over stdioA process the client starts and stopsOneOver stdio
To a Unix socketFrontMCP's Node server on a socket fileMany, on the same machineOver a Unix socket
In the same processconnect() or createDirect()Your code

Most clients keep their servers in a JSON file with an mcpServers object, one entry per server; the field that says "this entry is a URL" differs between clients (type, transport, or none), so check your client's documentation for it. The shapes on this page are the server's side of the contract.

Over HTTP

The URL is the server's public address plus its MCP endpoint, http.entryPath:

The serverIts URL
frontmcp dev in a project from frontmcp createhttp://localhost:3000/mcp: the port and path of transport.http in frontmcp.config
The node build, started with its script dist/node/<name> or with node dist/node/<name>.bundle.js, which a Docker image's CMD runshttp.entryPath, else transport.http.path (/mcp in a project from frontmcp create). Changed in 1.9 for node dist/node/<name>.bundle.js: it served /.
A vercel, lambda, distributed or cloudflare buildhttp.entryPath, else transport.http.path (changed in 1.8.7 for the first three: they served /)
A server started without the frontmcp command, like tsx src/main.ts/ of its address, unless http.entryPath is set: see the endpoint path

What a client sends and gets:

  • Clients on MCP 2026-07-28 send a POST for every call, with the method and name repeated in headers, and keep no session. See request headers.
  • Older clients start with initialize, get an Mcp-Session-Id, and send it with every request. A server that runs as several instances needs Redis for them.
  • With auth, a request without credentials gets 401 and a WWW-Authenticate header. For static auth, give the client the key as a header, Authorization: Bearer <key>; for local, remote and transparent, a client that supports MCP's OAuth follows the header to the sign-in: see the flow a client follows.
  • Clients that run in a web page need CORS.

Over stdio

The client starts the command with its arguments and environment, writes JSON-RPC messages to its stdin, one per line, and reads answers from its stdout. So stdout must carry nothing else.

CommandServes stdio
dist/node/<name> --stdio, the node build's start scriptYes. It sets FRONTMCP_STDIO=1 and runs the bundle.
node dist/node/<name>.bundle.js --stdio, or FRONTMCP_STDIO=1 node dist/node/<name>.bundle.jsYes. (Changed in 1.9: before, the bundle ignored --stdio, started the HTTP server and wrote its log to stdout, which the client couldn't parse.)
dist/cli/<name> --stdio, or dist/cli/<name> serve --stdio, from --target cliYes.
npx -y <package> --stdio, for a server published to npmYes. Without --stdio, the program prints its help and exits.
An entry file that calls FrontMcpInstance.runStdio()Yes.

In stdio mode no port is opened, so several copies can run at once. Logs go to stderr, and to a file in ~/.frontmcp/logs/ (where the lines go); anything your own code prints with console.log before the server starts still reaches stdout. Configuration comes from the environment the client gives the process, through its env field: the same variables as any deployment, like secrets and NODE_ENV. Each client starts its own process, so nothing is shared between clients, and the process ends when the client closes it.

Over a Unix socket

A server on a socket (Serving on a Unix socket) speaks the same HTTP as on a port, to anyone who can open the file. A client needs to support sockets to use it; curl --unix-socket can, for testing:

curl --unix-socket ./help-desk.sock http://localhost/healthz

The host name in the URL doesn't matter on a socket; the path does.

frontmcp eject-mcp-config <client>

Prints the mcpServers entry for a client, built from clients.<client> in frontmcp.config. frontmcp eject-mcp-config --help lists the clients it knows; -o <file> writes the snippet to a file instead. It creates the file's missing folders, and when the file already holds a client config it merges the entry into it and keeps the rest (Wrote … (merged into existing config)); a file that isn't valid JSON is refused and left alone (Existing client config is not valid JSON, refusing to overwrite it). Changed in 1.8.7: -o didn't create folders and overwrote an existing file.

clients.<client>Prints
{ transport: "http", url? }{ "url": …, "transport": "http" }. Without url: http://<transport.http.host, or 127.0.0.1>:<transport.http.port><transport.http.path, or /mcp>.
{ transport: "stdio", command?, args?, env? }{ "command": …, "args": […], "env": {…} }, with npx -y <name> when command and args are missing. env is env.shared and env.ship from frontmcp.config, then the client's own env, which wins (since 1.9).

The entry's key is clients.<client>.name, else the config's name. With env: { shared: { DESK_REGION: "eu-west" }, ship: { NODE_ENV: "production" } } and a client entry with env: { DESK_REGION: "us-east" }, the snippet's env was { "DESK_REGION": "us-east", "NODE_ENV": "production" }. A client missing from clients fails with frontmcp.config has no clients.<client> entry. Projects from frontmcp create have one HTTP client entry.

MCP Bundles

frontmcp build --target mcpb writes dist/mcpb/<name>-<version>.mcpb, a ZIP archive for desktop clients that install MCP Bundles: a manifest.json (MCPB 0.3) with the name, version, the tools found in your server, and server.mcp_config: { command: "node", args: ["${__dirname}/server/index.js"], env: { FRONTMCP_STDIO: "1" } }, plus server/index.js, your code with FrontMCP's packages bundled into it (about 10 MB, so a small server's archive is about 2 MB). The archive needs no node_modules: extracted into an empty folder, FRONTMCP_STDIO=1 node server/index.js answered initialize and a tools/call on stdin, wrote nothing else to stdout, and logged to stderr. frontmcp mcpb validate <file> checks an archive, and now fails one whose server/index.js still require()s @frontmcp/sdk or reflect-metadata (validate below). The build is deterministic, and prints the archive's sha256.

The archive has no widget files: frontmcp build --target mcpb doesn't copy them, though node, cli and the serverless targets do, so a tool whose widget is a file has no file to read in the extracted archive.


Usage

The Playground is itself an MCP client, but it can't show another client's settings, so this section uses code blocks. Each server command was run with FrontMCP 1.9.2, with a small script playing the client; no MCP desktop or editor client was used.

Connecting to frontmcp dev

npx frontmcp dev
[dev] listening on port: 3000
[dev] MCP endpoint path: /mcp
{ "mcpServers": { "help-desk": { "url": "http://localhost:3000/mcp" } } }

frontmcp dev restarts the server when you change a file. Built for production, the same server answers at /mcp too, started with the node build's script or with node dist/node/<name>.bundle.js.

Connecting to a deployed server with a key

For a server with auth: { mode: "static", tokens: [process.env.DESK_API_KEY] }, the client sends the key with every request:

{
  "mcpServers": {
    "help-desk": {
      "url": "https://desk.example.com/mcp",
      "headers": { "Authorization": "Bearer ${DESK_API_KEY}" }
    }
  }
}

Whether a client supports headers, and how it fills in ${…}, is up to the client. Without the header, the server answers 401 with WWW-Authenticate: Bearer realm="mcp".

Starting the server over stdio

Build with the node target, and point the client at the start script:

{
  "mcpServers": {
    "help-desk": {
      "command": "/opt/help-desk/dist/node/help-desk",
      "args": ["--stdio"],
      "env": { "DESK_API_URL": "https://api.desk.example.com" }
    }
  }
}

A client that sends initialize and then tools/call on stdin gets two JSON lines back on stdout, and nothing else; the log goes to stderr. node dist/node/help-desk.bundle.js --stdio does the same. The start script needs the project's node_modules next to dist/, as any node build does. For a server your users install from npm, the cli target makes a program they can run with --stdio:

npx frontmcp build --target cli --js
./dist/cli/help-desk --stdio

Publishing a server to npm turns that program into a package.

Publishing a server to npm

Most stdio servers reach their users as npm packages that the client starts with npx. A FrontMCP server does too, as the cli target's JavaScript bundle. Build it:

npx frontmcp build --target cli --js

dist/cli/ then holds the program, help-desk-cli.bundle.js, which starts with #!/usr/bin/env node; your server, help-desk.bundle.js, which the program loads from its own folder; and bin-meta.json and _skills/, which the program's install command reads. Ship the whole folder. Both bundles require() @frontmcp/sdk and reflect-metadata when they run, so those two are the package's dependencies; the CLI, TypeScript and the test packages are only for building, so they go in devDependencies:

package.json
{
  "name": "help-desk-mcp",
  "version": "0.1.1",
  "description": "Search and read help desk tickets from an MCP client",
  "bin": { "help-desk": "dist/cli/help-desk-cli.bundle.js" },
  "files": ["dist/cli"],
  "scripts": {
    "build": "frontmcp build --target cli --js",
    "prepublishOnly": "npm run build"
  },
  "dependencies": {
    "@frontmcp/sdk": "1.9.4",
    "reflect-metadata": "^0.2.2"
  },
  "devDependencies": {
    "frontmcp": "1.9.4",
    "typescript": "^5.5.3"
  }
}
  • private: a project from frontmcp create has "private": true, and npm publish refuses it with EPRIVATE until you remove it.
  • bin: npx -y help-desk-mcp runs the package's only command, whatever its name. Name it after name in frontmcp.config anyway: the program's install -p claude writes an entry that runs help-desk serve --stdio, which needs a help-desk command on the PATH.
  • files: a package without help-desk.bundle.js fails at start with Cannot find module '…/help-desk.bundle.js', and one without bin-meta.json can't run install -p.
  • version: without version in frontmcp.config, the build and --version take the package's.

Try the package before you publish it, from its tarball:

npm pack
npx -y ./help-desk-mcp-0.1.1.tgz --version     # 0.1.1
npm publish

The client's entry starts it with npx, and gives it its settings in env, which the tools read as process.env.DESK_API_URL:

{
  "mcpServers": {
    "help-desk": {
      "command": "npx",
      "args": ["-y", "help-desk-mcp", "--stdio"],
      "env": { "DESK_API_URL": "https://api.desk.example.com" }
    }
  }
}

Keep --stdio in args: without it the program prints its help and exits. The first start downloads the package and its dependencies, 127 packages for this server, and later starts use npx's cache. frontmcp eject-mcp-config prints this entry when clients.<client> is { transport: "stdio", command: "npx", args: ["-y", "help-desk-mcp", "--stdio"], env }; its default, npx -y <name>, uses frontmcp.config's name, which isn't the package's here, and has no --stdio.

People who install the package for good, with npm install -g help-desk-mcp, get a help-desk command, and help-desk install -p claude writes a Claude Code plugin for it, with the server's skills: see Installing into Claude Code and Codex.

Every step here was run with FrontMCP 1.9.4 against a local npm registry (Verdaccio), not the public one: npx -y help-desk-mcp --stdio answered initialize, tools/list and tools/call on stdout, with DESK_API_URL from env reaching the tool, and its log on stderr.

Trying a server by hand

An MCP 2026-07-28 call is one POST, so curl can check a URL before you give it to a client:

curl -s https://desk.example.com/mcp \
  -H 'content-type: application/json' \
  -H 'mcp-protocol-version: 2026-07-28' \
  -H 'mcp-method: tools/call' \
  -H 'mcp-name: get_ticket' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_ticket","arguments":{"id":"T-1"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}'
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"id\":\"T-1\",\"title\":\"Cannot log in\",\"status\":\"open\"}"}],"structuredContent":{"id":"T-1","title":"Cannot log in","status":"open"},"resultType":"complete","_meta":{"io.modelcontextprotocol/serverInfo":{"name":"help-desk","version":"1.0.0"}}}}

/healthz answers even where the MCP endpoint needs a token, so check it first to tell a network problem from an auth one.

Printing a client's snippet

With the clients entry frontmcp create wrote, { transport: "http", url: "http://127.0.0.1:3000/mcp" }:

npx frontmcp eject-mcp-config <client>
{
  "mcpServers": {
    "help-desk": {
      "url": "http://127.0.0.1:3000/mcp",
      "transport": "http"
    }
  }
}

Installing from an MCP Bundle

npx frontmcp build --target mcpb
npx frontmcp mcpb validate dist/mcpb/help-desk-0.1.0.mcpb
[build:mcpb] extracted: 5 tools, 0 resources, 0 prompts
[build:mcpb] server bundle (runtime inlined): 11.0 MB
[build:mcpb] wrote manifest.json
[build:mcpb] dist/mcpb/help-desk-0.1.0.mcpb (2.1 MB)
[mcpb:validate] archive is valid

Without version in frontmcp.config, the archive takes the one in package.json. An archive whose server/index.js needs packages it doesn't contain fails the check, with a line per package, and the command exits with 1:

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

--sea adds a single-executable binary, which hosts of an operating system run only when the archive has every architecture of it, added with --merge-from: see the mcpb target. The name, icon, links and settings a host shows come from the mcpb deployment in frontmcp.config, or from package.json: see Bundle metadata and user settings, which also covers --icon and --stage-only.


Troubleshooting

The client gets 404 at /mcp

The server's endpoint is /: it was started without the frontmcp command, which is what passes transport.http.path on, or it's a node build of 1.8.7 or earlier started with node dist/node/<name>.bundle.js. Set http.entryPath in @FrontMcp, or connect to the root. See the endpoint path.

A stdio client says it can't parse the server's output

The command starts the HTTP server instead of stdio, and its log reaches stdout: typically node dist/node/<name>.bundle.js --stdio with a bundle built by FrontMCP 1.8.7 or earlier. Build again, use the start script with --stdio, or set FRONTMCP_STDIO=1. Also check that none of your code prints to stdout before the server starts.

A server started with npx exits at once

The client's args have no --stdio, so the program prints its help and exits instead of serving. Use ["-y", "<package>", "--stdio"]. An entry from eject-mcp-config without command and args is npx -y <name> with the config's name, which may be another package on npm, and has no --stdio: give the client command and args in frontmcp.config (Publishing a server to npm).

Cannot find module '…/help-desk.bundle.js'

The published program can't find your server, which it loads from its own folder: the package has help-desk-cli.bundle.js without help-desk.bundle.js. Publish the whole of dist/cli/, with "files": ["dist/cli"]. Cannot find module 'reflect-metadata' means the package's dependencies leave out reflect-metadata and @frontmcp/sdk.

Error: unsupported flag '--port' on the server runner.

The node build's start script only takes --stdio, --help, --version and --print-manifest, and exits with code 2 on any other flag. Set the port with PORT. The cli target's serve command does take --port.

frontmcp.config has no clients.<client> entry

eject-mcp-config only prints what frontmcp.config describes. Add the client under clients, with its transport.

Cannot find module 'reflect-metadata' from an MCP Bundle

The archive was built by FrontMCP 1.8.6 or earlier, and doesn't contain FrontMCP's packages. Rebuild with 1.9.3. See MCP Bundles.

Header mismatch: the mcp-protocol-version header is required

The request has a 2026-07-28 body without the headers that go with it; clients built for 2026-07-28 send them. See request headers.

401 Unauthorized from a client that can't sign in

The server uses an auth mode that needs OAuth, and the client doesn't support it, or has no key for static auth. Give the client a key in its headers, or use a client that supports MCP's OAuth. See Auth modes.