Documentation

A condensed reference. For the complete guide โ€” Docker details, remote-server proxying, and the extended API โ€” see the README on GitHub.

Configuration files

All config lives as flat, hand-editable JSON under DATA_DIR/config (./data/config locally, /data/config in Docker). Files are written with mode 0600; unknown keys are preserved across round-trips. After hand-editing, wait for the file watcher or apply the change explicitly:

curl -X POST -H "Authorization: Bearer $MCP_ROUTER_TOKEN" \
  http://localhost:3000/api/reload

settings.json

{
  "port": 3000,                 // PORT env wins over this
  "authToken": "a-long-secret", // MCP_ROUTER_TOKEN env wins
  "authEnabled": true,          // false = unauthenticated (trusted nets only)
  "allowedOrigins": [],         // extra browser Origins allowed to reach /mcp
  "idleTimeoutMs": 300000,      // default stdio idle shutdown (ms)
  "sessionIdleTimeoutMs": 1800000, // reclaim an idle MCP session after this (ms)
  "maxSessions": 1000           // cap on live sessions; LRU-evicted past it
}

The optional host key (or HOST env) picks the bind interface. Leave it unset to bind all interfaces โ€” that's what Docker and LAN exposure need, so the published port stays reachable. Set it to 127.0.0.1 only for a bare-metal, localhost-only install; inside a container that would make the mapped port refuse connections.

The /mcp endpoints run stateful sessions: initialize mints an Mcp-Session-Id reused across the session's requests and its GET SSE stream. Idle sessions are reclaimed after sessionIdleTimeoutMs, and a browser Origin guard (loopback + allowedOrigins) fronts /mcp to block DNS-rebinding โ€” it applies even when auth is disabled.

registries.json

{
  "registries": [
    { "name": "official", "url": "https://registry.modelcontextprotocol.io" }
  ]
}

Any MCP-registry-API-compatible service (GET {url}/v0/servers) works. The official registry is seeded on first run.

servers/<name>.json

One file per installed server. The name doubles as the route segment (/mcp/<name>) and the install directory (data/servers/<name>). Names must match ^[a-z0-9][a-z0-9._-]*$ (max 64 chars). A local stdio server installed from a registry:

{
  "name": "github",
  "enabled": true,
  "source": {
    "type": "registry",
    "registry": "official",
    "serverName": "io.github.github/github-mcp-server"
  },
  "transport": {
    "type": "stdio",
    "command": "node",
    "args": ["/data/servers/github/node_modules/.bin/github-mcp-server"]
  },
  "env": { "GITHUB_TOKEN": "ghp_..." }
}

Use "source": { "type": "npm", "package": "@scope/pkg" } to install straight from npm, or "source": { "type": "remote" } with a streamable-http transport to proxy a remote server without installing anything.

workspaces/<slug>.json

A workspace is a custom aggregate: it exposes a chosen subset of servers at its own /mcp/w/<slug> endpoint. The slug is derived from the name (and doubles as the filename); each member may override the base server's env, args, or headers for this workspace only. Each member runs as an isolated downstream instance, so a workspace can run a server that's globally disabled โ€” and its overrides never touch the base server.

{
  "name": "Coding assistant",
  "slug": "coding-assistant",
  "enabled": true,               // false = the /mcp/w/<slug> endpoint 404s
  "description": "GitHub + local files for a code client.",
  "members": {
    "github": { "enabled": true },
    "filesystem": {
      "enabled": true,
      "args": ["/data/servers/filesystem", "/srv/repo"],  // replaces the base args
      "env": { "READ_ONLY": "1" }                          // merged over base env
    }
  }
}

The effective enabled state of a member is workspace.enabled && member.enabled โ€” the base server's own enabled flag is ignored inside a workspace. Members referencing a server that doesn't exist are skipped.

REST API

Management API under /api, bearer auth, JSON in/out. Errors are non-2xx with { "error": "...", "detail": "..." }.

Method & pathPurpose
GET /api/statusRouter status (version, auth mode, running/total)
GET/POST /api/registriesList / add registries
DELETE /api/registries/:nameRemove a registry
GET /api/registries/:name/serversSearch that registry
GET /api/serversAll installed servers with runtime state
POST /api/serversInstall (registry entry, npm package, or remote URL)
PATCH /api/servers/:nameUpdate env / enabled / transport
DELETE /api/servers/:nameStop, delete config, remove install dir
POST /api/servers/:name/restartKill and respawn (after env edits)
GET /api/servers/:name/toolsConnect and list downstream tools
GET /api/workspacesAll workspaces with their endpoint paths
POST /api/workspacesCreate a workspace (slug auto-derived from the name)
PATCH /api/workspaces/:slugUpdate members / overrides / enabled (renaming re-slugs)
DELETE /api/workspaces/:slugRemove a workspace (servers untouched)
POST /api/reloadRe-read config and reconcile processes

MCP endpoints

EndpointPurpose
POST/GET/DELETE /mcp/<name>Proxy 1:1 to that server
POST/GET/DELETE /mcpAggregate of all enabled servers, <server>__ prefix
POST/GET/DELETE /mcp/w/<slug>A workspace's custom aggregate โ€” same <server>__ prefix

Both speak streamable HTTP and require the bearer token (unless authEnabled is false). Disabled servers return 404; auth failures return 401 before any MCP handling.

Security notes

  • Secrets are plaintext in the JSON config (env values, remote headers, the auth token). Files are mode 0600, but anyone with read access to DATA_DIR can read them. Protect the directory.
  • One bearer token guards everything. Treat it like a password. Set it via MCP_ROUTER_TOKEN.
  • Don't expose the router directly to the internet โ€” it speaks plain HTTP. Put it behind a TLS-terminating reverse proxy, or bind to localhost / a private network.
  • Installed servers run as child processes with the env you configure. Installing a server means running its code with those secrets โ€” install packages you trust.

Read the full README โ†—