PM2 for MCP servers: one config file, one command, one log stream.
mcp-hub is a single Rust binary that runs your local (stdio) MCP servers as supervised child processes, restarts them when they crash, shows their health and logs in one place, and generates the client config for Claude Code, Cursor, Codex CLI and Gemini CLI from the same server list.
cargo binstall mcp-server-hub # installs the `mcp-hub` binary
mcp-hub init # add a server to ./mcp-hub.toml
mcp-hub start --daemon # run everything in the background
mcp-hub status # state, health, PID, uptime, restarts
mcp-hub logs --follow # all servers' logs, interleaved
mcp-hub gen-config --format claude # paste into .mcp.json- Supervises processes. Reads server definitions (
command,args,env) from a TOML file, and starts, stops and restarts them. Each server runs in its own process group, so stopping it also kills the child processes it spawned. - Restarts on crash. Exponential backoff (1s, 2s, 4s, up to 60s) with a retry limit (
max_retries); a server that keeps failing is markedfatal. - Checks health. Sends MCP
pingrequests at a configurable interval and reports each server as Healthy, Degraded or Failed. - Introspects servers. Lists each server's tools, resources and prompts over the MCP protocol.
- Streams logs.
mcp-hub logs --followinterleaves the stderr of all servers (or one, with--server). - Serves a local web UI. Status, a tools browser and live logs at
http://127.0.0.1:3456, plus a/healthJSON endpoint that returns HTTP 503 when a server has failed. - Generates client config.
mcp-hub gen-config --format claude|cursor|codex|geminiprints the config document on stdout (notes go to stderr).${VAR}and${VAR:-default}are expanded inenvandargs, andenv_fileis supported. - One daemon per project. Each config file gets its own daemon, so several projects can run side by side (see Several projects).
- Runs as a daemon.
start --daemonwaits until the servers and sockets are up and reports startup errors; its output goes todaemon-<key>.login the state directory (or--log-file).mcp-hub reloadapplies config changes and reports how many servers were added, removed and changed.
- It is not an MCP proxy or gateway. It does not sit between your client and your servers and does not forward MCP traffic.
gen-configwrites each server's owncommand,argsandenvinto the client config, so the client starts its own copy of the server. mcp-hub's copy is separate: it is the one that is supervised, health-checked, logged and shown in the web UI. If you want a single endpoint that aggregates many servers, use an MCP proxy or gateway instead. - It does not install MCP servers.
commandis whatever you would type in a terminal (npx ...,uvx ..., a local binary). - It does not manage remote machines. Everything runs on the local host, and the control socket is local.
- The web UI has no authentication. It only binds to
127.0.0.1; do not expose that port to other machines. - Linux and macOS only (Unix-like systems); Windows is not supported (#30).
- No Homebrew formula yet (#7); use
cargo binstallorcargo install.
The crate on crates.io is mcp-server-hub (the name mcp-hub belongs to an unrelated project). The repository and the installed binary are both mcp-hub. Install with mcp-server-hub, run mcp-hub.
Prebuilt binary (fastest) — cargo binstall:
cargo binstall mcp-server-hub # downloads a prebuilt mcp-hub binary, no compileFrom source — cargo install:
cargo install mcp-server-hub # compiles and installs the mcp-hub binaryDirect download:
Grab mcp-server-hub-<target>-v<version>.tgz (Linux or macOS) from
GitHub Releases, extract the
mcp-hub binary, and put it on your PATH.
Prebuilt targets built by the release workflow (v0.1.1 shipped Linux only; macOS archives start with the next release):
| Platform | Target triple | Asset |
|---|---|---|
| Linux x86_64 (glibc) | x86_64-unknown-linux-gnu |
mcp-server-hub-x86_64-unknown-linux-gnu-v<version>.tgz |
| Linux aarch64 (glibc) | aarch64-unknown-linux-gnu |
mcp-server-hub-aarch64-unknown-linux-gnu-v<version>.tgz |
| Linux x86_64 (musl) | x86_64-unknown-linux-musl |
mcp-server-hub-x86_64-unknown-linux-musl-v<version>.tgz |
| Linux aarch64 (musl) | aarch64-unknown-linux-musl |
mcp-server-hub-aarch64-unknown-linux-musl-v<version>.tgz |
| macOS Apple Silicon (aarch64) | aarch64-apple-darwin |
mcp-server-hub-aarch64-apple-darwin-v<version>.tgz |
| macOS Intel (x86_64) | x86_64-apple-darwin |
mcp-server-hub-x86_64-apple-darwin-v<version>.tgz |
Windows is not supported (#30). mcp-hub targets Unix-like systems only: it relies on Unix sockets, process groups and signals.
After installing, verify with:
mcp-hub --versionCreate a mcp-hub.toml in your project directory:
[hub]
web_port = 3456
[servers.filesystem]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
[servers.github]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]
env = { GITHUB_PERSONAL_ACCESS_TOKEN = "${GITHUB_TOKEN}" }Then start all servers:
mcp-hub start| Command | Description |
|---|---|
mcp-hub start |
Start all servers in foreground mode |
mcp-hub start --daemon |
Start all servers as a background daemon |
mcp-hub stop |
Stop the running daemon |
mcp-hub restart <name> |
Restart a specific server |
mcp-hub status |
Show status of all servers as a table (daemon mode); --json for raw JSON |
mcp-hub logs |
Show recent logs from all servers |
mcp-hub logs --server <name> |
Show logs for a specific server |
mcp-hub logs --follow |
Stream live logs from all servers (recent lines first; Ctrl+C to stop) |
mcp-hub logs --follow --server <name> |
Stream live logs for one server |
mcp-hub reload |
Reload config and apply changes without restart |
-c <config> (any command) |
Target the daemon of that config file, from any directory |
mcp-hub init |
Interactive wizard to add a new server |
mcp-hub gen-config --format claude |
Generate Claude Code mcpServers JSON block |
mcp-hub gen-config --format cursor |
Generate Cursor MCP config snippet |
mcp-hub gen-config --format codex |
Generate Codex CLI [mcp_servers.<name>] TOML |
mcp-hub gen-config --format gemini |
Generate Gemini CLI mcpServers JSON block |
Codex CLI / Gemini CLI. --format codex prints TOML to append to ~/.codex/config.toml (or .codex/config.toml in a trusted project); --format gemini prints JSON to merge into ~/.gemini/settings.json (user) or .gemini/settings.json (project). As with the other formats, stdout holds only the config document and the destination hint and warnings go to stderr, so mcp-hub gen-config -f codex >> ~/.codex/config.toml works. cwd is emitted for these two formats when a server sets one. transport = "http" servers are skipped with a warning.
Full server block reference:
[hub]
web_port = 3456 # Web UI port (default: 3456)
[servers.my-server]
command = "npx" # Executable to run (required, must not be empty)
args = ["-y", "@scope/server"] # Arguments passed to the executable (default: [])
env = { LOG_LEVEL = "info" } # Inline environment variables; ${VAR} is expanded (default: none)
env_file = ".env" # Path to a KEY=VALUE file (# comments ok); its values override `env` (default: none)
transport = "stdio" # "stdio" or "http" (default: "stdio")
cwd = "/path/to/project" # Working directory for the process (default: inherited)
health_check_interval = 30 # Seconds between MCP pings (default: 30)
max_retries = 10 # Consecutive crashes before marking Fatal (default: 10)
restart_delay = 1 # Base restart backoff delay in seconds (default: 1)transport = "http" servers are spawned and supervised like any other (restart with backoff, process-group kill, stdout and stderr in the logs; stdin is /dev/null), but health checks and MCP introspection only work over stdio, so their health shows not checked and counts as OK for GET /health. An optional url that would enable HTTP health checks is a possible follow-up (see #59).
env values and args may reference the environment mcp-hub itself runs in, so secrets can stay out of the config file:
${NAME}is replaced by the value ofNAME. IfNAMEis not set, loading the config fails with an error naming the server, the field and the variable.${NAME:-default}usesdefault(which may be empty) whenNAMEis unset or empty.$${is an escape and produces a literal${.- A lone
$or$NAMEwithout braces is left as written.NAMEmust match[A-Za-z_][A-Za-z0-9_]*, and an unterminated${is an error.
command, cwd and env_file are not expanded, and values read from env_file are always literal; env_file remains the alternative for secrets you would rather keep in a separate file. mcp-hub gen-config writes the expanded values (that is what ends up in .mcp.json), and prints a warning on stderr when one of them came from a variable whose name contains TOKEN, SECRET, KEY or PASSWORD.
mcp-hub runs one daemon per config file. A daemon is identified by the canonical path of its config (-c <path>, else ./mcp-hub.toml, else ~/.config/mcp-hub/mcp-hub.toml), so mcp-hub start --daemon in two project directories starts two independent daemons.
- Every command targets one project.
stop,restart,status,logs,reloadandgen-config --liveresolve the config exactly likestartdoes, then talk to that project's daemon. Use-c path/to/mcp-hub.tomlto target a project from anywhere. - Without a config in sight (for example
mcp-hub stopin a directory withoutmcp-hub.toml): if exactly one daemon is running it is used (its config is printed on stderr); if several are running, the error lists their configs and asks for-c. - Give each project its own
web_port. Each daemon binds its[hub] web_port(default 3456); a second project with the same port fails to start and the error names the config to change. mcp-hub statusstarts withDaemon for <config> (pid N, web UI http://127.0.0.1:PORT).--jsonis unchanged.
State lives in the state directory (~/.config/mcp-hub/ on Linux, ~/Library/Application Support/mcp-hub/ on macOS; override with MCP_HUB_STATE_DIR), as mcp-hub-<key>.sock, .pid, .json (config path, pid, web port) and daemon-<key>.log, where <key> is 12 hex characters derived from the config path.
If you upgrade while an older (single-socket) daemon is still running, commands keep talking to it and print a note; start --daemon refuses until you mcp-hub stop it.
The web UI is served at http://127.0.0.1:3456 by default (configurable via [hub] web_port). It listens on the loopback interface only and has no authentication. It shows:
- Live server status cards with health state
- MCP tool browser with per-server tool listings
- Real-time log viewer with SSE streaming and per-server filtering
Requests whose Host header is not localhost, 127.0.0.1 or [::1] (optionally with the web port) get a 403, which blocks DNS-rebinding attacks from web pages. If the browser falls behind the log stream, a [mcp-hub] N log lines dropped line marks the gap.
It also serves GET /health, a JSON summary that returns HTTP 503 when a server has failed.
MIT -- see LICENSE