@ni-c/mcp-hub 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +619 -0
  2. package/README.md +189 -64
  3. package/dist/admin.js +232 -8
  4. package/dist/admin.js.map +1 -1
  5. package/dist/auth/address.js +89 -0
  6. package/dist/auth/address.js.map +1 -0
  7. package/dist/auth/api-tokens.js +27 -0
  8. package/dist/auth/api-tokens.js.map +1 -0
  9. package/dist/auth/cimd.js +367 -0
  10. package/dist/auth/cimd.js.map +1 -0
  11. package/dist/auth/consent-page.js +4 -7
  12. package/dist/auth/consent-page.js.map +1 -1
  13. package/dist/auth/headers.js +22 -1
  14. package/dist/auth/headers.js.map +1 -1
  15. package/dist/auth/login-page.js +4 -7
  16. package/dist/auth/login-page.js.map +1 -1
  17. package/dist/auth/oidc/adapter.js +135 -0
  18. package/dist/auth/oidc/adapter.js.map +1 -0
  19. package/dist/auth/oidc/interactions.js +187 -0
  20. package/dist/auth/oidc/interactions.js.map +1 -0
  21. package/dist/auth/oidc/mount.js +144 -0
  22. package/dist/auth/oidc/mount.js.map +1 -0
  23. package/dist/auth/oidc/provider.js +440 -0
  24. package/dist/auth/oidc/provider.js.map +1 -0
  25. package/dist/auth/oidc/quirks.js +234 -0
  26. package/dist/auth/oidc/quirks.js.map +1 -0
  27. package/dist/auth/oidc/verifier.js +121 -0
  28. package/dist/auth/oidc/verifier.js.map +1 -0
  29. package/dist/auth/page.js +22 -0
  30. package/dist/auth/page.js.map +1 -1
  31. package/dist/auth/pinned-fetch.js +129 -0
  32. package/dist/auth/pinned-fetch.js.map +1 -0
  33. package/dist/auth/protected-resource.js +41 -0
  34. package/dist/auth/protected-resource.js.map +1 -0
  35. package/dist/auth/rate-limit.js +156 -0
  36. package/dist/auth/rate-limit.js.map +1 -0
  37. package/dist/auth/redirect-uri.js +90 -0
  38. package/dist/auth/redirect-uri.js.map +1 -0
  39. package/dist/auth/registration.js +146 -0
  40. package/dist/auth/registration.js.map +1 -0
  41. package/dist/auth/session.js +43 -0
  42. package/dist/auth/session.js.map +1 -0
  43. package/dist/auth/signed-token.js +49 -0
  44. package/dist/auth/signed-token.js.map +1 -0
  45. package/dist/auth/store.js +516 -5
  46. package/dist/auth/store.js.map +1 -1
  47. package/dist/auth/text.js +51 -0
  48. package/dist/auth/text.js.map +1 -0
  49. package/dist/config.js +184 -5
  50. package/dist/config.js.map +1 -1
  51. package/dist/docker-proxy/policy.js +1 -0
  52. package/dist/docker-proxy/policy.js.map +1 -1
  53. package/dist/docker-proxy/server.js +1 -0
  54. package/dist/docker-proxy/server.js.map +1 -1
  55. package/dist/elicitation.js +0 -0
  56. package/dist/elicitation.js.map +1 -0
  57. package/dist/forward.js +0 -0
  58. package/dist/forward.js.map +1 -0
  59. package/dist/health.js +22 -3
  60. package/dist/health.js.map +1 -1
  61. package/dist/hub.js +337 -29
  62. package/dist/hub.js.map +1 -1
  63. package/dist/index.js +218 -23
  64. package/dist/index.js.map +1 -1
  65. package/dist/limits.js +13 -1
  66. package/dist/limits.js.map +1 -1
  67. package/dist/mcp-limits.js +14 -2
  68. package/dist/mcp-limits.js.map +1 -1
  69. package/dist/proxy.js +262 -34
  70. package/dist/proxy.js.map +1 -1
  71. package/dist/stdio.js +97 -7
  72. package/dist/stdio.js.map +1 -1
  73. package/dist/subscriptions.js +236 -0
  74. package/dist/subscriptions.js.map +1 -0
  75. package/dist/supervisor.js +472 -28
  76. package/dist/supervisor.js.map +1 -1
  77. package/dist/timings.js +61 -0
  78. package/dist/timings.js.map +1 -0
  79. package/dist/tool-filter.js +59 -0
  80. package/dist/tool-filter.js.map +1 -0
  81. package/dist/transports/docker.js.map +1 -1
  82. package/dist/transports/stream.js +33 -20
  83. package/dist/transports/stream.js.map +1 -1
  84. package/dist/upstream/auth.js +435 -0
  85. package/dist/upstream/auth.js.map +1 -0
  86. package/dist/upstream/login.js +94 -0
  87. package/dist/upstream/login.js.map +1 -0
  88. package/dist/upstream/provider.js +286 -0
  89. package/dist/upstream/provider.js.map +1 -0
  90. package/dist/upstream/routes.js +97 -0
  91. package/dist/upstream/routes.js.map +1 -0
  92. package/package.json +23 -7
  93. package/dist/auth/provider.js +0 -380
  94. package/dist/auth/provider.js.map +0 -1
  95. package/dist/auth/routes.js +0 -223
  96. package/dist/auth/routes.js.map +0 -1
package/README.md CHANGED
@@ -1,20 +1,32 @@
1
1
  # mcp-hub
2
2
 
3
- [![CI](https://github.com/ni-c/mcp-hub/actions/workflows/ci.yml/badge.svg)](https://github.com/ni-c/mcp-hub/actions/workflows/ci.yml)
3
+ [![CI](https://img.shields.io/github/actions/workflow/status/ni-c/mcp-hub/ci.yml?branch=main&label=CI)](https://github.com/ni-c/mcp-hub/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/%40ni-c%2Fmcp-hub)](https://www.npmjs.com/package/@ni-c/mcp-hub)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/%40ni-c%2Fmcp-hub)](https://www.npmjs.com/package/@ni-c/mcp-hub)
6
- [![Container](https://img.shields.io/badge/ghcr.io-ni--c%2Fmcp--hub-2496ED?logo=docker&logoColor=white)](https://github.com/ni-c/mcp-hub/pkgs/container/mcp-hub)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
8
- [![Docs](https://img.shields.io/badge/docs-mcp--hub.ni--c.de-4f46e5)](https://mcp-hub.ni-c.de)
9
-
10
- 📖 **Full documentation: <https://mcp-hub.ni-c.de>**
11
-
12
- Serve many stdio MCP servers from **one container**, published over HTTPS for
13
- ChatGPT connectors, Claude (Web and Code), Mistral Le Chat, Cursor, LibreChat
14
- and any other Streamable-HTTP MCP clientwith a built-in OAuth 2.1 login
15
- protected by a single password, plus long-lived API tokens for clients that
16
- cannot do OAuth (OpenAI Responses API, xAI API, Gemini API). Per-client
17
- recipes: [client compatibility](https://mcp-hub.ni-c.de/guide/client-compatibility).
6
+ [![node](https://img.shields.io/node/v/%40ni-c%2Fmcp-hub)](https://nodejs.org)
7
+ [![license](https://img.shields.io/npm/l/%40ni-c%2Fmcp-hub)](LICENSE)
8
+ [![container](https://img.shields.io/badge/ghcr.io-ni--c%2Fmcp--hub-blue)](https://github.com/ni-c/mcp-hub/pkgs/container/mcp-hub)
9
+ [![docs](https://img.shields.io/badge/docs-mcp--hub.ni--c.de-informational)](https://mcp-hub.ni-c.de)
10
+ [![sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/ni-c)
11
+
12
+ A dual-era [Model Context Protocol](https://modelcontextprotocol.io) (MCP) gateway: it
13
+ serves many stdio MCP servers from **one container**, published over HTTPS, and speaks
14
+ **both MCP revisions on every endpoint**`2026-07-28` and `2025-11-25`. The client
15
+ picks, and cannot tell which one it is on from the answers. On the 2026 revision that
16
+ includes **elicitation** a child server's question reaches the person at the far end
17
+ instead of dying at the gateway
18
+ ([how](https://mcp-hub.ni-c.de/guide/elicitation)) — and **subscriptions**: the hub
19
+ serves `subscriptions/listen` to its clients and subscribes to its children on
20
+ whichever revision _they_ speak, so a server that has never heard of it still reaches
21
+ a client that speaks nothing else
22
+ ([how](https://mcp-hub.ni-c.de/guide/subscriptions)).
23
+
24
+ Lets MCP clients that cannot spawn a local process — ChatGPT connectors, Claude on
25
+ the Web and in Code, Mistral Le Chat, Cursor, LibreChat and any other
26
+ Streamable-HTTP client — reach every server behind it, with a built-in OAuth 2.1
27
+ login protected by a single password, plus long-lived API tokens for clients that
28
+ cannot do OAuth (OpenAI Responses API, xAI API, Gemini API). Per-client recipes:
29
+ [client compatibility](https://mcp-hub.ni-c.de/guide/client-compatibility).
18
30
 
19
31
  <!-- <picture> is resolved against the colour scheme of the page showing it, so GitHub
20
32
  picks the variant that matches its own theme toggle. npm strips <picture> and
@@ -28,18 +40,29 @@ recipes: [client compatibility](https://mcp-hub.ni-c.de/guide/client-compatibili
28
40
 
29
41
  ![Demo: config in, hub up, servers reachable through one endpoint](https://mcp-hub.ni-c.de/demo.gif)
30
42
 
43
+ **Want to poke at it first?** [`demo/`](demo/) is a throwaway hub with three
44
+ fake servers — `docker compose up -d`, then point the
45
+ [MCP Inspector](https://mcp-hub.ni-c.de/guide/demo#mcp-inspector) or
46
+ [MCPJam](https://mcp-hub.ni-c.de/guide/demo#mcpjam) at it. Nothing to
47
+ configure, nothing to clean up but a volume.
48
+
31
49
  ## Why
32
50
 
33
51
  Wrapping each stdio MCP server in its own auth-proxy container costs a full
34
- image, an OAuth stack, a hostname and a compose stack *per server*. mcp-hub
52
+ image, an OAuth stack, a hostname and a compose stack _per server_. mcp-hub
35
53
  replaces N containers with one process:
36
54
 
37
55
  - **Config is exactly Claude Code's `mcpServers` format** — copy entries 1:1.
38
56
  - **Path-based routing**: `https://host/paperless`, `https://host/homeassistant`, …
39
- - **`/hub` aggregate**: register a *single* connector and reach every server
57
+ - **`/hub` aggregate**: register a _single_ connector and reach every server
40
58
  through 6 meta-tools (`list_servers`, `list_tools`, `get_tool_schema`,
41
59
  `call_tool`, `wake_server`, `sleep_server`) without flooding the model
42
60
  context with N×tools schemas.
61
+ - **Per-server tool filtering**: `allowTools` / `denyTools` on any server decide
62
+ which of its tools the hub exposes — exact names or `list_*` prefixes. A
63
+ filtered tool is hidden from `tools/list` _and_ refused if a client calls it
64
+ anyway, before the server is even woken, so a client holding a stale schema
65
+ cannot reach it.
43
66
  - **Also without HTTP**: `mcp-hub --stdio` serves that same aggregate on
44
67
  stdin/stdout for clients that can only spawn a local process (Claude Desktop,
45
68
  Codex, …) — same `mcp.json`, no TLS, no reverse proxy, no login. Auth exists
@@ -48,6 +71,20 @@ replaces N containers with one process:
48
71
  after 60 idle minutes, answering `initialize`/`tools/list` from a persistent
49
72
  snapshot meanwhile — a dozen servers cost only the memory of the ones in
50
73
  use. `keepAlive: true` exempts a server, `IDLE_TIMEOUT_MINUTES=0` the hub.
74
+ - **CIMD-first OAuth 2.1**: clients identify themselves with a [Client ID
75
+ Metadata Document](https://mcp-hub.ni-c.de/guide/client-registration) — the
76
+ registration-free path the MCP spec now prefers — including `private_key_jwt`
77
+ against the keys in their own document (metadata-document clients only). RFC
78
+ 7591 dynamic registration stays advertised beside it for older clients,
79
+ `mcp-hub-admin clients add` issues credentials by hand for anything that can
80
+ do neither, and `CLIENT_REGISTRATION` turns either mechanism off.
81
+ - **OAuth outwards, too**: a remote server that speaks OAuth gets an `oauth`
82
+ block instead of a static header. The hub registers itself — with credentials
83
+ the upstream issued, via RFC 7591, or with its own client metadata document —
84
+ then obtains and refreshes the token. `client_credentials` upstreams need no
85
+ attention at all; where a person must sign in, `mcp-hub-admin upstream login`
86
+ prints one URL. An upstream that needs re-authorizing shows up as one server
87
+ `unauthorized`, not as a confusing 401 in your client.
51
88
  - **Supervision**: children are pinged and restarted with exponential backoff
52
89
  when they die. A down server answers 503, not silence; a crash-looping
53
90
  server nobody uses is parked instead of restarted forever.
@@ -55,11 +92,62 @@ replaces N containers with one process:
55
92
  servers.
56
93
  - **Stateless Streamable HTTP**: no session state, so claude.ai's
57
94
  reconnect-without-DELETE behaviour cannot leak processes or memory.
95
+ - **Dual-era**: every endpoint — `/hub`, `/<name>/mcp` and `--stdio` — answers
96
+ MCP `2026-07-28` and `2025-11-25` alike; the client picks and cannot tell
97
+ from the answers which it got. On the 2026 revision that includes
98
+ **elicitation**: a server asking the user something returns the question
99
+ rather than pushing it, so it reaches the person at the far end instead of
100
+ dying at the gateway. The hub attributes it to the server that asked, strips
101
+ what could lie about that, drops embedded sampling and roots requests, and
102
+ seals the resumption state against the call it belongs to. `passthrough:
103
+ "off"` withdraws one server's right to ask;
104
+ [details](https://mcp-hub.ni-c.de/guide/elicitation).
105
+ - **Change notifications, in both eras**: a client opens a
106
+ `subscriptions/listen` stream and hears when a child's tools, prompts or
107
+ resources change. The hub subscribes to each child the way that child
108
+ understands — `subscriptions/listen` to a 2026 server, `resources/subscribe`
109
+ to a 2025 one — so the era gap is the gateway's problem rather than either
110
+ end's. The state is the open response, not a session table, so this costs the
111
+ stateless design nothing. A sleeping server watches nothing and is told to
112
+ re-read on waking; `subscriptions: "off"` withdraws one server's right to
113
+ push; [details](https://mcp-hub.ni-c.de/guide/subscriptions).
58
114
  - **Lightweight by design**: one Node process, no database (state is one JSON
59
- file plus a JWT key under `/data`), a handful of runtime dependencies, and
115
+ file plus a signing key under `/data`), six runtime dependencies, and
60
116
  multi-arch images — a stated project goal is to run comfortably on a
61
117
  single-board computer like a Raspberry Pi.
62
118
 
119
+ ## Servers to run behind it
120
+
121
+ The hub is server-agnostic — it serves any stdio MCP server whose entry fits
122
+ Claude Code's `mcpServers` format, which is most of them. These seventeen are
123
+ built and maintained alongside it, so their documentation carries the hub entry
124
+ you need and their tool filters line up with the hub's own `allowTools` /
125
+ `denyTools`:
126
+
127
+ | Server | npm | What it reaches |
128
+ | ---------------------------------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------ |
129
+ | [audiobookshelf-mcp](https://audiobookshelf-mcp.ni-c.de) | `audiobookshelf-mcp` | Audiobookshelf — libraries, listening progress, collections and playlists |
130
+ | [calibreweb-mcp](https://calibreweb-mcp.ni-c.de) | `calibreweb-mcp` | Calibre-Web — read-only library access through the OPDS feed |
131
+ | [freshrss-mcp](https://freshrss-mcp.ni-c.de) | `@ni-c/freshrss-mcp` | FreshRSS — feeds, categories and articles as plain text, not stream ids |
132
+ | [google-search-console-mcp](https://google-search-console-mcp.ni-c.de) | `@ni-c/google-search-console-mcp` | Google Search Console — properties, sitemaps, search analytics, URL inspection |
133
+ | [healthchecks-mcp](https://healthchecks-mcp.ni-c.de) | `healthchecks-mcp` | Healthchecks — cron and uptime checks, and why one failed |
134
+ | [hetzner-dns-mcp](https://hetzner-dns-mcp.ni-c.de) | `hetzner-dns-mcp` | Hetzner Cloud DNS — zones, record sets and BIND import/export |
135
+ | [imap-mcp](https://imap-mcp.ni-c.de) | `@ni-c/imap-mcp` | IMAP mailboxes — read, search, organise and draft mail; it cannot send |
136
+ | [linkwarden-mcp](https://linkwarden-mcp.ni-c.de) | `linkwarden-mcp` | Linkwarden — bookmarks, collections and the article text it preserved |
137
+ | [mealie-mcp](https://mealie-mcp.ni-c.de) | `@ni-c/mealie-mcp` | Mealie — recipes, meal plans, shopping lists and cookbooks |
138
+ | [ntfy-mcp](https://ntfy-mcp.ni-c.de) | `@ni-c/ntfy-mcp` | ntfy — publish and update notifications, manage users and topic access |
139
+ | [opengist-mcp](https://opengist-mcp.ni-c.de) | `opengist-mcp` | Opengist — gists, revisions, commit history and raw files |
140
+ | [osm-mcp](https://osm-mcp.ni-c.de) | `osm-mcp` | OpenStreetMap — geocoding, routing, isochrones and POI search |
141
+ | [rustpad-mcp](https://rustpad-mcp.ni-c.de) | `rustpad-mcp` | Rustpad — collaborative pads edited through real OT, not overwrites |
142
+ | [smtp-mcp](https://smtp-mcp.ni-c.de) | `@ni-c/smtp-mcp` | SMTP — sends mail, behind a recipient allowlist and a human confirmation |
143
+ | [wg-easy-mcp](https://wg-easy-mcp.ni-c.de) | `wg-easy-mcp` | wg-easy v15+ — the full WireGuard client lifecycle |
144
+ | [wikijs-mcp](https://wikijs-mcp.ni-c.de) | `@ni-c/wikijs-mcp` | Wiki.js — search, read and edit pages, plus assets, users and groups |
145
+ | [woodpecker-ci-mcp](https://woodpecker-ci-mcp.ni-c.de) | `@ni-c/woodpecker-ci-mcp` | Woodpecker CI — repositories, pipelines, logs, secrets and crons |
146
+
147
+ Each one runs perfectly well on its own over stdio. Put them behind the hub when
148
+ you want them reachable from a client that cannot spawn a local process, or when
149
+ you would rather register one connector than seventeen.
150
+
63
151
  ## Configuration
64
152
 
65
153
  `/config/mcp.json` — identical to Claude Code (`${VAR}` expands from the
@@ -81,6 +169,11 @@ version in your image; do not download mutable packages at runtime:
81
169
  "headers": { "Authorization": "Bearer ${HA_TOKEN}" }
82
170
  },
83
171
  "private-thing": { "command": "some-mcp", "args": [], "hub": false },
172
+ "paperless-readonly": {
173
+ "command": "paperless-mcp",
174
+ "allowTools": ["search_*", "get_document"],
175
+ "denyTools": ["delete_document"]
176
+ },
84
177
  "untrusted": {
85
178
  "type": "docker",
86
179
  "image": "ghcr.io/example/untrusted-mcp@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
@@ -95,13 +188,16 @@ Stdio servers (`command`/`args`/`env`) are spawned as supervised child
95
188
  processes. Remote servers (`type: "http"` or `"sse"` with `url` and optional
96
189
  `headers`) are connected as MCP clients with the configured headers injected
97
190
  on every request — the same supervision (ping, backoff reconnect, hot reload)
98
- applies. Upstreams that require their own *interactive* OAuth cannot be
99
- configured with static headers; bridge those with an
100
- [`mcp-remote`](https://github.com/geelen/mcp-remote) stdio entry and persist
101
- its token cache (`MCP_REMOTE_CONFIG_DIR`) under `/data`.
191
+ applies. An upstream that speaks OAuth gets an `oauth` block instead of a
192
+ header: the hub registers itself (statically, via RFC 7591 or via a client
193
+ metadata document), obtains the token and refreshes it, with one browser visit
194
+ started from the admin CLI where the grant needs a person.
102
195
  `"hub": false` hides a server from the `/hub` aggregate; its own path keeps
103
- working. Reserved names: `mcp`, `hub`, `authorize`, `token`, `register`,
104
- `login`, `consent`, `health`, `livez`, `revoke`.
196
+ working. `allowTools` / `denyTools` cut finer and apply to every kind of
197
+ server: a filtered tool is absent from both `tools/list` and `/hub`, and is
198
+ refused if called anyway — before the server is woken.
199
+ Reserved names: `mcp`, `hub`, `authorize`, `token`, `register`,
200
+ `login`, `consent`, `health`, `livez`, `revoke`, `upstream`, `.well-known`.
105
201
 
106
202
  All stdio children share the hub's Unix user and can read its mounted files.
107
203
  Only install fully trusted stdio servers. A server with a different trust level
@@ -126,7 +222,7 @@ and [SECURITY.md](SECURITY.md).
126
222
  For a custom image, pin every package to an exact version:
127
223
 
128
224
  ```dockerfile
129
- FROM ghcr.io/ni-c/mcp-hub:0.6.0 # pin @sha256:<digest> in production
225
+ FROM ghcr.io/ni-c/mcp-hub:0.10.0 # pin @sha256:<digest> in production
130
226
  USER root
131
227
  RUN npm install -g your-mcp-package@1.2.3
132
228
  USER node
@@ -134,24 +230,38 @@ USER node
134
230
 
135
231
  ### Environment
136
232
 
137
- | Variable | Required | Description |
138
- |---|---|---|
139
- | `EXTERNAL_URL` | yes | Public base URL, e.g. `https://mcp.example.net` (no path) |
140
- | `PASSWORD_HASH` | one of | bcrypt hash of the login password (`htpasswd -bnBC 10 "" 'pw' \| tr -d ':\n'`) |
141
- | `PASSWORD` | one of | plain-text alternative to `PASSWORD_HASH` |
142
- | `TRUSTED_PROXIES` | no | comma-separated IPs/CIDRs allowed to set `X-Forwarded-*` (see below) |
143
- | `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 |
144
- | `DEFAULT_RESOURCE` | no | server name (or `hub`) to bind tokens to when a client sends no `resource` parameter; unset → such requests are refused |
145
- | `MCP_BODY_LIMIT` | no | authenticated MCP JSON body limit, default `1mb` |
146
- | `MCP_REQUESTS_PER_MINUTE` | no | limit per OAuth client, default `120` |
147
- | `MCP_MAX_CONCURRENT_REQUESTS` | no | in-flight request limit per OAuth client, default `4` |
148
- | `MCP_MAX_CONCURRENT_STREAMS` | no | open SSE listening streams per OAuth client — one per connected session, default `32` |
149
- | `HTTP_HEADERS_TIMEOUT_MS` | no | Node HTTP header timeout, default `10000` |
150
- | `HTTP_REQUEST_TIMEOUT_MS` | no | complete request timeout, default `310000` (slightly above the tool-call timeout) |
151
- | `PORT` | no | listen port (default 80 in the image, 3000 outside) |
152
- | `CONFIG_PATH` | no | default `/config/mcp.json` |
153
- | `DATA_PATH` | no | default `/data` |
154
- | `LOG_FILE` | no | additionally mirror all log output into this file, e.g. `/data/mcp-hub.log` (see below) |
233
+ | Variable | Required | Description |
234
+ | ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
235
+ | `EXTERNAL_URL` | yes | Public base URL, e.g. `https://mcp.example.net` (no path) |
236
+ | `PASSWORD_HASH` | one of | bcrypt hash of the login password (`htpasswd -bnBC 10 "" 'pw' \| tr -d ':\n'`) |
237
+ | `PASSWORD` | one of | plain-text alternative to `PASSWORD_HASH` |
238
+ | `TRUSTED_PROXIES` | no | comma-separated IPs/CIDRs allowed to set `X-Forwarded-*` (see below) |
239
+ | `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 |
240
+ | `DEFAULT_RESOURCE` | no | server name (or `hub`) to bind tokens to when a client sends no `resource` parameter; unset → such requests are refused |
241
+ | `MCP_BODY_LIMIT` | no | authenticated MCP JSON body limit, default `1mb` |
242
+ | `MCP_REQUESTS_PER_MINUTE` | no | limit per OAuth client, default `120` |
243
+ | `MCP_MAX_CONCURRENT_REQUESTS` | no | in-flight request limit per OAuth client, default `4` |
244
+ | `MCP_MAX_CONCURRENT_STREAMS` | no | open SSE listening streams per OAuth client — one per connected session, default `32` |
245
+ | `HTTP_HEADERS_TIMEOUT_MS` | no | Node HTTP header timeout, default `10000` |
246
+ | `HTTP_REQUEST_TIMEOUT_MS` | no | complete request timeout, default `310000` (slightly above the tool-call timeout) |
247
+ | `PORT` | no | listen port (default 80 in the image, 3000 outside) |
248
+ | `CONFIG_PATH` | no | default `/config/mcp.json` |
249
+ | `DATA_PATH` | no | default `/data` |
250
+ | `LOG_FILE` | no | additionally mirror all log output into this file, e.g. `/data/mcp-hub.log` (see below) |
251
+ | `CLIENT_REGISTRATION` | no | which mechanisms a client may use for a `client_id`: `cimd`, `dcr` or both (default) |
252
+ | `CIMD_ALLOWED_ORIGINS` | no | bare https origins whose metadata documents are accepted; unset → any |
253
+ | `CIMD_ALLOW_PRIVATE_ADDRESSES` | no | local development only; relaxes the SSRF guard, warns on every start |
254
+ | `DCR_MAX_CLIENTS` | no | ceiling on stored dynamic registrations, default `500` |
255
+ | `DCR_PENDING_TTL_HOURS` | no | how long a never-approved registration is kept, default `24` |
256
+ | `DCR_INACTIVE_DAYS` | no | how long an unused approved registration is kept, default `90` |
257
+ | `IDLE_TIMEOUT_MINUTES` | no | idle minutes before an on-demand server sleeps, default `60`; `0` disables it |
258
+ | `TOOL_CACHE_PATH` | no | snapshots of sleeping servers, default `<DATA_PATH>/tool-cache.json` |
259
+ | `MCP_CALL_TIMEOUT_MS` | no | deadline for one forwarded tool call, default `300000` |
260
+ | `MCP_RESET_TIMEOUT_ON_PROGRESS` | no | let progress notifications extend that deadline, default `false` |
261
+ | `DOCKER_HOST` | with docker servers | the **policy proxy's** socket; a direct daemon socket fails closed |
262
+
263
+ The full table, including what applies in stdio mode, is in the
264
+ [environment reference](https://mcp-hub.ni-c.de/reference/environment).
155
265
 
156
266
  `/data` holds the Ed25519 JWT key, registered OAuth clients, approvals and
157
267
  refresh tokens. **Mount it as a volume** — recreating it invalidates every
@@ -170,7 +280,7 @@ invalidates those unbound tokens, so every connector authorizes once more.
170
280
 
171
281
  `TRUSTED_PROXIES` decides what `req.ip` is, and therefore what the login rate
172
282
  limiter counts. List **only** your own reverse proxy, and make sure it
173
- *overwrites* `X-Forwarded-For` rather than appending to it — otherwise a
283
+ _overwrites_ `X-Forwarded-For` rather than appending to it — otherwise a
174
284
  client can supply its own address and rotate it to sidestep the per-IP limit.
175
285
  If the variable is unset, every request appears to come from the proxy and
176
286
  per-IP limiting degrades to a single global counter (the hub logs a warning
@@ -185,7 +295,7 @@ Published on every push to `main` and every `vX.Y.Z` release tag, for
185
295
  [package page](https://github.com/ni-c/mcp-hub/pkgs/container/mcp-hub).
186
296
 
187
297
  ```sh
188
- docker pull ghcr.io/ni-c/mcp-hub:0.6.0
298
+ docker pull ghcr.io/ni-c/mcp-hub:0.10.0
189
299
  ```
190
300
 
191
301
  Tags: `latest` (tip of `main`), `X.Y.Z` and `X.Y` (releases), and
@@ -200,7 +310,7 @@ With compose, copy the example and point it at the image instead of building:
200
310
  ```yaml
201
311
  services:
202
312
  mcp-hub:
203
- image: ghcr.io/ni-c/mcp-hub:0.6.0 # replaces `build: .`; pin a digest in production
313
+ image: ghcr.io/ni-c/mcp-hub:0.10.0 # replaces `build: .`; pin a digest in production
204
314
  # ...rest of docker-compose.example.yml unchanged
205
315
  ```
206
316
 
@@ -222,7 +332,7 @@ docker run -d --name mcp-hub \
222
332
  -e TRUSTED_PROXIES="192.168.1.0/24" \
223
333
  -v "$PWD/config:/config:ro" \
224
334
  -v "$PWD/data:/data" \
225
- ghcr.io/ni-c/mcp-hub:0.6.0
335
+ ghcr.io/ni-c/mcp-hub:0.10.0
226
336
  ```
227
337
 
228
338
  Update to a newer image with `docker compose pull && docker compose up -d`
@@ -263,7 +373,7 @@ see [client compatibility](https://mcp-hub.ni-c.de/guide/client-compatibility).
263
373
 
264
374
  Each client is confirmed once. Entering the password approves the client that
265
375
  asked; while a login session is still valid, a client you have not seen before
266
- gets an explicit *Approve / Deny* page instead of a code. Approved clients
376
+ gets an explicit _Approve / Deny_ page instead of a code. Approved clients
267
377
  reconnect silently from then on.
268
378
 
269
379
  List clients or revoke one. The CLI shares `/data` with the running hub and
@@ -273,10 +383,17 @@ live container — a revocation takes effect on the next request:
273
383
  ```sh
274
384
  docker exec mcp-hub node /app/dist/admin.js clients list
275
385
  docker exec mcp-hub node /app/dist/admin.js clients revoke CLIENT_ID
386
+ docker exec mcp-hub node /app/dist/admin.js clients delete CLIENT_ID
387
+ docker exec mcp-hub node /app/dist/admin.js clients prune --dry-run
276
388
  ```
277
389
 
278
390
  Revocation removes the approval and all refresh tokens and immediately rejects
279
391
  already-issued access tokens. The next connection needs explicit approval.
392
+ `delete` goes further and removes the registration itself, and `prune` applies
393
+ the [registration lifecycle rules](https://mcp-hub.ni-c.de/guide/client-registration)
394
+ on demand — registrations that were never approved expire after a day, unused
395
+ ones after 90 days, and a dynamically registered client can also remove its own
396
+ registration through RFC 7592.
280
397
 
281
398
  For clients that cannot do OAuth at all — the OpenAI Responses API, the xAI
282
399
  API, Gemini's `mcp_server` tool, plain-header clients — the same CLI mints
@@ -294,27 +411,35 @@ immediately. Per-client recipes:
294
411
 
295
412
  ## Endpoints
296
413
 
297
- | Path | Auth | Purpose |
298
- |---|---|---|
299
- | `/<name>`, `/<name>/mcp` | Bearer | Streamable HTTP endpoint of one server |
300
- | `/hub` | Bearer | aggregate endpoint with the 4 meta-tools |
301
- | `/livez` | none | minimal process liveness (`200`) |
302
- | `/health` | Bearer | per-server status (`200` all up / `503` degraded) |
303
- | `/authorize`, `/token`, `/register`, `/login`, `/consent`, `/revoke` | — | OAuth 2.1 + DCR |
304
- | `/.well-known/oauth-authorization-server[/…]` | none | RFC 8414 metadata |
305
- | `/.well-known/oauth-protected-resource[/…]` | none | RFC 9728 metadata (path-scoped) |
414
+ | Path | Auth | Purpose |
415
+ | -------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------- |
416
+ | `/<name>`, `/<name>/mcp` | Bearer | Streamable HTTP endpoint of one server |
417
+ | `/hub` | Bearer | aggregate endpoint with the 6 meta-tools |
418
+ | `/livez` | none | minimal process liveness (`200`) |
419
+ | `/health` | Bearer | per-server status (`200` all up / `503` degraded) |
420
+ | `/authorize`, `/token`, `/register`, `/login`, `/consent`, `/revoke` | — | OAuth 2.1 · CIMD + DCR |
421
+ | `/register/<client_id>` | registration token | RFC 7592: a client reads, changes or removes its own registration |
422
+ | `/upstream/callback` | signed state + hub session | where an upstream returns after `upstream login` |
423
+ | `/.well-known/mcp-hub-client/<id>.json` | none | the hub's own client metadata document, one per `cimd` upstream |
424
+ | `/.well-known/oauth-authorization-server[/…]` | none | RFC 8414 metadata |
425
+ | `/.well-known/oauth-protected-resource[/…]` | none | RFC 9728 metadata (path-scoped) |
306
426
 
307
427
  ## Notes & limitations
308
428
 
309
- - Stateless transport: server-initiated notifications (`listChanged`,
310
- subscriptions, sampling) are not delivered to clients. Tool/resource/prompt
311
- request-response works fully; the hub's tool cache does follow
312
- `tools/list_changed` internally.
313
- - Access tokens are self-contained 15-minute JWTs. Revoking a client rejects
314
- its existing JWTs and removes all of its refresh tokens. Refresh tokens
315
- rotate; replaying a token
316
- that was already rotated away revokes its whole chain, and a refresh cannot
317
- ask for more scope than the original grant.
429
+ - Change notifications (`listChanged`, resource updates) are carried on
430
+ `2026-07-28` via `subscriptions/listen`, whose state is the open response
431
+ rather than a session table. A `2025-11-25` client is offered neither, because
432
+ that revision needs a channel the stateless transport does not keep — so the
433
+ capability is withheld instead of announced and dropped. An on-demand server
434
+ watches nothing while it sleeps; the subscription is re-established on the
435
+ next wake and the client is told to re-read.
436
+ - Elicitation travels end to end on `2026-07-28`: it is a result rather than a
437
+ push. Sampling and log messages are not forwarded.
438
+ - Access tokens are opaque and last 15 minutes. Revoking a client takes effect
439
+ on its next request rather than when the token expires — the token is a
440
+ reference to a stored record, so withdrawing it is a deletion. Refresh tokens
441
+ rotate; replaying one that was already rotated away is treated as a leak and
442
+ revokes the whole grant, access tokens included.
318
443
  - Upstream auth is fully decoupled from the hub's own OAuth: an expired
319
444
  upstream token just marks that one server `down` (503 on its path, visible
320
445
  in `/health`) — clients never see the upstream's 401.