@ni-c/mcp-hub 0.11.4 → 0.11.5

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  <!-- #region changelog -->
9
9
 
10
+ ## [0.11.5] - 2026-10-06
11
+
12
+ ### Security
13
+
14
+ - `proxy-addr` 2.0.8 (was 2.0.7, GHSA-jqcg-44mw-7w3h, critical). A trust
15
+ subnet written as an IPv4-mapped IPv6 block with a short prefix, such as
16
+ `::ffff:10.0.0.0/8`, matched every IPv4 address. `trustedProxies` is handed
17
+ to Express as `trust proxy`, so with such an entry any client could set its
18
+ own `req.ip` through `X-Forwarded-For` — the key of the per-caller rate limit
19
+ and the address in the consent log. Plain IPv4 notation (`10.0.0.0/8`) was
20
+ never affected.
21
+ - `source-map-js` 1.2.2 (was 1.2.1, GHSA-68fv-2mgg-jv7q). A development
22
+ dependency only; it is not in the package or the image.
23
+ - The documentation site builds with `vue` and `@vue/server-renderer` 3.5.43
24
+ (was 3.5.41, GHSA-g2v6-rqmx-r4w6). The site renders only its own Markdown at
25
+ build time; neither the package nor the image contains Vue.
26
+ - The image overwrites npm's vendored `brace-expansion` with 5.0.11 instead of
27
+ 5.0.9 and now also its vendored `undici`, with 6.28.1. npm 12.0.2 pins
28
+ `undici` 6.27.0, and the latest npm, 12.1.0, still ships 6.28.0 and
29
+ `brace-expansion` 5.0.9, which CVE-2026-19534 (`undici`) and CVE-2026-102276
30
+ and CVE-2026-102278 (`brace-expansion`) apply to. Only the npm CLI inside the
31
+ image uses these copies; the hub's own dependencies were not affected.
32
+
10
33
  ## [0.11.4] - 2026-09-24
11
34
 
12
35
  ### Security
@@ -30,6 +30,11 @@ function refuse(response, status, reason, closeConnection = false) {
30
30
  });
31
31
  response.end(body);
32
32
  }
33
+ /**
34
+ * An upgrade is answered on the raw socket, and a refused one must not leave
35
+ * the caller holding a half-open connection: write, then destroy.
36
+ */
37
+ const hangUp = (clientSocket, status, reason) => clientSocket.end(`HTTP/1.1 ${status} ${status === 403 ? 'Forbidden' : 'Bad Gateway'}\r\nConnection: close\r\nContent-Length: ${Buffer.byteLength(reason)}\r\n\r\n${reason}`, () => clientSocket.destroy());
33
38
  function readBody(request) {
34
39
  return new Promise((resolve, reject) => {
35
40
  const chunks = [];
@@ -251,16 +256,13 @@ export function createDockerProxy(options) {
251
256
  server.on('upgrade', (request, clientSocket, head) => {
252
257
  void (async () => {
253
258
  const decision = decide(request.method ?? 'GET', request.url ?? '/', undefined);
254
- // An upgrade is answered on the raw socket, and a refused one must not
255
- // leave the caller holding a half-open connection: write, then destroy.
256
- const hangUp = (status, reason) => clientSocket.end(`HTTP/1.1 ${status} ${status === 403 ? 'Forbidden' : 'Bad Gateway'}\r\nConnection: close\r\nContent-Length: ${Buffer.byteLength(reason)}\r\n\r\n${reason}`, () => clientSocket.destroy());
257
259
  if (!decision.allow || !decision.upgrade) {
258
- hangUp(decision.allow ? 403 : decision.status, decision.allow ? 'this endpoint does not support upgrades' : decision.reason);
260
+ hangUp(clientSocket, decision.allow ? 403 : decision.status, decision.allow ? 'this endpoint does not support upgrades' : decision.reason);
259
261
  return;
260
262
  }
261
263
  const ownershipError = await verifyContainer(options, decision);
262
264
  if (ownershipError) {
263
- hangUp(ownershipError.status, ownershipError.reason);
265
+ hangUp(clientSocket, ownershipError.status, ownershipError.reason);
264
266
  return;
265
267
  }
266
268
  const upstream = http.request({
@@ -297,11 +299,11 @@ export function createDockerProxy(options) {
297
299
  });
298
300
  upstream.on('response', upstreamResponse => {
299
301
  upstreamResponse.resume();
300
- hangUp(502, `docker daemon answered ${upstreamResponse.statusCode} instead of upgrading`);
302
+ hangUp(clientSocket, 502, `docker daemon answered ${upstreamResponse.statusCode} instead of upgrading`);
301
303
  });
302
304
  upstream.on('error', error => {
303
305
  console.error(`docker-proxy: upstream error on attach: ${error.message}`);
304
- hangUp(502, `docker daemon unreachable: ${error.message}`);
306
+ hangUp(clientSocket, 502, `docker daemon unreachable: ${error.message}`);
305
307
  });
306
308
  upstream.end();
307
309
  })().catch(error => {
package/dist/hub.js CHANGED
@@ -178,6 +178,41 @@ const requireAllowedTool = (managed, tool, server) => {
178
178
  console.warn(`[${managed.name}] refused "${loggableToolName(tool)}": not permitted by allowTools/denyTools`);
179
179
  return toolError(`Unknown tool "${tool}" on server "${server}". Use list_tools to see available tools.`);
180
180
  };
181
+ /**
182
+ * `hub: false` hides a server's TOOLS from the aggregate — those are meant
183
+ * to be used through the server's own endpoint. Its lifecycle is a different
184
+ * matter: wake_server/sleep_server manage hidden servers too, so the error
185
+ * points at the right door instead of pretending the server does not exist
186
+ * (/health names every server to the same token anyway).
187
+ */
188
+ const requireExposed = (managed) => {
189
+ if (managed.config.hub)
190
+ return undefined;
191
+ return toolError(`Server "${managed.name}" is not exposed through /hub — connect to its own endpoint /${managed.name}/mcp instead.`);
192
+ };
193
+ /**
194
+ * Asking about an on-demand server's tools is the strongest hint that a
195
+ * call follows, so a sleeping server is pre-warmed in the background while
196
+ * the cached snapshot answers. Only a server with nothing cached yet (first
197
+ * ever run) blocks on the start — there is nothing truthful to answer from.
198
+ * Always-running servers pass through untouched.
199
+ */
200
+ const prepare = async (managed) => {
201
+ if (!managed.onDemand)
202
+ return undefined;
203
+ if (!managed.hasSnapshot) {
204
+ try {
205
+ await managed.wake();
206
+ }
207
+ catch (error) {
208
+ return toolError(`Server "${managed.name}" failed to start: ${reason(error)}`);
209
+ }
210
+ }
211
+ else if (managed.state === 'sleeping') {
212
+ void managed.wake().catch(() => { });
213
+ }
214
+ return undefined;
215
+ };
181
216
  /**
182
217
  * The aggregate, built for the era it will serve.
183
218
  *
@@ -190,41 +225,6 @@ const requireAllowedTool = (managed, tool, server) => {
190
225
  export function buildHubServer(supervisor, secret, era) {
191
226
  const hub = new McpServer({ name: 'mcp-hub', version: VERSION });
192
227
  const findServer = (name) => supervisor.get(name);
193
- /**
194
- * `hub: false` hides a server's TOOLS from the aggregate — those are meant
195
- * to be used through the server's own endpoint. Its lifecycle is a different
196
- * matter: wake_server/sleep_server manage hidden servers too, so the error
197
- * points at the right door instead of pretending the server does not exist
198
- * (/health names every server to the same token anyway).
199
- */
200
- const requireExposed = (managed) => {
201
- if (managed.config.hub)
202
- return undefined;
203
- return toolError(`Server "${managed.name}" is not exposed through /hub — connect to its own endpoint /${managed.name}/mcp instead.`);
204
- };
205
- /**
206
- * Asking about an on-demand server's tools is the strongest hint that a
207
- * call follows, so a sleeping server is pre-warmed in the background while
208
- * the cached snapshot answers. Only a server with nothing cached yet (first
209
- * ever run) blocks on the start — there is nothing truthful to answer from.
210
- * Always-running servers pass through untouched.
211
- */
212
- const prepare = async (managed) => {
213
- if (!managed.onDemand)
214
- return undefined;
215
- if (!managed.hasSnapshot) {
216
- try {
217
- await managed.wake();
218
- }
219
- catch (error) {
220
- return toolError(`Server "${managed.name}" failed to start: ${reason(error)}`);
221
- }
222
- }
223
- else if (managed.state === 'sleeping') {
224
- void managed.wake().catch(() => { });
225
- }
226
- return undefined;
227
- };
228
228
  hub.registerTool('list_servers', {
229
229
  title: 'List MCP servers',
230
230
  description: 'List all MCP servers available through this hub, with their status. Call this first to see what is available. ' +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ni-c/mcp-hub",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
4
4
  "description": "Serve multiple stdio MCP servers from one container: Claude-Code-style mcpServers config, path-based routing, hub meta-tools, and CIMD-first OAuth 2.1 + API tokens for ChatGPT, Claude and any Streamable-HTTP MCP client.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -78,7 +78,7 @@
78
78
  "@types/node": "^26.2.0",
79
79
  "@types/oidc-provider": "^9.11.1",
80
80
  "@types/supertest": "^7.2.1",
81
- "@vitest/coverage-v8": "5.0.1",
81
+ "@vitest/coverage-v8": "5.0.2",
82
82
  "fast-check": "^4.9.0",
83
83
  "oxlint": "^1.80.0",
84
84
  "supertest": "^7.0.0",