Your existing config, unchanged
/config/mcp.json is exactly Claude Code's mcpServers format. Copy entries over 1:1 — stdio commands, environment variables and native http/sse upstreams all work.
Configuration reference
One container for all your MCP servers, speaking both MCP revisions on every endpoint — 2026-07-28 and 2025-11-25, the client picks. Serve many stdio servers over one HTTPS endpoint for ChatGPT, Claude, Le Chat, Cursor and any other MCP client, with the Claude Code config format you already have, path-based routing, a context-friendly aggregate endpoint, and OAuth 2.1 in both directions.
/config/mcp.json is exactly Claude Code's mcpServers format. Copy entries over 1:1 — stdio commands, environment variables and native http/sse upstreams all work.
Configuration reference
Every server gets its own Streamable HTTP endpoint at /«name»/mcp. Register the servers you use most as individual connectors, with no extra container per server.
Endpoint reference
A single connector that exposes every server through six meta-tools — so your model's context holds 6 tool schemas instead of N×tools. Per-server allowTools and denyTools narrow that further — a filtered tool is hidden and refused, not merely hidden.
Meta-tool reference
Every endpoint answers MCP 2026-07-28 and 2025-11-25 alike — the client picks, and cannot tell from the answers. On the 2026 revision a child server's question reaches the person at the far end: attributed to the server that asked, stripped of anything that could lie about it, and sealed so it cannot be resumed on another call.
How elicitation travels
A child's tool list moves, a watched resource changes — and the client hears about it. The hub serves subscriptions/listen to its clients and subscribes to its children on whichever revision they speak, so a server that has never heard of it still reaches a client that speaks nothing else. The state is the open response, not a session table.
What a client can watch
The same four ways in and out — operator-issued credentials, RFC 7591 registration with RFC 7592 management, and Client ID Metadata Documents — for the clients that connect to the hub and for the upstreams the hub connects to. PKCE, private_key_jwt, resource-bound tokens, rotating refresh tokens.
Every standard, named
Children are spawned at boot, pinged every 60 s and restarted with exponential backoff. A dead server answers 503 on its path — never silence.
How it works
One Node process, no database — state is one JSON file plus a JWT key on a volume — and a stateless transport that cannot leak processes. Multi-arch images (amd64/arm64) run comfortably on a single-board computer.
Architecture

A client no longer has to register to use this hub. It publishes a Client ID Metadata Document at a stable HTTPS URL and uses that URL as its client_id; the hub fetches the document, reads the client's name and redirect URIs out of it, and shows you the origin that vouched before you approve anything. Nothing is stored, so a client that is reinstalled or moves to another machine is still the same client — and the approval you gave it still holds.
That URL is chosen by an unauthenticated caller, so the fetch is treated as hostile: https only, no redirects, no private addresses, the checked address pinned into the connection, a hard size cap and a short timeout.
Confidential clients authenticate at the token endpoint with private_key_jwt against the keys in their own document. Everything older keeps working: RFC 7591 dynamic registration stays advertised beside it, mcp-hub-admin clients add issues credentials by hand for a client that can do neither, and one setting turns a mechanism off once you no longer need it.
The interesting half is that it works in the other direction too. A remote MCP server that requires OAuth is not a problem to be bridged around — the hub is an OAuth client in its own right:
| A client connecting to the hub | The hub connecting to an upstream | |
|---|---|---|
| Operator-issued credentials | mcp-hub-admin clients add | oauth.mode: "static" |
| Dynamic registration (RFC 7591) | POST /register | oauth.mode: "dcr" |
| Registration management (RFC 7592) | GET/PUT/DELETE /register/<id> | deleted again on upstream logout |
| Client ID metadata document | the client's URL as its client_id | the hub's own document, one per upstream |
| Plain bearer token | an API token | a static Authorization header |
Machine-to-machine upstreams need no attention at all — the hub fetches a token and renews it. Where a person has to sign in, one command prints a URL, and the server comes up by itself once you have.
mcp-hub-admin upstream login some-saasEvery standard, named and linked →
The same trick, applied to the protocol itself. Every endpoint answers MCP 2026-07-28 and 2025-11-25 alike, and the client cannot tell from the answers which one it got. Where the two revisions genuinely differ, the hub absorbs the difference rather than passing it on:
| Towards a client | Towards a child server | |
|---|---|---|
| Elicitation — a server asking a person something | returned as a result on 2026-07-28, so it reaches the person instead of dying at the gateway | forwarded to the child that asked, with the answer carried back |
| Subscriptions — a server announcing that something changed | served as a subscriptions/listen stream | subscriptions/listen to a 2026 child, resources/subscribe to a 2025 one |
That second row is the useful one in practice: most MCP servers in the wild are still on the older revision, and their notifications now reach clients that only speak the newer one.
How elicitation travels → · What a client can watch →
mkdir -p data && sudo chown -R 1000:1000 data # the container runs as uid 1000
docker run -d --name mcp-hub \
-p 127.0.0.1:7690:80 \
-e EXTERNAL_URL="https://mcp.example.net" \
-e PASSWORD_HASH="$(htpasswd -bnBC 10 '' 'yourpassword' | tr -d ':\n')" \
-e TRUSTED_PROXIES="192.168.1.0/24" \
-v "$PWD/config:/config:ro" \
-v "$PWD/data:/data" \
ghcr.io/ni-c/mcp-hub:0.10.0Point a reverse proxy with a TLS certificate at 127.0.0.1:7690, then add https://mcp.example.net/hub as a custom connector in your MCP client — ChatGPT, Claude, Le Chat, Cursor and friends — and log in once with the password.
| If you want to… | Read |
|---|---|
| understand the problem it solves | What is mcp-hub? |
| get a working deployment | Getting started · Deployment |
write your mcp.json | Configuration |
| connect ChatGPT, Claude, Cursor, an API… | Connecting clients · Client compatibility |
know how clients get a client_id | Client registration |
| reach an upstream that needs OAuth | Upstreams that speak OAuth |
| check which standard is implemented | Standards |
| watch a server for changes | Subscriptions |
| run servers only when they are used | On-demand servers |
| isolate a server you do not trust | Sandboxing |
| run it safely on the public internet | Security |
| manage clients, tokens and upstreams | Admin CLI |
| know what happens inside | Architecture |
| fix something that is misbehaving | FAQ & troubleshooting |