Serve multiple stdio MCP servers from one container: path routing, hub meta-tools, OAuth 2.1 + API tokens for ChatGPT, Claude, Cursor and other MCP clients
- ✓Open-source license (MIT)
- ✓Actively maintained (<30d)
- ✓Clear description
- ✓Topics declared
- ✓Documented (README)
claude mcp add mcp-hub -- npx -y @ni-c/mcp-hub{
"mcpServers": {
"mcp-hub": {
"command": "npx",
"args": ["-y", "@ni-c/mcp-hub"],
"env": {
"EXTERNAL_URL": "<external_url>"
}
}
}
}EXTERNAL_URLMCP Servers overview
# mcp-hub
[](https://github.com/ni-c/mcp-hub/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@ni-c/mcp-hub)
[](https://www.npmjs.com/package/@ni-c/mcp-hub)
[](https://github.com/ni-c/mcp-hub/pkgs/container/mcp-hub)
[](LICENSE)
[](https://mcp-hub.ni-c.de)
📖 **Full documentation: <https://mcp-hub.ni-c.de>**
Serve many stdio MCP servers from **one container**, published over HTTPS for
ChatGPT connectors, Claude (Web and Code), Mistral Le Chat, Cursor, LibreChat
and any other Streamable-HTTP MCP client — with a built-in OAuth 2.1 login
protected by a single password, plus long-lived API tokens for clients that
cannot do OAuth (OpenAI Responses API, xAI API, Gemini API). Per-client
recipes: [client compatibility](https://mcp-hub.ni-c.de/guide/client-compatibility).
<!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub
picks the variant that matches its own theme toggle. npm strips <picture> and
<source> when it sanitises the README and keeps the <img>, which is why that
fallback brings its own dark card instead of relying on a media query. -->
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://mcp-hub.ni-c.de/architecture-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="https://mcp-hub.ni-c.de/architecture-light.svg">
<img src="https://mcp-hub.ni-c.de/architecture.svg" alt="MCP clients connect through a reverse proxy to mcp-hub: one Node process with an OAuth 2.1 authorization server, one path per server plus the /hub aggregate, and a supervisor keeping the stdio children and remote upstreams alive" width="800">
</picture>

## Why
Wrapping each stdio MCP server in its own auth-proxy container costs a full
image, an OAuth stack, a hostname and a compose stack *per server*. mcp-hub
replaces N containers with one process:
- **Config is exactly Claude Code's `mcpServers` format** — copy entries 1:1.
- **Path-based routing**: `https://host/paperless`, `https://host/homeassistant`, …
- **`/hub` aggregate**: register a *single* connector and reach every server
through 4 meta-tools (`list_servers`, `list_tools`, `get_tool_schema`,
`call_tool`) without flooding the model context with N×tools schemas.
- **Supervision**: children are spawned at boot, pinged, and restarted with
exponential backoff when they die. A down server answers 503, not silence.
- **Hot reload**: edits to `mcp.json` start/stop/restart only the affected
servers.
- **Stateless Streamable HTTP**: no session state, so claude.ai's
reconnect-without-DELETE behaviour cannot leak processes or memory.
- **Lightweight by design**: one Node process, no database (state is one JSON
file plus a JWT key under `/data`), a handful of runtime dependencies, and
multi-arch images — a stated project goal is to run comfortably on a
single-board computer like a Raspberry Pi.
## Configuration
`/config/mcp.json` — identical to Claude Code (`${VAR}` expands from the
container environment; unknown fields are ignored by Claude Code, so the file
stays interchangeable). Install stdio server binaries at a reviewed, exact
version in your image; do not download mutable packages at runtime:
```json
{
"mcpServers": {
"paperless": {
"command": "paperless-mcp",
"args": [],
"env": { "PAPERLESS_API_TOKEN": "${PAPERLESS_API_TOKEN}" }
},
"homeassistant": {
"type": "http",
"url": "http://homeassistant:8123/api/mcp",
"headers": { "Authorization": "Bearer ${HA_TOKEN}" }
},
"private-thing": { "command": "some-mcp", "args": [], "hub": false },
"untrusted": {
"type": "docker",
"image": "ghcr.io/example/untrusted-mcp@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"network": "none",
"memory": "256m"
}
}
}
```
Stdio servers (`command`/`args`/`env`) are spawned as supervised child
processes. Remote servers (`type: "http"` or `"sse"` with `url` and optional
`headers`) are connected as MCP clients with the configured headers injected
on every request — the same supervision (ping, backoff reconnect, hot reload)
applies. Upstreams that require their own *interactive* OAuth cannot be
configured with static headers; bridge those with an
[`mcp-remote`](https://github.com/geelen/mcp-remote) stdio entry and persist
its token cache (`MCP_REMOTE_CONFIG_DIR`) under `/data`.
`"hub": false` hides a server from the `/hub` aggregate; its own path keeps
working. Reserved names: `mcp`, `hub`, `authorize`, `token`, `register`,
`login`, `consent`, `health`, `livez`, `revoke`.
All stdio children share the hub's Unix user and can read its mounted files.
Only install fully trusted stdio servers. A server with a different trust level
belongs in its own container — and it does not have to speak HTTP to get there:
- `type: "docker"` — the hub creates the container and talks **stdio across the
container boundary** over the Docker API. No HTTP listener, no bearer token,
no bridge process in the image. The hub itself never gets the Docker socket:
a separate `mcp-hub-docker-proxy` container holds it and allows only the
container operations `mcp.json` describes — nothing privileged, no host
mounts, no other images. Credentials can live with the proxy (`secretsFrom`)
so the hub process never holds them — and rotating one is just an edit: the
proxy watches the file and recreates the sandbox with the new values.
- `type: "unix"` / `"tcp"` — you start the container, the hub connects to a
socket. Costs the hub no privileges at all, and a Unix socket reaches a
sandbox running with `network_mode: none`.
Both carry the newline-delimited JSON-RPC the specification asks custom
transports to reuse. See [sandboxing](https://mcp-hub.ni-c.de/guide/sandboxing)
and [SECURITY.md](SECURITY.md).
For a custom image, pin every package to an exact version:
```dockerfile
FROM ghcr.io/ni-c/mcp-hub:0.6.0 # pin @sha256:<digest> in production
USER root
RUN npm install -g your-mcp-package@1.2.3
USER node
```
### Environment
| Variable | Required | Description |
|---|---|---|
| `EXTERNAL_URL` | yes | Public base URL, e.g. `https://mcp.example.net` (no path) |
| `PASSWORD_HASH` | one of | bcrypt hash of the login password (`htpasswd -bnBC 10 "" 'pw' \| tr -d ':\n'`) |
| `PASSWORD` | one of | plain-text alternative to `PASSWORD_HASH` |
| `TRUSTED_PROXIES` | no | comma-separated IPs/CIDRs allowed to set `X-Forwarded-*` (see below) |
| `RESOURCE_BOUND_TOKENS` | no | RFC 8707 tokens bound to `/hub` or one `/<name>/mcp`, default `true`; set `false` only to keep pre-0.5 unbound tokens working |
| `DEFAULT_RESOURCE` | no | server name (or `hub`) to bind tokens to when a client sends no `resource` parameter; unset → such requests are refused |
| `MCP_BODY_LIMIT` | no | authenticated MCP JSON body limit, default `1mb` |
| `MCP_REQUESTS_PER_MINUTE` | no | limit per OAuth client, default `120` |
| `MCP_MAX_CONCURRENT_REQUESTS` | no | in-flight limit per OAuth client, default `4` |
| `HTTP_HEADERS_TIMEOUT_MS` | no | Node HTTP header timeout, default `10000` |
| `HTTP_REQUEST_TIMEOUT_MS` | no | complete request timeout, default `310000` (slightly above the tool-call timeout) |
| `PORT` | no | listen port (default 80 in the image, 3000 outside) |
| `CONFIG_PATH` | no | default `/config/mcp.json` |
| `DATA_PATH` | no | default `/data` |
| `LOG_FILE` | no | additionally mirror all log output into this file, e.g. `/data/mcp-hub.log` (see below) |
`/data` holds the Ed25519 JWT key, registered OAuth clients, approvals and
refresh tokens. **Mount it as a volume** — recreating it invalidates every
connector authorization.
Every access token is bound to one resource. The OAuth client includes the
resource advertised by the endpoint's RFC 9728 document — no client-side
configuration needed — and the resulting token is valid only there: a token for
`/paperless/mcp` cannot call `/hub`, `/health` or another server. The shorter
`/<name>` route is canonicalized to `/<name>/mcp`.
`RESOURCE_BOUND_TOKENS=false` turns this off and is a migration mode for
deployments from 0.4 and earlier, where tokens were issued without a resource
and reach every path. The hub logs a warning while it is set. Removing it
invalidates those unbound tokens, so every connector authorizes once more.
`TRUSTED_PROXIES` decides what `req.ip` is, and therefore what the login rate
limiter counts. List **only** your own reverse proxy, and make sure it
*overwrites* `X-Forwarded-For` rather than appending to it — otherwise a
client can supply its own address and rotate it to sidestep the per-IP limit.
If the variable is unset, every request appears to come from the proxy and
per-IP limiting degrades to a single global counter (the hub logs a warning
at startup). A global cap of 100 failures per 15 minutes applies either way.
## Running
### Option A — prebuilt image from GHCR (recommended)
Published on every push to `main` and every `vX.Y.Z` release tag, for
`linux/amd64` and `linux/arm64`. Browse the versions on the
[package page](https://github.com/ni-c/mcp-hub/pkgs/container/mcp-hub).
```sh
docker pull ghcr.io/ni-c/mcp-hub:0.6.0
```
Tags: `latest` (tip of `main`), `X.Y.Z` and `X.Y` (releases), and
`sha-<commit>` for a specific build.
Use a version tag instead of `latest` for controlled updates. For an immutable
deployment, record the resolved digest from `docker image inspect` and use
`ghcr.io/ni-c/mcp-hub:<version>@What people ask about mcp-hub
What is ni-c/mcp-hub?
+
ni-c/mcp-hub is mcp servers for the Claude AI ecosystem. Serve multiple stdio MCP servers from one container: path routing, hub meta-tools, OAuth 2.1 + API tokens for ChatGPT, Claude, Cursor and other MCP clients It has 0 GitHub stars and its last recorded update is dated 2026-08-19.
How do I install mcp-hub?
+
You can install mcp-hub by cloning the repository (https://github.com/ni-c/mcp-hub) or following the README instructions on GitHub. ClaudeWave also provides quick install blocks on this page.
Is ni-c/mcp-hub safe to use?
+
Our security agent has analyzed ni-c/mcp-hub and assigned a Trust Score of 95/100 (tier: Verified). See the full breakdown of passed checks and flags on this page.
Who maintains ni-c/mcp-hub?
+
ni-c/mcp-hub is maintained by ni-c. The last recorded GitHub activity is dated 2026-08-19, with 3 open issues.
Are there alternatives to mcp-hub?
+
Yes. On ClaudeWave you can browse similar mcp servers at /categories/mcp, sorted by popularity or recent activity.
Deploy mcp-hub to your cloud
Ship this repo to production in minutes. Each platform spins up its own environment with editable env vars.
Maintain this repo? Add a badge to your README
Drop the badge into your GitHub README to show it's tracked on ClaudeWave. Each badge links back to this page and reflects the live Trust Score.
More MCP Servers
Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.
User-friendly AI Interface (Supports Ollama, OpenAI API, ...)
An open-source AI agent that brings the power of Gemini directly into your terminal.
Real-time global intelligence dashboard. AI-powered news aggregation, geopolitical monitoring, and infrastructure tracking in a unified situational awareness interface
The fastest path to AI-powered full stack observability, even for lean teams.
🕷️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!