@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 +23 -0
- package/dist/docker-proxy/server.js +9 -7
- package/dist/hub.js +35 -35
- package/package.json +2 -2
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.
|
|
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.
|
|
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",
|