@ni-c/mcp-hub 0.6.4 → 0.7.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.
- package/CHANGELOG.md +134 -0
- package/README.md +25 -5
- package/dist/auth/headers.js +33 -0
- package/dist/auth/headers.js.map +1 -0
- package/dist/auth/provider.js +4 -0
- package/dist/auth/provider.js.map +1 -1
- package/dist/auth/routes.js +4 -1
- package/dist/auth/routes.js.map +1 -1
- package/dist/auth/store.js +152 -35
- package/dist/auth/store.js.map +1 -1
- package/dist/config.js +209 -27
- package/dist/config.js.map +1 -1
- package/dist/docker-proxy/index.js +106 -0
- package/dist/docker-proxy/index.js.map +1 -0
- package/dist/docker-proxy/policy.js +278 -0
- package/dist/docker-proxy/policy.js.map +1 -0
- package/dist/docker-proxy/secrets.js +111 -0
- package/dist/docker-proxy/secrets.js.map +1 -0
- package/dist/docker-proxy/server.js +265 -0
- package/dist/docker-proxy/server.js.map +1 -0
- package/dist/health.js +11 -1
- package/dist/health.js.map +1 -1
- package/dist/hub.js +3 -6
- package/dist/hub.js.map +1 -1
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-limits.js +69 -0
- package/dist/mcp-limits.js.map +1 -0
- package/dist/proxy.js +4 -5
- package/dist/proxy.js.map +1 -1
- package/dist/sandbox/container-spec.js +115 -0
- package/dist/sandbox/container-spec.js.map +1 -0
- package/dist/sandbox/docker-client.js +291 -0
- package/dist/sandbox/docker-client.js.map +1 -0
- package/dist/sandbox/policy-protocol.js +11 -0
- package/dist/sandbox/policy-protocol.js.map +1 -0
- package/dist/supervisor.js +90 -17
- package/dist/supervisor.js.map +1 -1
- package/dist/transports/docker.js +163 -0
- package/dist/transports/docker.js.map +1 -0
- package/dist/transports/socket.js +59 -0
- package/dist/transports/socket.js.map +1 -0
- package/dist/transports/stream.js +102 -0
- package/dist/transports/stream.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { buildCreateRequest, containerName } from '../sandbox/container-spec.js';
|
|
2
|
+
import { StreamTransport } from './stream.js';
|
|
3
|
+
/** Guards against a corrupt header turning into a multi-gigabyte allocation. */
|
|
4
|
+
const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
5
|
+
const STDOUT = 1;
|
|
6
|
+
const STDERR = 2;
|
|
7
|
+
/**
|
|
8
|
+
* Demultiplexes Docker's attach stream.
|
|
9
|
+
*
|
|
10
|
+
* Without a TTY the daemon frames every chunk with an 8-byte header —
|
|
11
|
+
* `[stream, 0, 0, 0, size:uint32be]` — so one connection can carry stdout and
|
|
12
|
+
* stderr. That is exactly what a sandboxed MCP server needs: stdout stays a
|
|
13
|
+
* clean protocol channel while the server's log lines still reach the operator.
|
|
14
|
+
*/
|
|
15
|
+
export class DockerFrameDecoder {
|
|
16
|
+
onFrame;
|
|
17
|
+
onError;
|
|
18
|
+
buffer = Buffer.alloc(0);
|
|
19
|
+
failed = false;
|
|
20
|
+
constructor(onFrame, onError) {
|
|
21
|
+
this.onFrame = onFrame;
|
|
22
|
+
this.onError = onError;
|
|
23
|
+
}
|
|
24
|
+
push(chunk) {
|
|
25
|
+
if (this.failed)
|
|
26
|
+
return;
|
|
27
|
+
this.buffer = this.buffer.length === 0 ? chunk : Buffer.concat([this.buffer, chunk]);
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (this.buffer.length < 8)
|
|
30
|
+
return;
|
|
31
|
+
const size = this.buffer.readUInt32BE(4);
|
|
32
|
+
if (size > MAX_FRAME_BYTES) {
|
|
33
|
+
this.failed = true;
|
|
34
|
+
this.onError(new Error(`docker frame of ${size} bytes exceeds the ${MAX_FRAME_BYTES} byte limit`));
|
|
35
|
+
this.buffer = Buffer.alloc(0);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (this.buffer.length < 8 + size)
|
|
39
|
+
return;
|
|
40
|
+
const stream = this.buffer[0];
|
|
41
|
+
const payload = this.buffer.subarray(8, 8 + size);
|
|
42
|
+
this.buffer = this.buffer.subarray(8 + size);
|
|
43
|
+
this.onFrame(stream, payload);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* An MCP server running in its own container, spoken to over the Docker API.
|
|
49
|
+
*
|
|
50
|
+
* The isolation is the container's (own filesystem, own credentials, own
|
|
51
|
+
* network policy, own memory limit); the protocol is plain stdio across the
|
|
52
|
+
* container boundary. No HTTP listener, no bridge process inside the image, no
|
|
53
|
+
* shared secret — the things an HTTP upstream forces on a server that only
|
|
54
|
+
* speaks stdio.
|
|
55
|
+
*
|
|
56
|
+
* Order matters: create, then attach, then start. Starting before the attach
|
|
57
|
+
* is in place loses whatever the server writes in its first milliseconds.
|
|
58
|
+
*/
|
|
59
|
+
export class DockerTransport {
|
|
60
|
+
server;
|
|
61
|
+
config;
|
|
62
|
+
client;
|
|
63
|
+
writeStderr;
|
|
64
|
+
onclose;
|
|
65
|
+
onerror;
|
|
66
|
+
onmessage;
|
|
67
|
+
inner;
|
|
68
|
+
stream;
|
|
69
|
+
closing = false;
|
|
70
|
+
stderrTail = '';
|
|
71
|
+
constructor(server, config, client, writeStderr = line => process.stderr.write(line)) {
|
|
72
|
+
this.server = server;
|
|
73
|
+
this.config = config;
|
|
74
|
+
this.client = client;
|
|
75
|
+
this.writeStderr = writeStderr;
|
|
76
|
+
}
|
|
77
|
+
async start() {
|
|
78
|
+
const { name, body } = buildCreateRequest(this.server, this.config);
|
|
79
|
+
await this.ensureImage();
|
|
80
|
+
// A container of that name can survive an unclean hub exit (AutoRemove
|
|
81
|
+
// only fires when the container itself stops), and create would then fail
|
|
82
|
+
// with a name conflict forever.
|
|
83
|
+
await this.client.removeContainer(name);
|
|
84
|
+
await this.client.createContainer(name, body);
|
|
85
|
+
let stream;
|
|
86
|
+
try {
|
|
87
|
+
stream = await this.client.attach(name);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
await this.client.removeContainer(name).catch(() => { });
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
this.stream = stream;
|
|
94
|
+
const inner = new StreamTransport(stream, false);
|
|
95
|
+
inner.onmessage = message => this.onmessage?.(message);
|
|
96
|
+
inner.onerror = error => this.onerror?.(error);
|
|
97
|
+
inner.onclose = () => {
|
|
98
|
+
this.onclose?.();
|
|
99
|
+
// Best effort: with AutoRemove the daemon usually got there first.
|
|
100
|
+
if (!this.closing)
|
|
101
|
+
void this.client.removeContainer(name).catch(() => { });
|
|
102
|
+
};
|
|
103
|
+
await inner.start();
|
|
104
|
+
this.inner = inner;
|
|
105
|
+
const decoder = new DockerFrameDecoder((streamType, payload) => {
|
|
106
|
+
if (streamType === STDOUT)
|
|
107
|
+
inner.receive(payload);
|
|
108
|
+
else if (streamType === STDERR)
|
|
109
|
+
this.logStderr(payload);
|
|
110
|
+
}, error => {
|
|
111
|
+
this.onerror?.(error);
|
|
112
|
+
// A corrupt length makes frame boundaries unknowable. Closing the
|
|
113
|
+
// attach stream triggers container cleanup and the supervisor's normal
|
|
114
|
+
// restart backoff instead of leaving a poisoned stream alive.
|
|
115
|
+
void inner.close();
|
|
116
|
+
});
|
|
117
|
+
stream.on('data', chunk => decoder.push(chunk));
|
|
118
|
+
try {
|
|
119
|
+
await this.client.startContainer(name);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
await this.client.removeContainer(name).catch(() => { });
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async send(message) {
|
|
127
|
+
if (!this.inner)
|
|
128
|
+
throw new Error(`Server "${this.server}" is not attached`);
|
|
129
|
+
await this.inner.send(message);
|
|
130
|
+
}
|
|
131
|
+
async close() {
|
|
132
|
+
this.closing = true;
|
|
133
|
+
await this.inner?.close();
|
|
134
|
+
this.stream?.destroy();
|
|
135
|
+
await this.client.removeContainer(containerName(this.server)).catch(() => { });
|
|
136
|
+
}
|
|
137
|
+
async ensureImage() {
|
|
138
|
+
if (await this.client.imageExists(this.config.image))
|
|
139
|
+
return;
|
|
140
|
+
if (this.config.pull !== 'missing') {
|
|
141
|
+
throw new Error(`image "${this.config.image}" is not present and "pull" is "never" — build or pull it first`);
|
|
142
|
+
}
|
|
143
|
+
await this.client.pullImage(this.config.image);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Prefix the container's stderr like a stdio child's, and write it straight
|
|
147
|
+
* to the process's stderr rather than through console: stdio children use
|
|
148
|
+
* `stderr: 'inherit'` and bypass console too, which is what keeps LOG_FILE
|
|
149
|
+
* (read by fail2ban) free of server chatter.
|
|
150
|
+
*/
|
|
151
|
+
logStderr(payload) {
|
|
152
|
+
this.stderrTail += payload.toString('utf8');
|
|
153
|
+
const lines = this.stderrTail.split('\n');
|
|
154
|
+
this.stderrTail = lines.pop() ?? '';
|
|
155
|
+
for (const line of lines)
|
|
156
|
+
this.writeStderr(`[${this.server}] ${line}\n`);
|
|
157
|
+
if (this.stderrTail.length > 64 * 1024) {
|
|
158
|
+
this.writeStderr(`[${this.server}] ${this.stderrTail}\n`);
|
|
159
|
+
this.stderrTail = '';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=docker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"docker.js","sourceRoot":"","sources":["../../src/transports/docker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { StreamTransport } from './stream.js';
|
|
3
|
+
const CONNECT_TIMEOUT_MS = 10_000;
|
|
4
|
+
/**
|
|
5
|
+
* Connects to a server that already listens on a Unix socket or a TCP port and
|
|
6
|
+
* speaks stdio-framed JSON-RPC there.
|
|
7
|
+
*
|
|
8
|
+
* This is the sandboxing route that costs the hub no privileges at all: the
|
|
9
|
+
* container is started by whoever owns the Compose file, and a Unix socket in
|
|
10
|
+
* a shared volume reaches it even with `network_mode: none` — something an
|
|
11
|
+
* HTTP upstream can never offer, because HTTP needs an interface to listen on.
|
|
12
|
+
*/
|
|
13
|
+
export class SocketTransport extends StreamTransport {
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
// The socket is created here but connected in start(); an unconnected
|
|
17
|
+
// net.Socket is a perfectly ordinary Duplex until then.
|
|
18
|
+
super(new net.Socket());
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
async start() {
|
|
22
|
+
const socket = this.stream;
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
const onError = (error) => {
|
|
25
|
+
socket.destroy();
|
|
26
|
+
reject(new Error(`cannot connect to ${this.describe()}: ${error.message}`));
|
|
27
|
+
};
|
|
28
|
+
const onTimeout = () => {
|
|
29
|
+
socket.destroy();
|
|
30
|
+
reject(new Error(`timed out connecting to ${this.describe()}`));
|
|
31
|
+
};
|
|
32
|
+
socket.once('error', onError);
|
|
33
|
+
socket.once('timeout', onTimeout);
|
|
34
|
+
socket.setTimeout(CONNECT_TIMEOUT_MS);
|
|
35
|
+
socket.once('connect', () => {
|
|
36
|
+
// Hand the socket over cleanly: the connect-phase handlers must not
|
|
37
|
+
// survive, or a later error would destroy the socket behind the
|
|
38
|
+
// transport's back instead of being reported through onerror. And the
|
|
39
|
+
// connect timeout must not linger as an idle timeout — an MCP session
|
|
40
|
+
// is idle most of the time and would be torn down mid-use.
|
|
41
|
+
socket.off('error', onError);
|
|
42
|
+
socket.off('timeout', onTimeout);
|
|
43
|
+
socket.setTimeout(0);
|
|
44
|
+
if (this.config.transport === 'tcp')
|
|
45
|
+
socket.setNoDelay(true);
|
|
46
|
+
resolve();
|
|
47
|
+
});
|
|
48
|
+
if (this.config.transport === 'unix')
|
|
49
|
+
socket.connect({ path: this.config.socketPath });
|
|
50
|
+
else
|
|
51
|
+
socket.connect({ host: this.config.host, port: this.config.port });
|
|
52
|
+
});
|
|
53
|
+
await super.start();
|
|
54
|
+
}
|
|
55
|
+
describe() {
|
|
56
|
+
return this.config.transport === 'unix' ? this.config.socketPath : `${this.config.host}:${this.config.port}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket.js","sourceRoot":"","sources":["../../src/transports/socket.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,UAAU,CAAC;AAE3B,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAgB,SAAQ,eAAe;IACrB,MAAM;IAAnC,YAA6B,MAA0B;QACrD,sEAAsE;QACtE,wDAAwD;QACxD,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;sBAHG,MAAM;IAInC,CAAC;IAEQ,KAAK,CAAC,KAAK;QAClB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAoB,CAAC;QACzC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC1C,MAAM,OAAO,GAAG,CAAC,KAAY,EAAE,EAAE;gBAC/B,MAAM,CAAC,OAAO,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,KAAK,CAAC,qBAAqB,IAAI,CAAC,QAAQ,EAAE,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC9E,CAAC,CAAC;YACF,MAAM,SAAS,GAAG,GAAG,EAAE;gBACrB,MAAM,CAAC,OAAO,EAAE,CAAC;gBACjB,MAAM,CAAC,IAAI,KAAK,CAAC,2BAA2B,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC;YAClE,CAAC,CAAC;YACF,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;YAClC,MAAM,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;YACtC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;gBAC1B,oEAAoE;gBACpE,gEAAgE;gBAChE,sEAAsE;gBACtE,sEAAsE;gBACtE,2DAA2D;gBAC3D,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC7B,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;gBACjC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;gBACrB,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,KAAK,KAAK;oBAAE,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;gBAC7D,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC,CAAC;YACH,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,KAAK,MAAM;gBAAE,MAAM,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,UAAW,EAAE,CAAC,CAAC;;gBACnF,MAAM,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAK,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAK,EAAE,CAAC,CAAC;QAC5E,CAAC,CAAC,CAAC;QACH,MAAM,KAAK,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAEO,QAAQ;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,UAAW,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,IAAK,IAAI,IAAI,CAAC,MAAM,CAAC,IAAK,EAAE,CAAC;IAClH,CAAC;CACF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { ReadBuffer, serializeMessage } from '@modelcontextprotocol/sdk/shared/stdio.js';
|
|
2
|
+
/**
|
|
3
|
+
* MCP over any reliable bidirectional byte stream, using the stdio framing.
|
|
4
|
+
*
|
|
5
|
+
* The specification says exactly this about custom transports: they may run
|
|
6
|
+
* over other channels (a Unix socket, a TCP connection, a container's attached
|
|
7
|
+
* stdio) and SHOULD reuse the stdio binding's newline-delimited JSON rather
|
|
8
|
+
* than invent a framing. So this class is only plumbing — ReadBuffer and
|
|
9
|
+
* serializeMessage are the SDK's own stdio codec, byte for byte.
|
|
10
|
+
*
|
|
11
|
+
* Reading is separated from the stream on purpose: an attached container
|
|
12
|
+
* multiplexes stdout and stderr over one connection, so the caller decodes the
|
|
13
|
+
* frames and feeds only the stdout payload in via `receive()`.
|
|
14
|
+
*/
|
|
15
|
+
export class StreamTransport {
|
|
16
|
+
stream;
|
|
17
|
+
feedFromStream;
|
|
18
|
+
onclose;
|
|
19
|
+
onerror;
|
|
20
|
+
onmessage;
|
|
21
|
+
readBuffer = new ReadBuffer();
|
|
22
|
+
started = false;
|
|
23
|
+
closed = false;
|
|
24
|
+
reportedClose = false;
|
|
25
|
+
/**
|
|
26
|
+
* @param stream the duplex carrying the protocol
|
|
27
|
+
* @param feedFromStream whether stream data is plain protocol bytes. False
|
|
28
|
+
* when the caller demultiplexes first (Docker attach).
|
|
29
|
+
*/
|
|
30
|
+
constructor(stream, feedFromStream = true) {
|
|
31
|
+
this.stream = stream;
|
|
32
|
+
this.feedFromStream = feedFromStream;
|
|
33
|
+
}
|
|
34
|
+
async start() {
|
|
35
|
+
if (this.started)
|
|
36
|
+
throw new Error('StreamTransport already started');
|
|
37
|
+
this.started = true;
|
|
38
|
+
if (this.feedFromStream)
|
|
39
|
+
this.stream.on('data', chunk => this.receive(chunk));
|
|
40
|
+
this.stream.on('error', error => this.onerror?.(error));
|
|
41
|
+
// 'close' rather than 'end': a half-open socket whose peer vanished never
|
|
42
|
+
// emits 'end', and the supervisor must learn about the death either way.
|
|
43
|
+
this.stream.on('close', () => this.reportClosed());
|
|
44
|
+
}
|
|
45
|
+
/** Feed protocol bytes that were read out of band (demultiplexed stdout). */
|
|
46
|
+
receive(chunk) {
|
|
47
|
+
try {
|
|
48
|
+
this.readBuffer.append(chunk);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// The SDK caps the buffer at 10 MB and throws when a peer keeps sending
|
|
52
|
+
// without ever writing a newline. This runs inside a 'data' handler, so
|
|
53
|
+
// an escaping throw would reach process.on('uncaughtException') and take
|
|
54
|
+
// the entire hub down — every other server with it — because one
|
|
55
|
+
// sandboxed server misbehaved. The stream is desynchronised anyway:
|
|
56
|
+
// report it, end this connection, let the supervisor restart it.
|
|
57
|
+
this.onerror?.(error);
|
|
58
|
+
void this.close();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (;;) {
|
|
62
|
+
let message;
|
|
63
|
+
try {
|
|
64
|
+
message = this.readBuffer.readMessage();
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// One malformed line must not kill the connection: report it and let
|
|
68
|
+
// the buffer continue with the bytes after the newline.
|
|
69
|
+
this.onerror?.(error);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (message === null)
|
|
73
|
+
return;
|
|
74
|
+
this.onmessage?.(message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async send(message) {
|
|
78
|
+
if (this.closed)
|
|
79
|
+
throw new Error('Transport is closed');
|
|
80
|
+
const payload = serializeMessage(message);
|
|
81
|
+
// The write callback fires once the chunk has left the buffer, so awaiting
|
|
82
|
+
// it respects backpressure: a server that stops reading slows us down
|
|
83
|
+
// instead of letting the socket buffer grow without bound.
|
|
84
|
+
await new Promise((resolve, reject) => {
|
|
85
|
+
this.stream.write(payload, error => (error ? reject(error) : resolve()));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async close() {
|
|
89
|
+
this.closed = true;
|
|
90
|
+
this.stream.destroy();
|
|
91
|
+
this.reportClosed();
|
|
92
|
+
}
|
|
93
|
+
reportClosed() {
|
|
94
|
+
if (this.reportedClose)
|
|
95
|
+
return;
|
|
96
|
+
this.reportedClose = true;
|
|
97
|
+
this.closed = true;
|
|
98
|
+
this.readBuffer.clear();
|
|
99
|
+
this.onclose?.();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","sourceRoot":"","sources":["../../src/transports/stream.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ni-c/mcp-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Serve multiple stdio MCP servers from one container: Claude-Code-style mcpServers config, path-based routing, hub meta-tools, and OAuth 2.1 + API tokens for ChatGPT, Claude and any Streamable-HTTP MCP client.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
},
|
|
23
23
|
"homepage": "https://mcp-hub.ni-c.de",
|
|
24
24
|
"engines": {
|
|
25
|
-
"node": ">=
|
|
25
|
+
"node": ">=22"
|
|
26
26
|
},
|
|
27
27
|
"main": "dist/index.js",
|
|
28
28
|
"bin": {
|
|
29
29
|
"mcp-hub": "dist/index.js",
|
|
30
|
-
"mcp-hub-admin": "dist/admin.js"
|
|
30
|
+
"mcp-hub-admin": "dist/admin.js",
|
|
31
|
+
"mcp-hub-docker-proxy": "dist/docker-proxy/index.js"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"dist",
|