@almyty/mcp-mux 1.2.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/README.md +99 -0
- package/dist/create-stdio-mux.d.ts +24 -0
- package/dist/create-stdio-mux.js +40 -0
- package/dist/create-stdio-mux.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/line-reader.d.ts +13 -0
- package/dist/line-reader.js +31 -0
- package/dist/line-reader.js.map +1 -0
- package/dist/mux.d.ts +52 -0
- package/dist/mux.js +180 -0
- package/dist/mux.js.map +1 -0
- package/dist/node-downstream.d.ts +30 -0
- package/dist/node-downstream.js +76 -0
- package/dist/node-downstream.js.map +1 -0
- package/dist/socket-listener.d.ts +16 -0
- package/dist/socket-listener.js +77 -0
- package/dist/socket-listener.js.map +1 -0
- package/dist/supervisor.d.ts +62 -0
- package/dist/supervisor.js +160 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/types.d.ts +74 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @almyty/mcp-mux
|
|
2
|
+
|
|
3
|
+
Southbound stdio-MCP **id-rewrite multiplexer**: fan N client sessions into one
|
|
4
|
+
downstream stdio MCP child over a Unix socket, with safe id remapping, framing
|
|
5
|
+
serialization, response routing, and crash-respawn.
|
|
6
|
+
|
|
7
|
+
> Scope: **southbound MCP only** (almyty as the client/proxy to a downstream MCP
|
|
8
|
+
> server). This package does not touch northbound ACP/A2A serving.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
A stdio MCP server is a single process with one stdin/stdout pair. If several
|
|
13
|
+
sessions write to it concurrently you get interleaved framing and JSON-RPC `id`
|
|
14
|
+
collisions (two clients both pick `id: 1`; responses can't be told apart). This
|
|
15
|
+
package puts the child behind a proxy that:
|
|
16
|
+
|
|
17
|
+
- **rewrites** every client request `id` to a global monotonic proxy id and
|
|
18
|
+
reverse-maps `{proxyId -> (sessionId, originalId)}`, so colliding client ids
|
|
19
|
+
never alias and each response routes home with its original id restored;
|
|
20
|
+
- **serializes** stdin writes through a single async queue, so concurrent
|
|
21
|
+
sessions can never interleave a frame;
|
|
22
|
+
- **routes** each response to its issuing session; load-and-delete on the id-map
|
|
23
|
+
means a duplicate/late response is dropped, never double-routed;
|
|
24
|
+
- **isolates teardown**: closing one session drops only that session's mappings,
|
|
25
|
+
leaving every other session's in-flight requests intact;
|
|
26
|
+
- **survives downstream death**: on child exit, all in-flight requests are
|
|
27
|
+
errored (`-32011`), the id-map is cleared, and the child is respawned with
|
|
28
|
+
exponential backoff (3s → 30s).
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
clients ──unix socket──► SocketListener ──Session──┐
|
|
34
|
+
├──► McpStdioMux ──Downstream──► child stdio MCP
|
|
35
|
+
Supervisor ──spawn/respawn/reap──► NodeDownstream ─┘ (id remap, write queue,
|
|
36
|
+
(FSM + error-path resource ledger) response routing, TTL sweep)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- `McpStdioMux` — pure logic, owns NO process or socket. Fully unit-testable
|
|
40
|
+
with in-process fakes (`Downstream` / `Session` are the test seams).
|
|
41
|
+
- `Supervisor` — owns the child lifecycle + respawn/teardown FSM, and enforces
|
|
42
|
+
the **error-path resource-ownership invariant** (see below).
|
|
43
|
+
- `NodeDownstream` / `SocketListener` — the only files that touch
|
|
44
|
+
`child_process` / `net`.
|
|
45
|
+
|
|
46
|
+
### The FD-leak invariant
|
|
47
|
+
|
|
48
|
+
The reference implementation this was specced against leaked a log fd: `Start()`
|
|
49
|
+
opened it, then errored *before* registering for `Stop()`, so the fd was never
|
|
50
|
+
closed — repeated failing attaches drained the fd budget over hours.
|
|
51
|
+
|
|
52
|
+
`Supervisor.start()` pre-empts that bug class with an **acquire ledger**: every
|
|
53
|
+
resource acquired is pushed onto a release list *before* the next acquisition.
|
|
54
|
+
If anything throws before the ownership-transfer point, the ledger is unwound in
|
|
55
|
+
reverse. The `FD-LEAK HAMMER` test runs 500 failing starts and asserts the net
|
|
56
|
+
open-handle count returns to zero.
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createStdioMux } from '@almyty/mcp-mux';
|
|
62
|
+
|
|
63
|
+
const handle = await createStdioMux({
|
|
64
|
+
socketPath: '/tmp/my-mcp.sock',
|
|
65
|
+
downstream: { command: 'npx', args: ['-y', 'some-mcp-server'] },
|
|
66
|
+
supervisor: { backoffBaseMs: 3000, backoffCapMs: 30000 },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ... clients connect to the socket and speak newline-delimited JSON-RPC ...
|
|
70
|
+
|
|
71
|
+
await handle.close(); // stop accepting, reap child, release resources
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Design note
|
|
75
|
+
|
|
76
|
+
Full design — id-map lifecycle, framing serialization, response routing, the
|
|
77
|
+
respawn/teardown state machine, and error-path resource ownership — lives in
|
|
78
|
+
[`DESIGN.md`](./DESIGN.md).
|
|
79
|
+
|
|
80
|
+
## Develop
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install
|
|
84
|
+
npm run typecheck # tsc --noEmit (includes tests)
|
|
85
|
+
npm test # vitest run
|
|
86
|
+
npm run build # emits dist/ (tests excluded)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## About almyty
|
|
90
|
+
|
|
91
|
+
almyty is the full-stack platform for AI agents, agnostic by design: any LLM, any
|
|
92
|
+
API turned into tools, served over MCP, A2A, UTCP, and Agent Skills. Open source,
|
|
93
|
+
no lock-in.
|
|
94
|
+
|
|
95
|
+
- Website — https://almyty.com
|
|
96
|
+
- Docs — https://docs.almyty.com
|
|
97
|
+
- Source — https://github.com/almyty-inc/almyty
|
|
98
|
+
|
|
99
|
+
Apache-2.0 © Almyty Inc.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createStdioMux — convenience wiring for the common case: serve one stdio MCP
|
|
3
|
+
* child to many clients over a Unix socket. Builds the McpStdioMux, a
|
|
4
|
+
* NodeDownstreamFactory + Supervisor (which owns spawn/respawn), and a
|
|
5
|
+
* SocketListener, then starts both.
|
|
6
|
+
*
|
|
7
|
+
* Returns a handle whose `close()` tears everything down in dependency order:
|
|
8
|
+
* stop accepting connections, stop the supervisor (reap child + release log),
|
|
9
|
+
* then close the mux. Southbound only — this never touches northbound serving.
|
|
10
|
+
*/
|
|
11
|
+
import { McpStdioMux } from './mux.js';
|
|
12
|
+
import { Supervisor, type SupervisorOptions } from './supervisor.js';
|
|
13
|
+
import { type NodeDownstreamSpec } from './node-downstream.js';
|
|
14
|
+
export interface StdioMuxConfig {
|
|
15
|
+
socketPath: string;
|
|
16
|
+
downstream: NodeDownstreamSpec;
|
|
17
|
+
supervisor?: SupervisorOptions;
|
|
18
|
+
}
|
|
19
|
+
export interface StdioMuxHandle {
|
|
20
|
+
readonly mux: McpStdioMux;
|
|
21
|
+
readonly supervisor: Supervisor;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare function createStdioMux(config: StdioMuxConfig): Promise<StdioMuxHandle>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createStdioMux — convenience wiring for the common case: serve one stdio MCP
|
|
3
|
+
* child to many clients over a Unix socket. Builds the McpStdioMux, a
|
|
4
|
+
* NodeDownstreamFactory + Supervisor (which owns spawn/respawn), and a
|
|
5
|
+
* SocketListener, then starts both.
|
|
6
|
+
*
|
|
7
|
+
* Returns a handle whose `close()` tears everything down in dependency order:
|
|
8
|
+
* stop accepting connections, stop the supervisor (reap child + release log),
|
|
9
|
+
* then close the mux. Southbound only — this never touches northbound serving.
|
|
10
|
+
*/
|
|
11
|
+
import { McpStdioMux } from './mux.js';
|
|
12
|
+
import { Supervisor } from './supervisor.js';
|
|
13
|
+
import { NodeDownstreamFactory } from './node-downstream.js';
|
|
14
|
+
import { SocketListener } from './socket-listener.js';
|
|
15
|
+
export async function createStdioMux(config) {
|
|
16
|
+
const mux = new McpStdioMux(config.supervisor);
|
|
17
|
+
const factory = new NodeDownstreamFactory(config.downstream);
|
|
18
|
+
const supervisor = new Supervisor(factory, mux, config.supervisor);
|
|
19
|
+
const listener = new SocketListener(mux, { socketPath: config.socketPath });
|
|
20
|
+
await supervisor.start(); // spawn the child first so early clients have a target
|
|
21
|
+
try {
|
|
22
|
+
await listener.listen();
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
// Listener failed to bind — don't leak the running child.
|
|
26
|
+
await supervisor.stop();
|
|
27
|
+
mux.close();
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
mux,
|
|
32
|
+
supervisor,
|
|
33
|
+
async close() {
|
|
34
|
+
await listener.close();
|
|
35
|
+
await supervisor.stop();
|
|
36
|
+
mux.close();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=create-stdio-mux.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-stdio-mux.js","sourceRoot":"","sources":["../src/create-stdio-mux.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,UAAU,EAA0B,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAA2B,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AActD,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,MAAsB;IACzD,MAAM,GAAG,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,qBAAqB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAC7D,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IACnE,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IAE5E,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC,CAAC,uDAAuD;IACjF,IAAI,CAAC;QACH,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;IAC1B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,0DAA0D;QAC1D,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;QACxB,GAAG,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,CAAC,CAAC;IACV,CAAC;IAED,OAAO;QACL,GAAG;QACH,UAAU;QACV,KAAK,CAAC,KAAK;YACT,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;YACvB,MAAM,UAAU,CAAC,IAAI,EAAE,CAAC;YACxB,GAAG,CAAC,KAAK,EAAE,CAAC;QACd,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { McpStdioMux } from './mux.js';
|
|
2
|
+
export { Supervisor } from './supervisor.js';
|
|
3
|
+
export type { SupervisorOptions, SupervisorState, Closable } from './supervisor.js';
|
|
4
|
+
export { LineReader } from './line-reader.js';
|
|
5
|
+
export { NodeDownstream, NodeDownstreamFactory } from './node-downstream.js';
|
|
6
|
+
export type { NodeDownstreamSpec } from './node-downstream.js';
|
|
7
|
+
export { SocketListener } from './socket-listener.js';
|
|
8
|
+
export type { SocketListenerOptions } from './socket-listener.js';
|
|
9
|
+
export { createStdioMux } from './create-stdio-mux.js';
|
|
10
|
+
export type { StdioMuxConfig, StdioMuxHandle } from './create-stdio-mux.js';
|
|
11
|
+
export { RPC } from './types.js';
|
|
12
|
+
export type { Downstream, DownstreamFactory, DownstreamExit, Session, JsonRpcFrame, JsonRpcId, IdMapping, MuxOptions, } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { McpStdioMux } from './mux.js';
|
|
2
|
+
export { Supervisor } from './supervisor.js';
|
|
3
|
+
export { LineReader } from './line-reader.js';
|
|
4
|
+
export { NodeDownstream, NodeDownstreamFactory } from './node-downstream.js';
|
|
5
|
+
export { SocketListener } from './socket-listener.js';
|
|
6
|
+
export { createStdioMux } from './create-stdio-mux.js';
|
|
7
|
+
export { RPC } from './types.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited frame reader. Downstream stdout arrives in arbitrary
|
|
3
|
+
* chunks; a frame may span chunks or several frames may share one chunk.
|
|
4
|
+
* NEVER JSON.parse a raw chunk — buffer until '\n'.
|
|
5
|
+
*/
|
|
6
|
+
export declare class LineReader {
|
|
7
|
+
private readonly onLine;
|
|
8
|
+
private buf;
|
|
9
|
+
constructor(onLine: (line: string) => void);
|
|
10
|
+
push(chunk: string | Buffer): void;
|
|
11
|
+
/** Any trailing bytes with no terminating newline (e.g. at EOF). */
|
|
12
|
+
flushRemainder(): string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited frame reader. Downstream stdout arrives in arbitrary
|
|
3
|
+
* chunks; a frame may span chunks or several frames may share one chunk.
|
|
4
|
+
* NEVER JSON.parse a raw chunk — buffer until '\n'.
|
|
5
|
+
*/
|
|
6
|
+
export class LineReader {
|
|
7
|
+
onLine;
|
|
8
|
+
buf = '';
|
|
9
|
+
constructor(onLine) {
|
|
10
|
+
this.onLine = onLine;
|
|
11
|
+
}
|
|
12
|
+
push(chunk) {
|
|
13
|
+
this.buf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
14
|
+
let nl;
|
|
15
|
+
while ((nl = this.buf.indexOf('\n')) !== -1) {
|
|
16
|
+
const line = this.buf.slice(0, nl);
|
|
17
|
+
this.buf = this.buf.slice(nl + 1);
|
|
18
|
+
// Tolerate \r\n and skip blank keep-alive lines.
|
|
19
|
+
const trimmed = line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
20
|
+
if (trimmed.length > 0)
|
|
21
|
+
this.onLine(trimmed);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Any trailing bytes with no terminating newline (e.g. at EOF). */
|
|
25
|
+
flushRemainder() {
|
|
26
|
+
const rem = this.buf;
|
|
27
|
+
this.buf = '';
|
|
28
|
+
return rem;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=line-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"line-reader.js","sourceRoot":"","sources":["../src/line-reader.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,UAAU;IAGQ;IAFrB,GAAG,GAAG,EAAE,CAAC;IAEjB,YAA6B,MAA8B;QAA9B,WAAM,GAAN,MAAM,CAAwB;IAAG,CAAC;IAE/D,IAAI,CAAC,KAAsB;QACzB,IAAI,CAAC,GAAG,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACvE,IAAI,EAAU,CAAC;QACf,OAAO,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACnC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YAClC,iDAAiD;YACjD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAC/D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;gBAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,oEAAoE;IACpE,cAAc;QACZ,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,EAAE,CAAC;QACd,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
|
package/dist/mux.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpStdioMux — the multiplexer proper. Fans N sessions into ONE downstream
|
|
3
|
+
* stdio MCP child:
|
|
4
|
+
* - rewrites each client request `id` to a global monotonic proxy id and
|
|
5
|
+
* reverse-maps {proxyId -> (sessionId, originalId)} so responses route back;
|
|
6
|
+
* - serializes stdin frames (write queue) so concurrent sessions can't
|
|
7
|
+
* interleave framing;
|
|
8
|
+
* - restores the original id on the way back and routes to the issuing session;
|
|
9
|
+
* - on downstream loss, errors every in-flight request and clears the map;
|
|
10
|
+
* - per-session teardown removes only that session's mappings.
|
|
11
|
+
*
|
|
12
|
+
* It owns NO process and NO socket — the supervisor hands it a Downstream and
|
|
13
|
+
* the listener hands it Sessions. That keeps it fully unit-testable with fakes.
|
|
14
|
+
*
|
|
15
|
+
* Direction scope (v1): client->downstream requests/notifications and
|
|
16
|
+
* downstream->client responses are fully multiplexed. downstream->client
|
|
17
|
+
* messages that carry a `method` (server-initiated notifications/requests, e.g.
|
|
18
|
+
* progress, sampling) are BROADCAST to all sessions — server-initiated requests
|
|
19
|
+
* cannot be cleanly fanned to one client, so they are surfaced, not routed.
|
|
20
|
+
*/
|
|
21
|
+
import type { Downstream, Session, MuxOptions } from './types.js';
|
|
22
|
+
export declare class McpStdioMux {
|
|
23
|
+
private readonly idMap;
|
|
24
|
+
private proxyIdSeq;
|
|
25
|
+
private readonly sessions;
|
|
26
|
+
private downstream;
|
|
27
|
+
private writeChain;
|
|
28
|
+
private sweepTimer;
|
|
29
|
+
private closed;
|
|
30
|
+
private readonly ttlMs;
|
|
31
|
+
private readonly warn;
|
|
32
|
+
constructor(opts?: MuxOptions);
|
|
33
|
+
/** In-flight request count (test/observability). */
|
|
34
|
+
get inFlight(): number;
|
|
35
|
+
get sessionCount(): number;
|
|
36
|
+
setDownstream(downstream: Downstream): void;
|
|
37
|
+
/** Called by the supervisor when the child is lost; errors all in-flight. */
|
|
38
|
+
onDownstreamGone(reason: string): void;
|
|
39
|
+
addSession(session: Session): void;
|
|
40
|
+
/** Per-session teardown: drop ONLY this session's mappings; never the child. */
|
|
41
|
+
teardownSession(sessionId: string): void;
|
|
42
|
+
private onClientFrame;
|
|
43
|
+
private nextProxyId;
|
|
44
|
+
/** Serialize frames to the downstream; resolves after the frame is flushed. */
|
|
45
|
+
private enqueueFrame;
|
|
46
|
+
private onDownstreamLine;
|
|
47
|
+
private normalizeId;
|
|
48
|
+
private sweep;
|
|
49
|
+
private errorSession;
|
|
50
|
+
/** Stop sweeping + forget everything. Does not touch the downstream/sessions' sockets. */
|
|
51
|
+
close(): void;
|
|
52
|
+
}
|
package/dist/mux.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { RPC } from './types.js';
|
|
2
|
+
const DEFAULT_TTL_MS = 300_000;
|
|
3
|
+
const DEFAULT_SWEEP_MS = 30_000;
|
|
4
|
+
export class McpStdioMux {
|
|
5
|
+
idMap = new Map();
|
|
6
|
+
proxyIdSeq = 0; // monotonic across the mux's life (incl. respawns)
|
|
7
|
+
sessions = new Map();
|
|
8
|
+
downstream = null;
|
|
9
|
+
writeChain = Promise.resolve();
|
|
10
|
+
sweepTimer = null;
|
|
11
|
+
closed = false;
|
|
12
|
+
ttlMs;
|
|
13
|
+
warn;
|
|
14
|
+
constructor(opts = {}) {
|
|
15
|
+
this.ttlMs = opts.requestTtlMs ?? DEFAULT_TTL_MS;
|
|
16
|
+
this.warn = opts.warn ?? ((m) => process.stderr.write(`[mcp-mux] ${m}\n`));
|
|
17
|
+
const sweepMs = opts.sweepIntervalMs ?? DEFAULT_SWEEP_MS;
|
|
18
|
+
this.sweepTimer = setInterval(() => this.sweep(), sweepMs);
|
|
19
|
+
this.sweepTimer.unref?.();
|
|
20
|
+
}
|
|
21
|
+
/** In-flight request count (test/observability). */
|
|
22
|
+
get inFlight() {
|
|
23
|
+
return this.idMap.size;
|
|
24
|
+
}
|
|
25
|
+
get sessionCount() {
|
|
26
|
+
return this.sessions.size;
|
|
27
|
+
}
|
|
28
|
+
// ── downstream wiring (called by the supervisor on spawn/respawn) ──
|
|
29
|
+
setDownstream(downstream) {
|
|
30
|
+
this.downstream = downstream;
|
|
31
|
+
downstream.on('line', (line) => this.onDownstreamLine(line));
|
|
32
|
+
downstream.once('exit', () => this.onDownstreamGone('downstream exited'));
|
|
33
|
+
downstream.on('error', (e) => this.warn(`downstream error: ${e?.message ?? e}`));
|
|
34
|
+
}
|
|
35
|
+
/** Called by the supervisor when the child is lost; errors all in-flight. */
|
|
36
|
+
onDownstreamGone(reason) {
|
|
37
|
+
this.downstream = null;
|
|
38
|
+
const affected = new Set();
|
|
39
|
+
for (const m of this.idMap.values())
|
|
40
|
+
affected.add(m.sessionId);
|
|
41
|
+
this.idMap.clear();
|
|
42
|
+
for (const sid of affected) {
|
|
43
|
+
this.errorSession(sid, null, RPC.DOWNSTREAM_GONE, `downstream unavailable: ${reason}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ── session wiring (called by the listener) ──
|
|
47
|
+
addSession(session) {
|
|
48
|
+
this.sessions.set(session.id, session);
|
|
49
|
+
session.on('frame', (frame) => this.onClientFrame(session.id, frame));
|
|
50
|
+
session.once('close', () => this.teardownSession(session.id));
|
|
51
|
+
}
|
|
52
|
+
/** Per-session teardown: drop ONLY this session's mappings; never the child. */
|
|
53
|
+
teardownSession(sessionId) {
|
|
54
|
+
for (const [proxyId, m] of this.idMap) {
|
|
55
|
+
if (m.sessionId === sessionId)
|
|
56
|
+
this.idMap.delete(proxyId);
|
|
57
|
+
}
|
|
58
|
+
this.sessions.delete(sessionId);
|
|
59
|
+
}
|
|
60
|
+
// ── client -> downstream ──
|
|
61
|
+
onClientFrame(sessionId, raw) {
|
|
62
|
+
if (this.closed)
|
|
63
|
+
return;
|
|
64
|
+
let frame;
|
|
65
|
+
try {
|
|
66
|
+
frame = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
this.errorSession(sessionId, null, RPC.PARSE_ERROR, 'invalid JSON frame');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Notification (request without an id): forward verbatim, allocate nothing —
|
|
73
|
+
// a mapping for something that never gets a response would leak forever.
|
|
74
|
+
if (frame.id === undefined || frame.id === null) {
|
|
75
|
+
void this.enqueueFrame(raw);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const proxyId = this.nextProxyId();
|
|
79
|
+
this.idMap.set(proxyId, { sessionId, originalId: frame.id, sentAt: Date.now() });
|
|
80
|
+
frame.id = proxyId;
|
|
81
|
+
void this.enqueueFrame(JSON.stringify(frame)).catch(() => {
|
|
82
|
+
// Write failed (downstream gone / EPIPE): free the mapping and tell the
|
|
83
|
+
// client now rather than leaving it hung until the TTL sweep.
|
|
84
|
+
if (this.idMap.delete(proxyId)) {
|
|
85
|
+
this.errorSession(sessionId, frame.id, RPC.DOWNSTREAM_GONE, 'downstream write failed');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
nextProxyId() {
|
|
90
|
+
if (this.proxyIdSeq >= Number.MAX_SAFE_INTEGER) {
|
|
91
|
+
// ~9e15 ids into a single process life — practically unreachable, but a
|
|
92
|
+
// wrap would alias live mappings, so refuse loudly instead.
|
|
93
|
+
throw new Error('mcp-mux: proxy id space exhausted');
|
|
94
|
+
}
|
|
95
|
+
return ++this.proxyIdSeq;
|
|
96
|
+
}
|
|
97
|
+
/** Serialize frames to the downstream; resolves after the frame is flushed. */
|
|
98
|
+
enqueueFrame(frame) {
|
|
99
|
+
const run = async () => {
|
|
100
|
+
const ds = this.downstream;
|
|
101
|
+
if (!ds)
|
|
102
|
+
throw new Error('no downstream');
|
|
103
|
+
await ds.write(frame);
|
|
104
|
+
};
|
|
105
|
+
// Chain so frames never interleave; isolate failures so the chain survives.
|
|
106
|
+
this.writeChain = this.writeChain.then(run, run);
|
|
107
|
+
return this.writeChain;
|
|
108
|
+
}
|
|
109
|
+
// ── downstream -> client ──
|
|
110
|
+
onDownstreamLine(line) {
|
|
111
|
+
let frame;
|
|
112
|
+
try {
|
|
113
|
+
frame = JSON.parse(line);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
this.warn(`unparseable downstream line dropped: ${line.slice(0, 120)}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Server-initiated (has a method): notification or request. Can't be routed
|
|
120
|
+
// to one session — broadcast and move on.
|
|
121
|
+
if (typeof frame.method === 'string') {
|
|
122
|
+
for (const s of this.sessions.values())
|
|
123
|
+
s.send(line);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Otherwise it's a response: route by the proxy id.
|
|
127
|
+
const proxyId = this.normalizeId(frame.id);
|
|
128
|
+
if (proxyId === null) {
|
|
129
|
+
this.warn('downstream response with non-numeric id dropped');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const mapping = this.idMap.get(proxyId);
|
|
133
|
+
if (!mapping) {
|
|
134
|
+
// Unknown or already-freed (duplicate/late) — drop, never double-route.
|
|
135
|
+
this.warn(`downstream response for unknown id ${proxyId} dropped`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
this.idMap.delete(proxyId); // load-and-delete: a second response can't re-route
|
|
139
|
+
frame.id = mapping.originalId; // restore the client's original id
|
|
140
|
+
const session = this.sessions.get(mapping.sessionId);
|
|
141
|
+
if (session)
|
|
142
|
+
session.send(JSON.stringify(frame));
|
|
143
|
+
// If the session is already gone, silently drop — its mappings were cleared
|
|
144
|
+
// at teardown; this guards the late-arrival race.
|
|
145
|
+
}
|
|
146
|
+
normalizeId(id) {
|
|
147
|
+
if (typeof id === 'number' && Number.isSafeInteger(id))
|
|
148
|
+
return id;
|
|
149
|
+
if (typeof id === 'string' && /^\d+$/.test(id))
|
|
150
|
+
return Number(id); // tolerant of stringified ids
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
// ── TTL sweep ──
|
|
154
|
+
sweep() {
|
|
155
|
+
const cutoff = Date.now() - this.ttlMs;
|
|
156
|
+
for (const [proxyId, m] of this.idMap) {
|
|
157
|
+
if (m.sentAt <= cutoff) {
|
|
158
|
+
this.idMap.delete(proxyId);
|
|
159
|
+
this.errorSession(m.sessionId, m.originalId, RPC.DOWNSTREAM_TIMEOUT, 'downstream did not respond in time');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
errorSession(sessionId, id, code, message) {
|
|
164
|
+
const session = this.sessions.get(sessionId);
|
|
165
|
+
if (!session)
|
|
166
|
+
return;
|
|
167
|
+
session.send(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }));
|
|
168
|
+
}
|
|
169
|
+
/** Stop sweeping + forget everything. Does not touch the downstream/sessions' sockets. */
|
|
170
|
+
close() {
|
|
171
|
+
this.closed = true;
|
|
172
|
+
if (this.sweepTimer)
|
|
173
|
+
clearInterval(this.sweepTimer);
|
|
174
|
+
this.sweepTimer = null;
|
|
175
|
+
this.idMap.clear();
|
|
176
|
+
this.sessions.clear();
|
|
177
|
+
this.downstream = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=mux.js.map
|
package/dist/mux.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mux.js","sourceRoot":"","sources":["../src/mux.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { Downstream, DownstreamFactory } from './types.js';
|
|
3
|
+
export interface NodeDownstreamSpec {
|
|
4
|
+
command: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
cwd?: string;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
/** Where to forward the child's stderr. Default: inherit to this process' stderr. */
|
|
9
|
+
stderr?: 'inherit' | 'ignore' | ((chunk: Buffer) => void);
|
|
10
|
+
}
|
|
11
|
+
export declare class NodeDownstream extends EventEmitter implements Downstream {
|
|
12
|
+
private readonly child;
|
|
13
|
+
private readonly reader;
|
|
14
|
+
private exited;
|
|
15
|
+
constructor(spec: NodeDownstreamSpec);
|
|
16
|
+
get pid(): number | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Write one frame + newline. Resolves once the data is accepted (or flushed
|
|
19
|
+
* past a full buffer). The mux awaits this to serialize framing, so a write
|
|
20
|
+
* that can't complete must reject rather than silently drop.
|
|
21
|
+
*/
|
|
22
|
+
write(frame: string): Promise<void>;
|
|
23
|
+
kill(signal?: NodeJS.Signals): void;
|
|
24
|
+
}
|
|
25
|
+
/** Factory the Supervisor uses to (re)spawn the child on demand. */
|
|
26
|
+
export declare class NodeDownstreamFactory implements DownstreamFactory {
|
|
27
|
+
private readonly spec;
|
|
28
|
+
constructor(spec: NodeDownstreamSpec);
|
|
29
|
+
spawn(): Promise<Downstream>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodeDownstream — the real child_process implementation of the `Downstream`
|
|
3
|
+
* seam. Spawns one stdio MCP server, parses its stdout into newline-delimited
|
|
4
|
+
* JSON frames (via LineReader, so partial lines never reach the mux), and
|
|
5
|
+
* serializes writes to stdin with back-pressure-aware flushing.
|
|
6
|
+
*
|
|
7
|
+
* This is the ONLY file in the package that touches child_process; everything
|
|
8
|
+
* else is driven through the `Downstream` interface so it stays unit-testable.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { EventEmitter } from 'node:events';
|
|
12
|
+
import { LineReader } from './line-reader.js';
|
|
13
|
+
export class NodeDownstream extends EventEmitter {
|
|
14
|
+
child;
|
|
15
|
+
reader = new LineReader((line) => this.emit('line', line));
|
|
16
|
+
exited = false;
|
|
17
|
+
constructor(spec) {
|
|
18
|
+
super();
|
|
19
|
+
const stderrMode = spec.stderr === undefined ? 'inherit' : spec.stderr;
|
|
20
|
+
this.child = spawn(spec.command, spec.args ?? [], {
|
|
21
|
+
cwd: spec.cwd,
|
|
22
|
+
env: spec.env,
|
|
23
|
+
stdio: ['pipe', 'pipe', typeof stderrMode === 'function' ? 'pipe' : stderrMode],
|
|
24
|
+
});
|
|
25
|
+
this.child.stdout.on('data', (chunk) => this.reader.push(chunk));
|
|
26
|
+
if (typeof stderrMode === 'function' && this.child.stderr) {
|
|
27
|
+
this.child.stderr.on('data', stderrMode);
|
|
28
|
+
}
|
|
29
|
+
this.child.on('error', (err) => this.emit('error', err));
|
|
30
|
+
this.child.once('exit', (code, signal) => {
|
|
31
|
+
this.exited = true;
|
|
32
|
+
const tail = this.reader.flushRemainder();
|
|
33
|
+
if (tail)
|
|
34
|
+
this.emit('line', tail);
|
|
35
|
+
this.emit('exit', { code, signal });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
get pid() {
|
|
39
|
+
return this.child.pid;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Write one frame + newline. Resolves once the data is accepted (or flushed
|
|
43
|
+
* past a full buffer). The mux awaits this to serialize framing, so a write
|
|
44
|
+
* that can't complete must reject rather than silently drop.
|
|
45
|
+
*/
|
|
46
|
+
write(frame) {
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
if (this.exited || !this.child.stdin.writable) {
|
|
49
|
+
reject(new Error('downstream stdin not writable'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.child.stdin.write(frame + '\n', (err) => (err ? reject(err) : resolve()));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
kill(signal = 'SIGTERM') {
|
|
56
|
+
if (this.exited)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
this.child.kill(signal);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* already gone */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Factory the Supervisor uses to (re)spawn the child on demand. */
|
|
67
|
+
export class NodeDownstreamFactory {
|
|
68
|
+
spec;
|
|
69
|
+
constructor(spec) {
|
|
70
|
+
this.spec = spec;
|
|
71
|
+
}
|
|
72
|
+
async spawn() {
|
|
73
|
+
return new NodeDownstream(this.spec);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=node-downstream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node-downstream.js","sourceRoot":"","sources":["../src/node-downstream.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { McpStdioMux } from './mux.js';
|
|
2
|
+
export interface SocketListenerOptions {
|
|
3
|
+
/** Filesystem path for the Unix domain socket. */
|
|
4
|
+
socketPath: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class SocketListener {
|
|
7
|
+
private readonly mux;
|
|
8
|
+
private readonly opts;
|
|
9
|
+
private readonly server;
|
|
10
|
+
private seq;
|
|
11
|
+
constructor(mux: McpStdioMux, opts: SocketListenerOptions);
|
|
12
|
+
/** Register the connection with the mux SYNCHRONOUSLY — no await before this. */
|
|
13
|
+
private onConnection;
|
|
14
|
+
listen(): Promise<void>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocketListener — the real Unix-socket implementation of the inbound side.
|
|
3
|
+
* Each accepted connection becomes one `Session`: its stream is framed by a
|
|
4
|
+
* LineReader (partial lines never reach the mux), `send` writes frame+newline,
|
|
5
|
+
* and `close` destroys the socket. The listener registers each session with the
|
|
6
|
+
* mux; the mux tears the session down on the socket's 'close' event.
|
|
7
|
+
*
|
|
8
|
+
* One process leak class to avoid here too: a socket accepted but never handed
|
|
9
|
+
* to the mux would leak. We register synchronously on 'connection', before any
|
|
10
|
+
* await, so every accepted fd has an owner.
|
|
11
|
+
*/
|
|
12
|
+
import { createServer } from 'node:net';
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
import { LineReader } from './line-reader.js';
|
|
15
|
+
class SocketSession extends EventEmitter {
|
|
16
|
+
id;
|
|
17
|
+
socket;
|
|
18
|
+
reader;
|
|
19
|
+
closed = false;
|
|
20
|
+
constructor(id, socket) {
|
|
21
|
+
super();
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.socket = socket;
|
|
24
|
+
this.reader = new LineReader((line) => this.emit('frame', line));
|
|
25
|
+
socket.setEncoding('utf8');
|
|
26
|
+
socket.on('data', (chunk) => this.reader.push(chunk));
|
|
27
|
+
socket.once('close', () => this.markClosed());
|
|
28
|
+
socket.on('error', () => this.markClosed());
|
|
29
|
+
}
|
|
30
|
+
send(frame) {
|
|
31
|
+
if (this.closed || !this.socket.writable)
|
|
32
|
+
return;
|
|
33
|
+
this.socket.write(frame + '\n');
|
|
34
|
+
}
|
|
35
|
+
close() {
|
|
36
|
+
if (this.closed)
|
|
37
|
+
return;
|
|
38
|
+
this.socket.destroy();
|
|
39
|
+
// 'close' will fire markClosed(); guard in case destroy is synchronous.
|
|
40
|
+
this.markClosed();
|
|
41
|
+
}
|
|
42
|
+
markClosed() {
|
|
43
|
+
if (this.closed)
|
|
44
|
+
return;
|
|
45
|
+
this.closed = true;
|
|
46
|
+
this.emit('close');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export class SocketListener {
|
|
50
|
+
mux;
|
|
51
|
+
opts;
|
|
52
|
+
server;
|
|
53
|
+
seq = 0;
|
|
54
|
+
constructor(mux, opts) {
|
|
55
|
+
this.mux = mux;
|
|
56
|
+
this.opts = opts;
|
|
57
|
+
this.server = createServer((socket) => this.onConnection(socket));
|
|
58
|
+
}
|
|
59
|
+
/** Register the connection with the mux SYNCHRONOUSLY — no await before this. */
|
|
60
|
+
onConnection(socket) {
|
|
61
|
+
const session = new SocketSession(`s${++this.seq}`, socket);
|
|
62
|
+
this.mux.addSession(session); // mux wires 'frame'/'close' and owns teardown
|
|
63
|
+
}
|
|
64
|
+
listen() {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
this.server.once('error', reject);
|
|
67
|
+
this.server.listen(this.opts.socketPath, () => {
|
|
68
|
+
this.server.removeListener('error', reject);
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
close() {
|
|
74
|
+
return new Promise((resolve) => this.server.close(() => resolve()));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=socket-listener.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-listener.js","sourceRoot":"","sources":["../src/socket-listener.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervisor — owns the downstream child's lifecycle and the respawn/teardown
|
|
3
|
+
* state machine, and enforces the error-path resource-ownership invariant that
|
|
4
|
+
* pre-empts the reference's FD leak.
|
|
5
|
+
*
|
|
6
|
+
* idle ──start()ok──► running ──exit/error──► failed ──reap+sweep──► respawning ──backoff──► (start again)
|
|
7
|
+
* ▲ │
|
|
8
|
+
* └──── stop() ────────┴──────────────────────────────────────────► stopped (terminal)
|
|
9
|
+
*
|
|
10
|
+
* THE INVARIANT (their bug class): every resource acquired in start() is pushed
|
|
11
|
+
* onto an `acquired` ledger BEFORE the next acquisition. If anything throws
|
|
12
|
+
* before ownership is transferred (registerForStop), the ledger is unwound in
|
|
13
|
+
* reverse — so a half-built start can never leak a log fd / socket / child.
|
|
14
|
+
* Their leak: Start() opened a log fd, then errored before registering for
|
|
15
|
+
* Stop(); the fd was never closed; repeated failing attaches drained the budget.
|
|
16
|
+
*/
|
|
17
|
+
import type { DownstreamFactory, MuxOptions } from './types.js';
|
|
18
|
+
import type { McpStdioMux } from './mux.js';
|
|
19
|
+
export interface Closable {
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
export type SupervisorState = 'idle' | 'running' | 'failed' | 'respawning' | 'stopped';
|
|
23
|
+
export interface SupervisorOptions extends MuxOptions {
|
|
24
|
+
/** Acquire the leak-prone pre-spawn resource (e.g. a log fd). Optional. */
|
|
25
|
+
openLog?: () => Closable;
|
|
26
|
+
backoffBaseMs?: number;
|
|
27
|
+
backoffCapMs?: number;
|
|
28
|
+
/** A run lasting at least this long resets the backoff. */
|
|
29
|
+
backoffResetAfterMs?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class Supervisor {
|
|
32
|
+
private readonly factory;
|
|
33
|
+
private readonly mux;
|
|
34
|
+
private readonly opts;
|
|
35
|
+
private state;
|
|
36
|
+
private downstream;
|
|
37
|
+
private log;
|
|
38
|
+
private childExited;
|
|
39
|
+
private reaped;
|
|
40
|
+
private respawnTimer;
|
|
41
|
+
private backoffMs;
|
|
42
|
+
private runStartedAt;
|
|
43
|
+
private readonly warn;
|
|
44
|
+
private readonly backoffBaseMs;
|
|
45
|
+
private readonly backoffCapMs;
|
|
46
|
+
private readonly backoffResetAfterMs;
|
|
47
|
+
constructor(factory: DownstreamFactory, mux: McpStdioMux, opts?: SupervisorOptions);
|
|
48
|
+
get current(): SupervisorState;
|
|
49
|
+
/**
|
|
50
|
+
* Acquire resources + spawn the downstream. The ledger guarantees that any
|
|
51
|
+
* failure before ownership transfer releases everything acquired so far.
|
|
52
|
+
*/
|
|
53
|
+
start(): Promise<void>;
|
|
54
|
+
/** Child exited unexpectedly (or via our kill during reap). */
|
|
55
|
+
private onChildExit;
|
|
56
|
+
/** Kill (if alive) + mark reaped EXACTLY once — pre-empts double-kill/zombie. */
|
|
57
|
+
private reap;
|
|
58
|
+
private releaseLog;
|
|
59
|
+
private scheduleRespawn;
|
|
60
|
+
/** Terminal shutdown. Idempotent. Cancels respawn, reaps, releases resources. */
|
|
61
|
+
stop(): Promise<void>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export class Supervisor {
|
|
2
|
+
factory;
|
|
3
|
+
mux;
|
|
4
|
+
opts;
|
|
5
|
+
state = 'idle';
|
|
6
|
+
downstream = null;
|
|
7
|
+
log = null;
|
|
8
|
+
childExited = false;
|
|
9
|
+
reaped = false;
|
|
10
|
+
respawnTimer = null;
|
|
11
|
+
backoffMs;
|
|
12
|
+
runStartedAt = 0;
|
|
13
|
+
warn;
|
|
14
|
+
backoffBaseMs;
|
|
15
|
+
backoffCapMs;
|
|
16
|
+
backoffResetAfterMs;
|
|
17
|
+
constructor(factory, mux, opts = {}) {
|
|
18
|
+
this.factory = factory;
|
|
19
|
+
this.mux = mux;
|
|
20
|
+
this.opts = opts;
|
|
21
|
+
this.warn = opts.warn ?? ((m) => process.stderr.write(`[mcp-mux:sup] ${m}\n`));
|
|
22
|
+
this.backoffBaseMs = opts.backoffBaseMs ?? 3_000;
|
|
23
|
+
this.backoffCapMs = opts.backoffCapMs ?? 30_000;
|
|
24
|
+
this.backoffResetAfterMs = opts.backoffResetAfterMs ?? 30_000;
|
|
25
|
+
this.backoffMs = this.backoffBaseMs;
|
|
26
|
+
}
|
|
27
|
+
get current() {
|
|
28
|
+
return this.state;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Acquire resources + spawn the downstream. The ledger guarantees that any
|
|
32
|
+
* failure before ownership transfer releases everything acquired so far.
|
|
33
|
+
*/
|
|
34
|
+
async start() {
|
|
35
|
+
if (this.state === 'stopped')
|
|
36
|
+
throw new Error('supervisor stopped');
|
|
37
|
+
const acquired = [];
|
|
38
|
+
const unwind = () => {
|
|
39
|
+
while (acquired.length) {
|
|
40
|
+
try {
|
|
41
|
+
acquired.pop()();
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
this.warn(`cleanup error: ${e?.message ?? e}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
// (1) leak-prone pre-spawn resource
|
|
50
|
+
if (this.opts.openLog) {
|
|
51
|
+
const log = this.opts.openLog();
|
|
52
|
+
acquired.push(() => log.close());
|
|
53
|
+
this.log = log;
|
|
54
|
+
}
|
|
55
|
+
// (2) the child + its pipes
|
|
56
|
+
const ds = await this.factory.spawn();
|
|
57
|
+
acquired.push(() => {
|
|
58
|
+
try {
|
|
59
|
+
ds.kill('SIGKILL');
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
/* */
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// (3) wire it up — still inside the guarded region
|
|
66
|
+
this.childExited = false;
|
|
67
|
+
this.reaped = false;
|
|
68
|
+
ds.once('exit', () => this.onChildExit());
|
|
69
|
+
this.mux.setDownstream(ds);
|
|
70
|
+
// ── ownership transfer point ──
|
|
71
|
+
// Past here the Supervisor (via stop()) owns these; do NOT unwind them.
|
|
72
|
+
this.downstream = ds;
|
|
73
|
+
acquired.length = 0;
|
|
74
|
+
this.state = 'running';
|
|
75
|
+
this.runStartedAt = Date.now();
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
unwind(); // releases log + child if acquired
|
|
79
|
+
this.log = null;
|
|
80
|
+
this.downstream = null;
|
|
81
|
+
this.state = 'failed';
|
|
82
|
+
throw e;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Child exited unexpectedly (or via our kill during reap). */
|
|
86
|
+
onChildExit() {
|
|
87
|
+
this.childExited = true;
|
|
88
|
+
if (this.state === 'stopped')
|
|
89
|
+
return; // intentional shutdown; nothing to do
|
|
90
|
+
// Reset backoff if the run was healthy for long enough.
|
|
91
|
+
if (Date.now() - this.runStartedAt >= this.backoffResetAfterMs) {
|
|
92
|
+
this.backoffMs = this.backoffBaseMs;
|
|
93
|
+
}
|
|
94
|
+
this.state = 'failed';
|
|
95
|
+
this.reap();
|
|
96
|
+
// NOTE: we do NOT call mux.onDownstreamGone() here. The mux self-subscribes
|
|
97
|
+
// to its downstream's 'exit' in setDownstream() and clears its own in-flight
|
|
98
|
+
// state. Single ownership: the mux owns its id-map; the supervisor owns the
|
|
99
|
+
// process (reap) and the respawn FSM. Calling it here would double-fire.
|
|
100
|
+
this.releaseLog();
|
|
101
|
+
this.scheduleRespawn();
|
|
102
|
+
}
|
|
103
|
+
/** Kill (if alive) + mark reaped EXACTLY once — pre-empts double-kill/zombie. */
|
|
104
|
+
reap() {
|
|
105
|
+
if (this.reaped)
|
|
106
|
+
return;
|
|
107
|
+
this.reaped = true;
|
|
108
|
+
const ds = this.downstream;
|
|
109
|
+
this.downstream = null;
|
|
110
|
+
if (ds && !this.childExited) {
|
|
111
|
+
try {
|
|
112
|
+
ds.kill('SIGKILL');
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
releaseLog() {
|
|
120
|
+
if (this.log) {
|
|
121
|
+
try {
|
|
122
|
+
this.log.close();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
/* */
|
|
126
|
+
}
|
|
127
|
+
this.log = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
scheduleRespawn() {
|
|
131
|
+
if (this.respawnTimer || this.state === 'stopped')
|
|
132
|
+
return; // never two in flight
|
|
133
|
+
this.state = 'respawning';
|
|
134
|
+
const delay = this.backoffMs;
|
|
135
|
+
this.backoffMs = Math.min(this.backoffCapMs, this.backoffMs * 2);
|
|
136
|
+
this.respawnTimer = setTimeout(() => {
|
|
137
|
+
this.respawnTimer = null;
|
|
138
|
+
if (this.state === 'stopped')
|
|
139
|
+
return;
|
|
140
|
+
this.start().catch((e) => {
|
|
141
|
+
this.warn(`respawn failed: ${e?.message ?? e}`);
|
|
142
|
+
this.scheduleRespawn(); // start() already set state=failed; back off again
|
|
143
|
+
});
|
|
144
|
+
}, delay);
|
|
145
|
+
this.respawnTimer.unref?.();
|
|
146
|
+
}
|
|
147
|
+
/** Terminal shutdown. Idempotent. Cancels respawn, reaps, releases resources. */
|
|
148
|
+
async stop() {
|
|
149
|
+
if (this.state === 'stopped')
|
|
150
|
+
return;
|
|
151
|
+
this.state = 'stopped';
|
|
152
|
+
if (this.respawnTimer) {
|
|
153
|
+
clearTimeout(this.respawnTimer);
|
|
154
|
+
this.respawnTimer = null;
|
|
155
|
+
}
|
|
156
|
+
this.reap();
|
|
157
|
+
this.releaseLog();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=supervisor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supervisor.js","sourceRoot":"","sources":["../src/supervisor.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire + seam types for the southbound stdio-MCP multiplexer.
|
|
3
|
+
*
|
|
4
|
+
* The `Downstream` and `Session` interfaces are the test seams (mirroring the
|
|
5
|
+
* runner's AdapterFactory idiom): the multiplexer never spawns a process or
|
|
6
|
+
* opens a socket itself — it's handed these, so tests inject in-process fakes.
|
|
7
|
+
*/
|
|
8
|
+
import type { EventEmitter } from 'node:events';
|
|
9
|
+
/** A JSON-RPC id per the spec: string | number | null (null only on errors). */
|
|
10
|
+
export type JsonRpcId = string | number | null;
|
|
11
|
+
/** Minimal JSON-RPC frame. We only care about `id`; everything else is opaque. */
|
|
12
|
+
export interface JsonRpcFrame {
|
|
13
|
+
jsonrpc?: string;
|
|
14
|
+
id?: JsonRpcId;
|
|
15
|
+
method?: string;
|
|
16
|
+
[k: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** One proxy-id -> origin reverse-map entry. */
|
|
19
|
+
export interface IdMapping {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
originalId: JsonRpcId;
|
|
22
|
+
/** Epoch ms the request was forwarded — drives TTL eviction. */
|
|
23
|
+
sentAt: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One downstream MCP child as seen by the multiplexer. Emits exactly one
|
|
27
|
+
* `'line'` per complete newline-delimited JSON frame from the child's stdout
|
|
28
|
+
* (partial-line buffering is the implementation's job, never the consumer's),
|
|
29
|
+
* and `'exit'` once when the child is gone.
|
|
30
|
+
*
|
|
31
|
+
* `write` resolves only after the frame (including its trailing newline) is
|
|
32
|
+
* flushed past back-pressure, so the caller can serialize frames by awaiting.
|
|
33
|
+
*/
|
|
34
|
+
export interface Downstream extends EventEmitter {
|
|
35
|
+
readonly pid: number | undefined;
|
|
36
|
+
/** Write one already-serialized JSON frame (no trailing newline; we add it). */
|
|
37
|
+
write(frame: string): Promise<void>;
|
|
38
|
+
/** Best-effort terminate. */
|
|
39
|
+
kill(signal?: NodeJS.Signals): void;
|
|
40
|
+
}
|
|
41
|
+
export interface DownstreamExit {
|
|
42
|
+
code: number | null;
|
|
43
|
+
signal: NodeJS.Signals | null;
|
|
44
|
+
}
|
|
45
|
+
/** Factory the supervisor uses to (re)spawn the downstream. */
|
|
46
|
+
export interface DownstreamFactory {
|
|
47
|
+
spawn(): Promise<Downstream>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* One client connection == one session. The multiplexer reads `'frame'`
|
|
51
|
+
* events (incoming client frames) and calls `send` to push responses back.
|
|
52
|
+
*/
|
|
53
|
+
export interface Session extends EventEmitter {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
/** Push one serialized JSON frame to the client (newline added by impl). */
|
|
56
|
+
send(frame: string): void;
|
|
57
|
+
close(): void;
|
|
58
|
+
}
|
|
59
|
+
export interface MuxOptions {
|
|
60
|
+
/** Evict id-map entries older than this (ms) and error their sessions. Default 300_000. */
|
|
61
|
+
requestTtlMs?: number;
|
|
62
|
+
/** Sweep cadence (ms). Default 30_000. */
|
|
63
|
+
sweepIntervalMs?: number;
|
|
64
|
+
/** Non-fatal logger. Defaults to stderr. */
|
|
65
|
+
warn?: (msg: string) => void;
|
|
66
|
+
}
|
|
67
|
+
/** JSON-RPC error codes we synthesize. */
|
|
68
|
+
export declare const RPC: {
|
|
69
|
+
readonly PARSE_ERROR: -32700;
|
|
70
|
+
readonly INVALID_REQUEST: -32600;
|
|
71
|
+
readonly INTERNAL: -32603;
|
|
72
|
+
readonly DOWNSTREAM_TIMEOUT: -32010;
|
|
73
|
+
readonly DOWNSTREAM_GONE: -32011;
|
|
74
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA6EA,0CAA0C;AAC1C,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,WAAW,EAAE,CAAC,KAAK;IACnB,eAAe,EAAE,CAAC,KAAK;IACvB,QAAQ,EAAE,CAAC,KAAK;IAChB,kBAAkB,EAAE,CAAC,KAAK;IAC1B,eAAe,EAAE,CAAC,KAAK;CACf,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@almyty/mcp-mux",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "stdio-MCP multiplexer \u2014 fans many client sessions into one downstream MCP child over a Unix socket, rewriting ids. Internal almyty plumbing.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc -p tsconfig.build.json",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.4.0",
|
|
26
|
+
"typescript": "^5.3.0",
|
|
27
|
+
"vitest": "^4.1.0"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"almyty",
|
|
31
|
+
"mcp",
|
|
32
|
+
"multiplexer",
|
|
33
|
+
"stdio",
|
|
34
|
+
"proxy"
|
|
35
|
+
],
|
|
36
|
+
"author": "almyty",
|
|
37
|
+
"license": "Apache-2.0",
|
|
38
|
+
"homepage": "https://almyty.com",
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/almyty-inc/almyty.git",
|
|
42
|
+
"directory": "packages/mcp-mux"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/almyty-inc/almyty/issues"
|
|
46
|
+
}
|
|
47
|
+
}
|