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 & path | Purpose |
|---|---|
GET /api/status | Router status (version, auth mode, running/total) |
GET/POST /api/registries | List / add registries |
DELETE /api/registries/:name | Remove a registry |
GET /api/registries/:name/servers | Search that registry |
GET /api/servers | All installed servers with runtime state |
POST /api/servers | Install (registry entry, npm package, or remote URL) |
PATCH /api/servers/:name | Update env / enabled / transport |
DELETE /api/servers/:name | Stop, delete config, remove install dir |
POST /api/servers/:name/restart | Kill and respawn (after env edits) |
GET /api/servers/:name/tools | Connect and list downstream tools |
GET /api/workspaces | All workspaces with their endpoint paths |
POST /api/workspaces | Create a workspace (slug auto-derived from the name) |
PATCH /api/workspaces/:slug | Update members / overrides / enabled (renaming re-slugs) |
DELETE /api/workspaces/:slug | Remove a workspace (servers untouched) |
POST /api/reload | Re-read config and reconcile processes |
MCP endpoints
| Endpoint | Purpose |
|---|---|
POST/GET/DELETE /mcp/<name> | Proxy 1:1 to that server |
POST/GET/DELETE /mcp | Aggregate 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 toDATA_DIRcan 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.