@animalabs/agent-framework 0.5.7 → 0.6.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/dist/src/framework.d.ts +67 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +469 -26
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +44 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +152 -8
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/gate-script-worker.d.ts +2 -0
- package/dist/src/gate/gate-script-worker.d.ts.map +1 -0
- package/dist/src/gate/gate-script-worker.js +69 -0
- package/dist/src/gate/gate-script-worker.js.map +1 -0
- package/dist/src/gate/gate-script.d.ts +44 -0
- package/dist/src/gate/gate-script.d.ts.map +1 -0
- package/dist/src/gate/gate-script.js +164 -0
- package/dist/src/gate/gate-script.js.map +1 -0
- package/dist/src/gate/types.d.ts +18 -3
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +43 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +82 -8
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.d.ts +7 -0
- package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
- package/dist/src/mcpl/checkpoint-manager.js +15 -0
- package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +1 -0
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +35 -30
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +145 -206
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +98 -0
- package/dist/src/mcpl/transport.d.ts.map +1 -0
- package/dist/src/mcpl/transport.js +281 -0
- package/dist/src/mcpl/transport.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +42 -2
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +2 -0
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +9 -2
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/prose-segments.d.ts +18 -0
- package/dist/src/prose-segments.d.ts.map +1 -0
- package/dist/src/prose-segments.js +39 -0
- package/dist/src/prose-segments.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts +2 -0
- package/dist/src/runtime/liveness-watchdog-worker.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog-worker.js +57 -0
- package/dist/src/runtime/liveness-watchdog-worker.js.map +1 -0
- package/dist/src/runtime/liveness-watchdog.d.ts +36 -0
- package/dist/src/runtime/liveness-watchdog.d.ts.map +1 -0
- package/dist/src/runtime/liveness-watchdog.js +106 -0
- package/dist/src/runtime/liveness-watchdog.js.map +1 -0
- package/dist/src/types/agent.d.ts +10 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +14 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +12 -0
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/test/channel-registry-routing.test.js +108 -1
- package/dist/test/channel-registry-routing.test.js.map +1 -1
- package/dist/test/checkpoint-attribution.test.d.ts +2 -0
- package/dist/test/checkpoint-attribution.test.d.ts.map +1 -0
- package/dist/test/checkpoint-attribution.test.js +40 -0
- package/dist/test/checkpoint-attribution.test.js.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts +2 -0
- package/dist/test/event-gate-policy-mutation.test.d.ts.map +1 -0
- package/dist/test/event-gate-policy-mutation.test.js +137 -0
- package/dist/test/event-gate-policy-mutation.test.js.map +1 -0
- package/dist/test/event-gate-script.test.d.ts +2 -0
- package/dist/test/event-gate-script.test.d.ts.map +1 -0
- package/dist/test/event-gate-script.test.js +95 -0
- package/dist/test/event-gate-script.test.js.map +1 -0
- package/dist/test/event-gate-tags.test.d.ts +2 -0
- package/dist/test/event-gate-tags.test.d.ts.map +1 -0
- package/dist/test/event-gate-tags.test.js +76 -0
- package/dist/test/event-gate-tags.test.js.map +1 -0
- package/dist/test/liveness-watchdog.test.d.ts +2 -0
- package/dist/test/liveness-watchdog.test.d.ts.map +1 -0
- package/dist/test/liveness-watchdog.test.js +78 -0
- package/dist/test/liveness-watchdog.test.js.map +1 -0
- package/dist/test/mcpl-ws-transport.test.d.ts +2 -0
- package/dist/test/mcpl-ws-transport.test.d.ts.map +1 -0
- package/dist/test/mcpl-ws-transport.test.js +155 -0
- package/dist/test/mcpl-ws-transport.test.js.map +1 -0
- package/dist/test/prose-segments.test.d.ts +2 -0
- package/dist/test/prose-segments.test.d.ts.map +1 -0
- package/dist/test/prose-segments.test.js +52 -0
- package/dist/test/prose-segments.test.js.map +1 -0
- package/dist/test/trunk-channel-routing.test.d.ts +19 -0
- package/dist/test/trunk-channel-routing.test.d.ts.map +1 -0
- package/dist/test/trunk-channel-routing.test.js +175 -0
- package/dist/test/trunk-channel-routing.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/framework.ts +509 -24
- package/src/gate/event-gate.ts +153 -8
- package/src/gate/gate-script-worker.ts +73 -0
- package/src/gate/gate-script.ts +180 -0
- package/src/gate/types.ts +19 -3
- package/src/mcpl/channel-registry.ts +112 -8
- package/src/mcpl/checkpoint-manager.ts +16 -0
- package/src/mcpl/push-handler.ts +1 -0
- package/src/mcpl/server-connection.ts +153 -224
- package/src/mcpl/transport.ts +307 -0
- package/src/mcpl/types.ts +49 -2
- package/src/module-registry.ts +13 -2
- package/src/prose-segments.ts +42 -0
- package/src/runtime/liveness-watchdog-worker.ts +66 -0
- package/src/runtime/liveness-watchdog.ts +122 -0
- package/src/types/agent.ts +10 -0
- package/src/types/framework.ts +15 -0
- package/src/types/module.ts +13 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* McplServerConnection — manages a single JSON-RPC 2.0 connection to an MCPL server.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Connects over a pluggable {@link McplTransport} (a spawned stdio child OR a
|
|
5
|
+
* WebSocket), performs the MCP initialize handshake with MCPL capability
|
|
6
|
+
* negotiation, and provides typed methods for all outbound MCPL messages plus
|
|
7
|
+
* EventEmitter events for inbound messages. The handshake, message routing, and
|
|
8
|
+
* every send path are transport-agnostic — they operate on NDJSON lines, so
|
|
9
|
+
* stdio and WebSocket behave identically above the transport seam.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
|
-
import { spawn, type ChildProcess } from 'node:child_process';
|
|
10
|
-
import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
|
|
11
12
|
import { EventEmitter } from 'node:events';
|
|
12
13
|
|
|
14
|
+
import { openTransport, type McplTransport, type TransportCloseInfo } from './transport.js';
|
|
15
|
+
|
|
13
16
|
import type {
|
|
14
17
|
McplServerConfig,
|
|
15
18
|
McplCapabilities,
|
|
@@ -78,8 +81,9 @@ export class McplServerConnection extends EventEmitter {
|
|
|
78
81
|
/** MCPL capabilities negotiated during the initialize handshake, or null if the server does not support MCPL. */
|
|
79
82
|
capabilities: McplCapabilities | null;
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
/** The active transport (stdio child or WebSocket). Null for a disconnected
|
|
85
|
+
* stub awaiting its first background reconnect. */
|
|
86
|
+
private transport: McplTransport | null;
|
|
83
87
|
private nextRequestId = 1;
|
|
84
88
|
private pendingRequests = new Map<string | number, PendingRequest>();
|
|
85
89
|
private closed = false;
|
|
@@ -95,28 +99,22 @@ export class McplServerConnection extends EventEmitter {
|
|
|
95
99
|
private reconnectIntervalMs = 5000;
|
|
96
100
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
97
101
|
|
|
98
|
-
/** Swaps the line handler on the current child's stderr pipe. Re-bound on reconnect. */
|
|
99
|
-
private rebindStderr: ((next: (line: string) => void) => void) | null = null;
|
|
100
|
-
|
|
101
102
|
/**
|
|
102
103
|
* Private constructor. Use `McplServerConnection.connect()` instead.
|
|
103
104
|
*/
|
|
104
105
|
private constructor(
|
|
105
106
|
id: string,
|
|
106
107
|
capabilities: McplCapabilities | null,
|
|
107
|
-
|
|
108
|
-
readline: ReadlineInterface,
|
|
108
|
+
transport: McplTransport | null,
|
|
109
109
|
) {
|
|
110
110
|
super();
|
|
111
111
|
this.id = id;
|
|
112
112
|
this.capabilities = capabilities;
|
|
113
|
-
this.
|
|
114
|
-
this.readline = readline;
|
|
113
|
+
this.transport = transport;
|
|
115
114
|
|
|
116
|
-
// Skip setup for disconnected stubs (
|
|
117
|
-
if (
|
|
118
|
-
this.
|
|
119
|
-
this.setupLifecycle();
|
|
115
|
+
// Skip setup for disconnected stubs (transport is null).
|
|
116
|
+
if (transport) {
|
|
117
|
+
this.wireTransport(transport);
|
|
120
118
|
}
|
|
121
119
|
}
|
|
122
120
|
|
|
@@ -155,159 +153,113 @@ export class McplServerConnection extends EventEmitter {
|
|
|
155
153
|
// ==========================================================================
|
|
156
154
|
|
|
157
155
|
/**
|
|
158
|
-
*
|
|
159
|
-
* supplied callback. The callback can be swapped at any time via `rebind`
|
|
160
|
-
* (used during reconnect, when we want already-wired pipes to target a new
|
|
161
|
-
* emitter).
|
|
162
|
-
*/
|
|
163
|
-
private static wireStderrForwarding(
|
|
164
|
-
child: ChildProcess,
|
|
165
|
-
initialLineHandler: (line: string) => void,
|
|
166
|
-
): { rebind: (next: (line: string) => void) => void } {
|
|
167
|
-
let onLine = initialLineHandler;
|
|
168
|
-
let carry = '';
|
|
169
|
-
child.stderr?.on('data', (chunk: Buffer) => {
|
|
170
|
-
const text = carry + chunk.toString('utf8');
|
|
171
|
-
const lines = text.split('\n');
|
|
172
|
-
carry = lines.pop() ?? '';
|
|
173
|
-
for (const line of lines) {
|
|
174
|
-
if (line.length > 0) onLine(line);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
return {
|
|
178
|
-
rebind(next) {
|
|
179
|
-
onLine = next;
|
|
180
|
-
},
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Spawn a server process, perform the MCP initialize handshake with MCPL
|
|
156
|
+
* Open the transport, perform the MCP initialize handshake with MCPL
|
|
186
157
|
* capability negotiation, and return a ready-to-use connection.
|
|
187
158
|
*
|
|
188
|
-
*
|
|
159
|
+
* Works over stdio (spawned child) or WebSocket, selected by `config` (see
|
|
160
|
+
* {@link openTransport}). Throws if the transport can't be opened, the server
|
|
161
|
+
* closes/errors before the handshake, or the handshake times out.
|
|
189
162
|
*/
|
|
190
163
|
static async connect(
|
|
191
164
|
config: McplServerConfig,
|
|
192
165
|
hostCapabilities: McplHostCapabilities,
|
|
193
166
|
): Promise<McplServerConnection> {
|
|
194
|
-
const
|
|
195
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
196
|
-
env: { ...process.env, ...config.env },
|
|
197
|
-
});
|
|
167
|
+
const { transport, capabilities } = await McplServerConnection.handshake(config, hostCapabilities);
|
|
198
168
|
|
|
199
|
-
|
|
200
|
-
// window (before the instance exists) lines are buffered; the buffer is
|
|
201
|
-
// drained once we have an instance to emit 'stderr' events on.
|
|
202
|
-
const earlyStderrBuffer: string[] = [];
|
|
203
|
-
const attachStderr = McplServerConnection.wireStderrForwarding(child, (line) =>
|
|
204
|
-
earlyStderrBuffer.push(line),
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
// Fail fast if the process exits before the handshake completes
|
|
208
|
-
const earlyExitPromise = new Promise<never>((_resolve, reject) => {
|
|
209
|
-
child.on('error', (err) => {
|
|
210
|
-
reject(new Error(`Failed to spawn MCPL server "${config.id}": ${err.message}`));
|
|
211
|
-
});
|
|
212
|
-
child.on('exit', (code) => {
|
|
213
|
-
reject(new Error(`MCPL server "${config.id}" exited before handshake (code ${code})`));
|
|
214
|
-
});
|
|
215
|
-
});
|
|
169
|
+
const connection = new McplServerConnection(config.id, capabilities, transport);
|
|
216
170
|
|
|
217
|
-
//
|
|
218
|
-
|
|
171
|
+
// Store config for reconnection
|
|
172
|
+
if (config.reconnect) {
|
|
173
|
+
connection.config = config;
|
|
174
|
+
connection.hostCapabilities = hostCapabilities;
|
|
175
|
+
connection.reconnectEnabled = true;
|
|
176
|
+
connection.reconnectIntervalMs = config.reconnectIntervalMs ?? 5000;
|
|
177
|
+
}
|
|
219
178
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// -----------------------------------------------------------------------
|
|
179
|
+
return connection;
|
|
180
|
+
}
|
|
223
181
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
},
|
|
237
|
-
clientInfo: {
|
|
238
|
-
name: 'agent-framework',
|
|
239
|
-
version: '1.0.0',
|
|
240
|
-
},
|
|
241
|
-
},
|
|
242
|
-
};
|
|
243
|
-
child.stdin!.write(JSON.stringify(initRequest) + '\n');
|
|
182
|
+
/**
|
|
183
|
+
* Open a fresh transport and run the MCP/MCPL initialize handshake over it,
|
|
184
|
+
* returning the connected transport plus the negotiated capabilities. Shared
|
|
185
|
+
* by {@link connect} and {@link attemptReconnect} so both transports handshake
|
|
186
|
+
* identically. On any failure the transport is closed so a spawned child /
|
|
187
|
+
* open socket never leaks.
|
|
188
|
+
*/
|
|
189
|
+
private static async handshake(
|
|
190
|
+
config: McplServerConfig,
|
|
191
|
+
hostCapabilities: McplHostCapabilities,
|
|
192
|
+
): Promise<{ transport: McplTransport; capabilities: McplCapabilities | null }> {
|
|
193
|
+
const transport = await openTransport(config);
|
|
244
194
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
195
|
+
try {
|
|
196
|
+
const initId = 0; // use id=0 for the handshake
|
|
197
|
+
const initRequest: JsonRpcRequest = {
|
|
198
|
+
jsonrpc: '2.0',
|
|
199
|
+
method: 'initialize',
|
|
200
|
+
id: initId,
|
|
201
|
+
params: {
|
|
202
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
203
|
+
capabilities: { experimental: { mcpl: hostCapabilities } },
|
|
204
|
+
clientInfo: { name: 'agent-framework', version: '1.0.0' },
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Await the initialize response, racing an early transport close/error and
|
|
209
|
+
// a timeout. All three paths clean up their listeners.
|
|
210
|
+
const capabilities = await new Promise<McplCapabilities | null>((resolve, reject) => {
|
|
211
|
+
const cleanup = () => {
|
|
212
|
+
transport.off('line', onLine);
|
|
213
|
+
transport.off('close', onClose);
|
|
214
|
+
transport.off('error', onError);
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
};
|
|
248
217
|
const onLine = (line: string) => {
|
|
218
|
+
let msg: JsonRpcResponse;
|
|
249
219
|
try {
|
|
250
|
-
|
|
251
|
-
if (msg.id === initId) {
|
|
252
|
-
rl.off('line', onLine);
|
|
253
|
-
if (msg.error) {
|
|
254
|
-
reject(new Error(`MCPL server "${config.id}" initialize error: ${msg.error.message}`));
|
|
255
|
-
} else {
|
|
256
|
-
resolve(msg);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
220
|
+
msg = JSON.parse(line) as JsonRpcResponse;
|
|
259
221
|
} catch {
|
|
260
|
-
// Ignore non-JSON lines (e.g. logback output from Java servers)
|
|
222
|
+
return; // Ignore non-JSON lines (e.g. logback output from Java servers)
|
|
261
223
|
}
|
|
224
|
+
if (msg.id !== initId) return;
|
|
225
|
+
cleanup();
|
|
226
|
+
if (msg.error) {
|
|
227
|
+
reject(new Error(`MCPL server "${config.id}" initialize error: ${msg.error.message}`));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const result = msg.result as Record<string, unknown> | undefined;
|
|
231
|
+
const caps = result?.capabilities as Record<string, unknown> | undefined;
|
|
232
|
+
const experimental = caps?.experimental as Record<string, unknown> | undefined;
|
|
233
|
+
resolve((experimental?.mcpl as McplCapabilities) ?? null);
|
|
234
|
+
};
|
|
235
|
+
const onClose = (info: TransportCloseInfo) => {
|
|
236
|
+
cleanup();
|
|
237
|
+
reject(new Error(`MCPL server "${config.id}" transport closed before handshake completed (${info.reason ?? 'unknown'})`));
|
|
262
238
|
};
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
setTimeout(() => {
|
|
239
|
+
const onError = (err: Error) => {
|
|
240
|
+
cleanup();
|
|
241
|
+
reject(new Error(`MCPL server "${config.id}" transport error during handshake: ${err.message}`));
|
|
242
|
+
};
|
|
243
|
+
const timer = setTimeout(() => {
|
|
244
|
+
cleanup();
|
|
268
245
|
reject(new Error(`MCPL server "${config.id}" initialize handshake timed out`));
|
|
269
|
-
}, INITIALIZE_TIMEOUT_MS)
|
|
270
|
-
),
|
|
271
|
-
]);
|
|
272
|
-
|
|
273
|
-
// Parse MCPL capabilities from the response
|
|
274
|
-
const result = initResponse.result as Record<string, unknown> | undefined;
|
|
275
|
-
const caps = result?.capabilities as Record<string, unknown> | undefined;
|
|
276
|
-
const experimental = caps?.experimental as Record<string, unknown> | undefined;
|
|
277
|
-
const mcplCaps = (experimental?.mcpl as McplCapabilities) ?? null;
|
|
278
|
-
|
|
279
|
-
// Step 3: Send `initialized` notification (no id)
|
|
280
|
-
const initializedNotification: JsonRpcRequest = {
|
|
281
|
-
jsonrpc: '2.0',
|
|
282
|
-
method: 'notifications/initialized',
|
|
283
|
-
};
|
|
284
|
-
child.stdin!.write(JSON.stringify(initializedNotification) + '\n');
|
|
246
|
+
}, INITIALIZE_TIMEOUT_MS);
|
|
285
247
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
248
|
+
transport.on('line', onLine);
|
|
249
|
+
transport.on('close', onClose);
|
|
250
|
+
transport.on('error', onError);
|
|
251
|
+
transport.writeLine(JSON.stringify(initRequest));
|
|
252
|
+
});
|
|
289
253
|
|
|
290
|
-
|
|
254
|
+
// Send `initialized` notification (no id)
|
|
255
|
+
transport.writeLine(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }));
|
|
291
256
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
for (const line of earlyStderrBuffer) {
|
|
298
|
-
connection.emit('stderr', { line });
|
|
257
|
+
return { transport, capabilities };
|
|
258
|
+
} catch (err) {
|
|
259
|
+
// Never leak the child / socket if the handshake fails.
|
|
260
|
+
await transport.close().catch(() => { /* best effort */ });
|
|
261
|
+
throw err;
|
|
299
262
|
}
|
|
300
|
-
earlyStderrBuffer.length = 0;
|
|
301
|
-
|
|
302
|
-
// Store config for reconnection
|
|
303
|
-
if (config.reconnect) {
|
|
304
|
-
connection.config = config;
|
|
305
|
-
connection.hostCapabilities = hostCapabilities;
|
|
306
|
-
connection.reconnectEnabled = true;
|
|
307
|
-
connection.reconnectIntervalMs = config.reconnectIntervalMs ?? 5000;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return connection;
|
|
311
263
|
}
|
|
312
264
|
|
|
313
265
|
/**
|
|
@@ -342,8 +294,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
342
294
|
config: McplServerConfig,
|
|
343
295
|
hostCapabilities: McplHostCapabilities,
|
|
344
296
|
): McplServerConnection {
|
|
345
|
-
//
|
|
346
|
-
const stub = new McplServerConnection(config.id, null, null
|
|
297
|
+
// Null transport — the stub is closed until its first background reconnect.
|
|
298
|
+
const stub = new McplServerConnection(config.id, null, null);
|
|
347
299
|
stub.closed = true;
|
|
348
300
|
stub.config = config;
|
|
349
301
|
stub.hostCapabilities = hostCapabilities;
|
|
@@ -356,18 +308,6 @@ export class McplServerConnection extends EventEmitter {
|
|
|
356
308
|
return stub;
|
|
357
309
|
}
|
|
358
310
|
|
|
359
|
-
/**
|
|
360
|
-
* Extract internals for transfer during reconnection.
|
|
361
|
-
* Strips the source connection — caller takes ownership of the process/readline.
|
|
362
|
-
* @internal Used only by attemptReconnect().
|
|
363
|
-
*/
|
|
364
|
-
extractInternals(): { process: ChildProcess; readline: ReadlineInterface } {
|
|
365
|
-
const result = { process: this.process, readline: this.readline };
|
|
366
|
-
// Prevent the source connection from killing the transferred process
|
|
367
|
-
this.closed = true;
|
|
368
|
-
return result;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
311
|
// ==========================================================================
|
|
372
312
|
// Outbound requests (return Promises)
|
|
373
313
|
// ==========================================================================
|
|
@@ -505,25 +445,11 @@ export class McplServerConnection extends EventEmitter {
|
|
|
505
445
|
}
|
|
506
446
|
this.pendingRequests.clear();
|
|
507
447
|
|
|
508
|
-
//
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// Kill the child process
|
|
514
|
-
if (this.process && !this.process.killed) {
|
|
515
|
-
this.process.kill();
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Wait for process to actually exit
|
|
519
|
-
if (this.process) {
|
|
520
|
-
await new Promise<void>((resolve) => {
|
|
521
|
-
if (this.process.exitCode !== null || this.process.killed) {
|
|
522
|
-
resolve();
|
|
523
|
-
} else {
|
|
524
|
-
this.process.once('exit', () => resolve());
|
|
525
|
-
}
|
|
526
|
-
});
|
|
448
|
+
// Tear down the transport (kills the child / closes the socket). The
|
|
449
|
+
// transport's own 'close' event is short-circuited by `this.closed` in
|
|
450
|
+
// setupLifecycle, so this is the single source of the connection 'close'.
|
|
451
|
+
if (this.transport) {
|
|
452
|
+
await this.transport.close();
|
|
527
453
|
}
|
|
528
454
|
|
|
529
455
|
this.emit('close');
|
|
@@ -542,42 +468,30 @@ export class McplServerConnection extends EventEmitter {
|
|
|
542
468
|
}
|
|
543
469
|
|
|
544
470
|
/**
|
|
545
|
-
* Attempt to reconnect by
|
|
471
|
+
* Attempt to reconnect by opening a fresh transport and performing the
|
|
472
|
+
* handshake, then adopting that transport in place. Unlike the previous
|
|
473
|
+
* approach this builds no throwaway connection instance, so there are no
|
|
474
|
+
* leaked listeners on a dead object.
|
|
546
475
|
*/
|
|
547
476
|
private async attemptReconnect(): Promise<void> {
|
|
548
477
|
if (!this.reconnectEnabled || !this.config || !this.hostCapabilities) return;
|
|
549
478
|
|
|
550
479
|
try {
|
|
551
|
-
const
|
|
552
|
-
|
|
480
|
+
const { transport, capabilities } = await McplServerConnection.handshake(
|
|
481
|
+
this.config,
|
|
482
|
+
this.hostCapabilities,
|
|
483
|
+
);
|
|
553
484
|
|
|
554
|
-
//
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
this.
|
|
485
|
+
// Adopt the new transport. Listeners were attached to external consumers
|
|
486
|
+
// on the first wire, so mark ready immediately (re-wire re-binds the
|
|
487
|
+
// transport-level handlers below).
|
|
488
|
+
this.transport = transport;
|
|
489
|
+
this.capabilities = capabilities;
|
|
558
490
|
this.closed = false;
|
|
559
491
|
this.nextRequestId = 1;
|
|
560
492
|
this.pendingRequests.clear();
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
//
|
|
564
|
-
// Note: this follows the same "transfer internals, leak listeners on the
|
|
565
|
-
// dead instance" pattern as the message-routing and lifecycle rewiring
|
|
566
|
-
// below — `fresh`'s own readline 'line' handler and process 'exit'
|
|
567
|
-
// handler stay registered on the transferred child, short-circuited by
|
|
568
|
-
// `fresh.closed = true` set in extractInternals(). Rebinding fresh's
|
|
569
|
-
// stderr listener (rather than removing it) keeps that invariant: if we
|
|
570
|
-
// ever clean up the message-routing duplication, fresh's stderr data
|
|
571
|
-
// listener closure-holds onLine and would need the same teardown.
|
|
572
|
-
if (fresh.rebindStderr) {
|
|
573
|
-
fresh.rebindStderr((line: string) => this.emit('stderr', { line }));
|
|
574
|
-
this.rebindStderr = fresh.rebindStderr;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Re-wire message routing and lifecycle on the new process
|
|
578
|
-
this.setupMessageRouting();
|
|
579
|
-
this.setupLifecycle();
|
|
580
|
-
this.readyFlag = true; // Listeners already attached from initial wire
|
|
493
|
+
this.wireTransport(transport);
|
|
494
|
+
this.readyFlag = true;
|
|
581
495
|
|
|
582
496
|
console.error(`MCPL server "${this.id}" reconnected successfully`);
|
|
583
497
|
this.emit('reconnect');
|
|
@@ -610,7 +524,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
610
524
|
|
|
611
525
|
return new Promise((resolve, reject) => {
|
|
612
526
|
this.pendingRequests.set(id, { resolve, reject, method });
|
|
613
|
-
this.
|
|
527
|
+
this.transport!.writeLine(JSON.stringify(request));
|
|
614
528
|
});
|
|
615
529
|
}
|
|
616
530
|
|
|
@@ -627,7 +541,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
627
541
|
method,
|
|
628
542
|
params,
|
|
629
543
|
};
|
|
630
|
-
this.
|
|
544
|
+
this.transport?.writeLine(JSON.stringify(notification));
|
|
631
545
|
}
|
|
632
546
|
|
|
633
547
|
// ==========================================================================
|
|
@@ -653,10 +567,23 @@ export class McplServerConnection extends EventEmitter {
|
|
|
653
567
|
};
|
|
654
568
|
|
|
655
569
|
/**
|
|
656
|
-
*
|
|
570
|
+
* Bind all transport-level handlers (inbound routing, lifecycle, stderr) to a
|
|
571
|
+
* transport. Called from the constructor and again from attemptReconnect when
|
|
572
|
+
* a new transport is adopted.
|
|
573
|
+
*/
|
|
574
|
+
private wireTransport(transport: McplTransport): void {
|
|
575
|
+
this.setupMessageRouting(transport);
|
|
576
|
+
this.setupLifecycle(transport);
|
|
577
|
+
// Surface transport diagnostics (stdio child stderr) as 'stderr' events,
|
|
578
|
+
// preserving the `{ line }` payload existing consumers expect.
|
|
579
|
+
transport.on('stderr', (line: string) => this.emit('stderr', { line }));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Wire up the transport's line stream to route incoming JSON-RPC messages.
|
|
657
584
|
*/
|
|
658
|
-
private setupMessageRouting(): void {
|
|
659
|
-
|
|
585
|
+
private setupMessageRouting(transport: McplTransport): void {
|
|
586
|
+
transport.on('line', (line: string) => {
|
|
660
587
|
let msg: JsonRpcRequest | JsonRpcResponse;
|
|
661
588
|
try {
|
|
662
589
|
msg = JSON.parse(line);
|
|
@@ -719,7 +646,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
719
646
|
private sendResponse(id: string | number, result: unknown): void {
|
|
720
647
|
if (this.closed) return;
|
|
721
648
|
const response: JsonRpcResponse = { jsonrpc: '2.0', id, result };
|
|
722
|
-
this.
|
|
649
|
+
this.transport?.writeLine(JSON.stringify(response));
|
|
723
650
|
}
|
|
724
651
|
|
|
725
652
|
/**
|
|
@@ -732,7 +659,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
732
659
|
id,
|
|
733
660
|
error: { code, message, data },
|
|
734
661
|
};
|
|
735
|
-
this.
|
|
662
|
+
this.transport?.writeLine(JSON.stringify(response));
|
|
736
663
|
}
|
|
737
664
|
|
|
738
665
|
// ==========================================================================
|
|
@@ -740,14 +667,16 @@ export class McplServerConnection extends EventEmitter {
|
|
|
740
667
|
// ==========================================================================
|
|
741
668
|
|
|
742
669
|
/**
|
|
743
|
-
* Set up
|
|
670
|
+
* Set up transport error/close handlers. An unexpected transport close
|
|
671
|
+
* (child exit or WebSocket drop) rejects in-flight requests, emits 'close',
|
|
672
|
+
* and — when enabled — schedules a background reconnect.
|
|
744
673
|
*/
|
|
745
|
-
private setupLifecycle(): void {
|
|
746
|
-
|
|
747
|
-
this.emit('error', new Error(`MCPL server "${this.id}"
|
|
674
|
+
private setupLifecycle(transport: McplTransport): void {
|
|
675
|
+
transport.on('error', (err: Error) => {
|
|
676
|
+
this.emit('error', new Error(`MCPL server "${this.id}" transport error: ${err.message}`));
|
|
748
677
|
});
|
|
749
678
|
|
|
750
|
-
|
|
679
|
+
transport.on('close', (info: TransportCloseInfo) => {
|
|
751
680
|
if (!this.closed) {
|
|
752
681
|
this.closed = true;
|
|
753
682
|
|
|
@@ -755,15 +684,15 @@ export class McplServerConnection extends EventEmitter {
|
|
|
755
684
|
for (const [id, pending] of this.pendingRequests) {
|
|
756
685
|
pending.reject(
|
|
757
686
|
new Error(
|
|
758
|
-
`MCPL server "${this.id}"
|
|
687
|
+
`MCPL server "${this.id}" disconnected unexpectedly (code=${info.code ?? 'n/a'}, signal=${info.signal ?? 'n/a'}, reason=${info.reason ?? 'unknown'}) while awaiting ${pending.method} (id=${id})`,
|
|
759
688
|
),
|
|
760
689
|
);
|
|
761
690
|
}
|
|
762
691
|
this.pendingRequests.clear();
|
|
763
692
|
|
|
764
|
-
this.emit('close', code, signal);
|
|
693
|
+
this.emit('close', info.code ?? null, info.signal ?? null);
|
|
765
694
|
|
|
766
|
-
// Schedule reconnect if enabled (unexpected
|
|
695
|
+
// Schedule reconnect if enabled (unexpected disconnect triggers auto-reconnect)
|
|
767
696
|
if (this.reconnectEnabled) {
|
|
768
697
|
this.scheduleReconnect();
|
|
769
698
|
}
|