mcp-pacemaker 2.0.1

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +452 -0
  2. package/CONTRIBUTING.md +222 -0
  3. package/LICENSE +21 -0
  4. package/README.md +621 -0
  5. package/SECURITY.md +120 -0
  6. package/THIRD_PARTY_NOTICES.txt +366 -0
  7. package/autostart/windows/launcher.vbs +11 -0
  8. package/autostart/windows/register-task.ps1 +63 -0
  9. package/bin/cli.mjs +880 -0
  10. package/bin/config-checks.mjs +62 -0
  11. package/bin/logs.mjs +465 -0
  12. package/bin/mcp-bridge.mjs +2162 -0
  13. package/bin/pooling-batch-scheduler.mjs +129 -0
  14. package/bin/pooling-batches.mjs +270 -0
  15. package/bin/pooling-config.mjs +577 -0
  16. package/bin/pooling-editor.mjs +128 -0
  17. package/bin/pooling-errors.mjs +23 -0
  18. package/bin/pooling-execution.mjs +104 -0
  19. package/bin/pooling-files.mjs +541 -0
  20. package/bin/pooling-trace.mjs +218 -0
  21. package/bin/pooling-writer.mjs +242 -0
  22. package/bin/prewarm.mjs +233 -0
  23. package/bin/server-metrics.mjs +69 -0
  24. package/bin/service-control.mjs +124 -0
  25. package/bin/shared-sessions.mjs +1083 -0
  26. package/bin/top.mjs +115 -0
  27. package/bin/update-check.mjs +153 -0
  28. package/bin/windows/PoolingSecurityHelper.build.json +42 -0
  29. package/bin/windows/PoolingSecurityHelper.exe +0 -0
  30. package/bin/windows/src/AssemblyInfo.cs +11 -0
  31. package/bin/windows/src/PoolingNativeFiles.cs +678 -0
  32. package/bin/windows/src/PoolingSecurityHelper.cs +178 -0
  33. package/bin/windows/src/PoolingSecurityReader.cs +50 -0
  34. package/docs/shared-sessions.md +181 -0
  35. package/examples/servers.example.json +31 -0
  36. package/package.json +77 -0
  37. package/supervisor/bridge-child.mjs +14 -0
  38. package/supervisor/supervise.mjs +172 -0
  39. package/supervisor/supervise.ps1 +23 -0
  40. package/supervisor/supervise.sh +11 -0
  41. package/tools/windows-security-helper/README.md +238 -0
  42. package/tools/windows-security-helper/build.ps1 +262 -0
  43. package/ui/dist/THIRD_PARTY_NOTICES.txt +366 -0
  44. package/ui/dist/assets/index-CaLPg0c-.css +1 -0
  45. package/ui/dist/assets/index-CgPruWYi.js +9 -0
  46. package/ui/dist/index.html +14 -0
  47. package/ui/dist/third-party-manifest.json +597 -0
@@ -0,0 +1,1083 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Readable, Writable } from 'node:stream';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { TextDecoder } from 'node:util';
5
+
6
+ export const SHARED_DEFAULTS = Object.freeze({
7
+ sharedLingerMs: 30 * 60_000,
8
+ sharedDrainTimeoutMs: 30_000,
9
+ sharedRetryDelayMs: 1_000,
10
+ sharedMaxSessions: 128,
11
+ sharedMaxInFlight: 8,
12
+ sharedMaxQueued: 32,
13
+ sharedMaxIds: 4_096,
14
+ sharedMaxCursors: 256,
15
+ sharedMaxLineBytes: 1_048_576,
16
+ sharedMaxBufferBytes: 4_194_304,
17
+ });
18
+
19
+ const LIMITS = {
20
+ sharedLingerMs: [0, 2_147_483_647],
21
+ sharedDrainTimeoutMs: [1, 2_147_483_647],
22
+ sharedRetryDelayMs: [1, 2_147_483_647],
23
+ sharedMaxSessions: [1, 1_024],
24
+ sharedMaxInFlight: [1, 1_024],
25
+ sharedMaxQueued: [0, 4_096],
26
+ sharedMaxIds: [1, 65_536],
27
+ sharedMaxCursors: [1, 4_096],
28
+ sharedMaxLineBytes: [256, 16_777_216],
29
+ sharedMaxBufferBytes: [256, 67_108_864],
30
+ };
31
+ const VERSIONS = new Set(['2025-03-26', '2025-06-18', '2025-11-25']);
32
+ const MAX_JSON_DEPTH = 64;
33
+ const SERVER_CAPABILITY_SHAPES = {
34
+ tools: { listChanged: 'boolean' },
35
+ prompts: { listChanged: 'boolean' },
36
+ resources: { subscribe: 'boolean', listChanged: 'boolean' },
37
+ logging: {},
38
+ completions: {},
39
+ tasks: { list: {}, cancel: {}, requests: { tools: { call: {} } } },
40
+ experimental: {},
41
+ };
42
+
43
+ export class SharedSessionManager extends EventEmitter {
44
+ constructor({ spawn, kill, onFailure = () => {}, onInitialized = () => {}, onResponse = () => {}, log = () => {},
45
+ initTimeoutMs = 180_000, requestTimeoutMs = 30_000 } = {}) {
46
+ super();
47
+ for (const fn of [spawn, kill, onFailure, onInitialized, onResponse, log]) {
48
+ if (typeof fn !== 'function') throw new TypeError('Shared callbacks must be functions');
49
+ }
50
+ this.validateInteger('initTimeoutMs', initTimeoutMs, 1, 2_147_483_647);
51
+ this.validateInteger('requestTimeoutMs', requestTimeoutMs, 1, 2_147_483_647);
52
+ Object.assign(this, { spawn, kill, onFailure, onInitialized, onResponse, log, initTimeoutMs, requestTimeoutMs });
53
+ this.groups = new Map();
54
+ this.cooldowns = new Map();
55
+ this.closed = false;
56
+ this.sequence = 0n;
57
+ }
58
+
59
+ validateInteger(name, value, min, max) {
60
+ if (!Number.isSafeInteger(value) ||
61
+ value < min ||
62
+ value > max) {
63
+ throw new TypeError(`${name} must be a safe integer between ${min} and ${max}`);
64
+ }
65
+ }
66
+
67
+ record(value) {
68
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
69
+ }
70
+
71
+ validId(value) {
72
+ return typeof value === 'string' || Number.isSafeInteger(value);
73
+ }
74
+
75
+ withinDepth(value, depth = 0) {
76
+ if (depth > MAX_JSON_DEPTH) return false;
77
+ if (value === null ||
78
+ typeof value !== 'object') return true;
79
+ return Object.values(value).every((entry) => this.withinDepth(entry, depth + 1));
80
+ }
81
+
82
+ validCapabilityRecord(value, shape = SERVER_CAPABILITY_SHAPES) {
83
+ if (!this.record(value)) return false;
84
+ return Object.entries(value).every(([key, entry]) => {
85
+ if (!Object.hasOwn(shape, key)) return false;
86
+ return shape[key] === 'boolean' ? typeof entry === 'boolean' : this.validCapabilityRecord(entry, shape[key]);
87
+ });
88
+ }
89
+
90
+ canonical(value, depth = 0) {
91
+ if (depth > MAX_JSON_DEPTH) throw this.error('SHARED_INVALID_CONFIG', 'validation', 'unsent');
92
+ if (value === null) return 'null';
93
+ if (Array.isArray(value)) {
94
+ return `[${Array.from(value, (entry) => this.canonical(entry, depth + 1)).join(',')}]`;
95
+ }
96
+ if (this.record(value)) {
97
+ const prototype = Object.getPrototypeOf(value);
98
+ if (prototype !== Object.prototype &&
99
+ prototype !== null) {
100
+ throw this.error('SHARED_INVALID_CONFIG', 'validation', 'unsent');
101
+ }
102
+ return `{${Object.keys(value).sort().map((key) =>
103
+ `${JSON.stringify(key)}:${this.canonical(value[key], depth + 1)}`).join(',')}}`;
104
+ }
105
+ if (typeof value === 'string' ||
106
+ typeof value === 'boolean' ||
107
+ (typeof value === 'number' && Number.isFinite(value))) {
108
+ return JSON.stringify(value);
109
+ }
110
+ throw this.error('SHARED_INVALID_CONFIG', 'validation', 'unsent');
111
+ }
112
+
113
+ options(definition) {
114
+ const options = {};
115
+ for (const [name, fallback] of Object.entries(SHARED_DEFAULTS)) {
116
+ const value = Object.hasOwn(definition, name) ? definition[name] : fallback;
117
+ this.validateInteger(name, value, ...LIMITS[name]);
118
+ options[name] = value;
119
+ }
120
+ return options;
121
+ }
122
+
123
+ validateInitialize(params) {
124
+ const valid = this.record(params) && VERSIONS.has(params.protocolVersion) &&
125
+ this.record(params.capabilities) && Object.keys(params.capabilities).length === 0 &&
126
+ this.record(params.clientInfo) && typeof params.clientInfo.name === 'string' &&
127
+ typeof params.clientInfo.version === 'string';
128
+ if (!valid) throw this.error('SHARED_INCOMPATIBLE', 'initialize', 'unsent');
129
+ }
130
+
131
+ detail(kind, phase, execution, generation = null) {
132
+ return { kind, phase, execution, generation };
133
+ }
134
+
135
+ error(kind, phase, execution, generation = null) {
136
+ const error = new Error(`shared: ${kind}`);
137
+ error.code = kind;
138
+ error.data = this.detail(kind, phase, execution, generation);
139
+ return error;
140
+ }
141
+
142
+ rpcError(id, detail, code = -32002) {
143
+ return { jsonrpc: '2.0', id, error: { code, message: `shared: ${detail.kind}`, data: detail } };
144
+ }
145
+
146
+ async acquire(name, definition, initializeParams) {
147
+ if (this.closed) throw this.error('SHARED_SHUTDOWN', 'admission', 'unsent');
148
+ const wrongTransport = this.record(definition) && definition.type !== undefined &&
149
+ definition.type !== 'stdio';
150
+ if (typeof name !== 'string' ||
151
+ !name.length ||
152
+ !this.record(definition) ||
153
+ definition.sharing !== 'shared' ||
154
+ wrongTransport) {
155
+ throw this.error('SHARED_INCOMPATIBLE', 'admission', 'unsent');
156
+ }
157
+ this.validateInitialize(initializeParams);
158
+ const options = this.options(definition);
159
+ const configKey = this.canonical(definition);
160
+ const initKey = this.canonical(initializeParams);
161
+ if (Buffer.byteLength(configKey) + Buffer.byteLength(initKey) > options.sharedMaxLineBytes) {
162
+ throw this.error('SHARED_INPUT_LIMIT', 'admission', 'unsent');
163
+ }
164
+ let group = this.groups.get(name);
165
+ if (group) {
166
+ if (group.configKey !== configKey ||
167
+ group.initKey !== initKey) {
168
+ throw this.error('SHARED_INCOMPATIBLE', 'admission', 'unsent', group.generation);
169
+ }
170
+ if (group.state !== 'starting' &&
171
+ group.state !== 'ready') {
172
+ throw this.error(`SHARED_${group.state.toUpperCase()}`, 'admission', 'unsent', group.generation);
173
+ }
174
+ } else {
175
+ const retryAt = this.cooldowns.get(name);
176
+ if (retryAt > Date.now()) {
177
+ const error = this.error('SHARED_RETRY_LATER', 'admission', 'unsent');
178
+ error.retryAfterMs = retryAt - Date.now();
179
+ throw error;
180
+ }
181
+ this.cooldowns.delete(name);
182
+ if (this.groups.size >= 128) throw this.error('SHARED_GROUP_LIMIT', 'admission', 'unsent');
183
+ group = new SharedGroup(this, name, JSON.parse(configKey), JSON.parse(initKey),
184
+ configKey, initKey, options, String(++this.sequence));
185
+ this.groups.set(name, group);
186
+ group.start();
187
+ }
188
+ if (group.members.size + group.waiters >= options.sharedMaxSessions) {
189
+ throw this.error('SHARED_SESSION_LIMIT', 'admission', 'unsent', group.generation);
190
+ }
191
+ group.waiters++;
192
+ clearTimeout(group.lingerTimer);
193
+ try {
194
+ await group.ready;
195
+ if (group.state !== 'ready') {
196
+ throw this.error('SHARED_RETIRED', 'admission', 'unsent', group.generation);
197
+ }
198
+ const child = new SharedChild(group);
199
+ group.members.add(child);
200
+ return child;
201
+ } finally {
202
+ group.waiters--;
203
+ group.idle();
204
+ }
205
+ }
206
+
207
+ recycle(name) {
208
+ const group = this.groups.get(name);
209
+ if (!group) return { found: false };
210
+ group.drain('SHARED_RECYCLED');
211
+ return { found: true, ...group.inspect() };
212
+ }
213
+
214
+ remove(name) {
215
+ this.cooldowns.delete(name);
216
+ const group = this.groups.get(name);
217
+ if (!group) return { found: false };
218
+ group.stop('SHARED_REMOVED', false);
219
+ return { found: true, ...group.inspect() };
220
+ }
221
+
222
+ shutdown() {
223
+ this.closed = true;
224
+ const groups = [...this.groups.values()];
225
+ for (const group of groups) group.stop('SHARED_SHUTDOWN', false);
226
+ this.cooldowns.clear();
227
+ return { groups: groups.length };
228
+ }
229
+
230
+ inspect(name) {
231
+ return this.groups.get(name)?.inspect() ?? null;
232
+ }
233
+
234
+ retired(group) {
235
+ if (this.groups.get(group.name) === group) this.groups.delete(group.name);
236
+ if (group.failed &&
237
+ !this.closed) {
238
+ this.cooldowns.delete(group.name);
239
+ this.cooldowns.set(group.name, Date.now() + group.options.sharedRetryDelayMs);
240
+ if (this.cooldowns.size > 128) this.cooldowns.delete(this.cooldowns.keys().next().value);
241
+ }
242
+ const detail = { name: group.name, ...group.inspect(), reason: group.reason, failed: group.failed };
243
+ queueMicrotask(() => this.emit('retired', detail));
244
+ }
245
+ }
246
+
247
+ class JsonLines {
248
+ constructor(limit, receive, fail) {
249
+ this.limit = limit;
250
+ this.receive = receive;
251
+ this.fail = fail;
252
+ this.buffer = Buffer.alloc(0);
253
+ this.decoder = new TextDecoder('utf-8', { fatal: true });
254
+ this.closed = false;
255
+ }
256
+
257
+ write(chunk) {
258
+ if (this.closed) return;
259
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
260
+ let start = 0;
261
+ while (start < bytes.length) {
262
+ const end = bytes.indexOf(10, start);
263
+ const stop = end < 0 ? bytes.length : end;
264
+ const part = bytes.subarray(start, stop);
265
+ if (this.buffer.length + part.length > this.limit) {
266
+ this.close();
267
+ this.fail('SHARED_INPUT_LIMIT');
268
+ return;
269
+ }
270
+ const line = this.buffer.length ? Buffer.concat([this.buffer, part]) : part;
271
+ if (end < 0) {
272
+ this.buffer = Buffer.from(line);
273
+ return;
274
+ }
275
+ this.buffer = Buffer.alloc(0);
276
+ let message;
277
+ try {
278
+ message = JSON.parse(this.decoder.decode(line));
279
+ } catch {
280
+ this.close();
281
+ this.fail('SHARED_INVALID_JSON');
282
+ return;
283
+ }
284
+ this.receive(message);
285
+ if (this.closed) return;
286
+ start = end + 1;
287
+ }
288
+ }
289
+
290
+ close() {
291
+ this.closed = true;
292
+ this.buffer = Buffer.alloc(0);
293
+ }
294
+ }
295
+
296
+ class SharedChild extends EventEmitter {
297
+ constructor(group) {
298
+ super();
299
+ this.group = group;
300
+ this.pid = group.child.pid;
301
+ this.__spawnedAt = group.child.__spawnedAt ?? group.spawnedAt;
302
+ this.__sharedSession = true;
303
+ this.__sharedGeneration = group.generation;
304
+ this.exitCode = null;
305
+ this.signalCode = null;
306
+ this.active = true;
307
+ this.outputFailed = false;
308
+ this.phase = 'attached';
309
+ this.ids = new Set();
310
+ this.retainedBytes = 0;
311
+ this.pending = new Map();
312
+ this.cursors = new Map();
313
+ this.queue = [];
314
+ this.stdout = new Readable({ read() {} });
315
+ this.stderr = new Readable({ read() {} });
316
+ this.lines = new JsonLines(group.options.sharedMaxLineBytes,
317
+ (message) => group.clientMessage(this, message),
318
+ (kind) => group.detach(this, kind));
319
+ this.stdin = new Writable({
320
+ highWaterMark: group.options.sharedMaxBufferBytes,
321
+ write: (chunk, encoding, callback) => {
322
+ if (!this.active) {
323
+ callback(group.manager.error('SHARED_DETACHED', 'transport', 'unsent', group.generation));
324
+ return;
325
+ }
326
+ this.lines.write(chunk);
327
+ callback();
328
+ },
329
+ final: (callback) => {
330
+ group.detach(this, this.lines.buffer.length ? 'SHARED_INVALID_JSON' : 'SHARED_DETACHED');
331
+ callback();
332
+ },
333
+ });
334
+ this.stdin.on('error', () => group.detach(this, 'SHARED_INPUT_CLOSED'));
335
+ this.stdout.on('error', () => group.detach(this, 'SHARED_OUTPUT_CLOSED'));
336
+ this.stdout.on('close', () => group.detach(this, 'SHARED_OUTPUT_CLOSED'));
337
+ this.initTimer = setTimeout(() => group.detach(this, 'SHARED_SESSION_INIT_TIMEOUT'),
338
+ group.manager.initTimeoutMs);
339
+ }
340
+
341
+ writeMessage(message) {
342
+ if (!this.active) {
343
+ throw this.group.manager.error('SHARED_DETACHED', 'admission', 'unsent', this.group.generation);
344
+ }
345
+ this.group.clientMessage(this, message);
346
+ }
347
+
348
+ send(message) {
349
+ if (!this.active ||
350
+ this.outputFailed) return false;
351
+ let line;
352
+ try {
353
+ line = `${JSON.stringify(message)}\n`;
354
+ } catch {
355
+ this.outputFailed = true;
356
+ this.group.detach(this, 'SHARED_SERIALIZATION_FAILED');
357
+ return false;
358
+ }
359
+ if (this.stdout.readableLength + Buffer.byteLength(line) > this.group.options.sharedMaxBufferBytes) {
360
+ this.group.detach(this, 'SHARED_OUTPUT_LIMIT');
361
+ return false;
362
+ }
363
+ this.stdout.push(line);
364
+ return true;
365
+ }
366
+
367
+ finish(kind) {
368
+ if (!this.active) return;
369
+ this.active = false;
370
+ this.phase = 'closed';
371
+ this.lines.close();
372
+ clearTimeout(this.initTimer);
373
+ this.ids.clear();
374
+ this.retainedBytes = 0;
375
+ this.cursors.clear();
376
+ this.pending.clear();
377
+ this.stdin.destroy();
378
+ this.stdout.push(null);
379
+ this.stderr.push(null);
380
+ const expected = ['SHARED_DETACHED', 'SHARED_RECYCLED', 'SHARED_REMOVED', 'SHARED_SHUTDOWN'].includes(kind);
381
+ this.exitCode = expected ? 0 : 1;
382
+ const detail = this.group.manager.detail(kind, 'session', 'none', this.group.generation);
383
+ queueMicrotask(() => {
384
+ this.emit('sharedFailure', detail);
385
+ this.emit('exit', this.exitCode, this.signalCode);
386
+ this.emit('close', this.exitCode, this.signalCode);
387
+ });
388
+ }
389
+
390
+ detach() {
391
+ return this.group.detach(this, 'SHARED_DETACHED');
392
+ }
393
+ }
394
+
395
+ class SharedGroup {
396
+ constructor(manager, name, definition, params, configKey, initKey, options, generation) {
397
+ Object.assign(this, { manager, name, definition, params, configKey, initKey, options, generation });
398
+ this.state = 'starting';
399
+ this.members = new Set();
400
+ this.waiters = 0;
401
+ this.operations = new Map();
402
+ this.progress = new Map();
403
+ this.lanes = [];
404
+ this.queued = 0;
405
+ this.queuedBytes = 0;
406
+ this.sequence = 0n;
407
+ this.prefix = randomUUID();
408
+ this.initId = this.id('init');
409
+ this.failed = false;
410
+ this.reported = false;
411
+ this.killRequested = false;
412
+ this.blocked = false;
413
+ this.pumping = false;
414
+ this.ready = new Promise((resolve, reject) => {
415
+ this.resolveReady = resolve;
416
+ this.rejectReady = reject;
417
+ });
418
+ this.lines = new JsonLines(options.sharedMaxLineBytes,
419
+ (message) => this.childMessage(message), (kind) => this.stop(kind, true));
420
+ }
421
+
422
+ id(prefix) {
423
+ return `${this.prefix}:${this.generation}:${prefix}:${++this.sequence}`;
424
+ }
425
+
426
+ start() {
427
+ this.spawnedAt = Date.now();
428
+ this.initTimer = setTimeout(() => this.stop('SHARED_INIT_TIMEOUT', true), this.manager.initTimeoutMs);
429
+ Promise.resolve().then(() => this.manager.spawn(this.name, this.definition)).then((child) => {
430
+ this.child = child;
431
+ this.listen(child);
432
+ if (this.state === 'stopping') {
433
+ this.kill();
434
+ return;
435
+ }
436
+ const validChild = child.stdin?.writable && child.stdout?.readable &&
437
+ child.exitCode == null && child.signalCode == null;
438
+ if (!validChild) {
439
+ this.stop('SHARED_CHILD_UNAVAILABLE', true);
440
+ return;
441
+ }
442
+ const params = structuredClone(this.params);
443
+ if (Object.hasOwn(params._meta ?? {}, 'progressToken')) {
444
+ params._meta.progressToken = this.id('init-progress');
445
+ }
446
+ this.write({ jsonrpc: '2.0', id: this.initId, method: 'initialize', params });
447
+ }).catch(() => {
448
+ if (this.child) {
449
+ this.stop('SHARED_SPAWN_FAILED', true);
450
+ } else {
451
+ this.stop('SHARED_SPAWN_FAILED', true);
452
+ this.dead();
453
+ }
454
+ });
455
+ }
456
+
457
+ listen(child) {
458
+ child.once('exit', () => this.exited());
459
+ child.once('close', () => this.exited());
460
+ child.on('error', () => this.stop('SHARED_CHILD_ERROR', true));
461
+ child.stdin?.on('error', () => this.stop('SHARED_STDIN_ERROR', true));
462
+ child.stdin?.on('drain', () => {
463
+ this.blocked = false;
464
+ this.pump();
465
+ });
466
+ child.stdout?.on('data', (chunk) => this.lines.write(chunk));
467
+ child.stdout?.on('error', () => this.stop('SHARED_STDOUT_ERROR', true));
468
+ child.stdout?.on('end', () => {
469
+ if (this.state !== 'stopping' &&
470
+ this.state !== 'dead') {
471
+ this.stop('SHARED_STDOUT_CLOSED', true);
472
+ }
473
+ });
474
+ child.stderr?.resume();
475
+ }
476
+
477
+ write(message) {
478
+ if (!this.child ||
479
+ this.state === 'dead') return false;
480
+ let line;
481
+ try {
482
+ line = `${JSON.stringify(message)}\n`;
483
+ } catch {
484
+ this.stop('SHARED_SERIALIZATION_FAILED', true);
485
+ return false;
486
+ }
487
+ const bytes = Buffer.byteLength(line);
488
+ if (bytes > this.options.sharedMaxLineBytes ||
489
+ this.child.stdin.writableLength + bytes > this.options.sharedMaxBufferBytes) {
490
+ this.stop('SHARED_UPSTREAM_BUFFER_LIMIT', true);
491
+ return false;
492
+ }
493
+ try {
494
+ const accepted = this.child.stdin.write(line, (error) => {
495
+ if (error) this.stop('SHARED_STDIN_ERROR', true);
496
+ });
497
+ if (!accepted) this.blocked = true;
498
+ return true;
499
+ } catch {
500
+ this.stop('SHARED_STDIN_ERROR', true);
501
+ return false;
502
+ }
503
+ }
504
+
505
+ inspect() {
506
+ return {
507
+ generation: this.generation, state: this.state, pid: this.child?.pid ?? null,
508
+ members: this.members.size, waiters: this.waiters,
509
+ unresolved: this.operations.size, queued: this.queued,
510
+ };
511
+ }
512
+
513
+ clientMessage(owner, message) {
514
+ if (!owner.active) return;
515
+ const valid = this.manager.record(message) && message.jsonrpc === '2.0' &&
516
+ typeof message.method === 'string' && !Object.hasOwn(message, 'result') &&
517
+ !Object.hasOwn(message, 'error');
518
+ if (!valid) {
519
+ this.detach(owner, 'SHARED_INVALID_MESSAGE');
520
+ return;
521
+ }
522
+ if (!Object.hasOwn(message, 'id')) {
523
+ if (this.checkClientInput(owner, message)) this.notification(owner, message);
524
+ return;
525
+ }
526
+ if (!this.manager.validId(message.id)) {
527
+ this.detach(owner, 'SHARED_INVALID_ID');
528
+ return;
529
+ }
530
+ if (owner.ids.has(message.id)) {
531
+ this.detach(owner, 'SHARED_DUPLICATE_ID');
532
+ return;
533
+ }
534
+ if (owner.ids.size >= this.options.sharedMaxIds) {
535
+ this.detach(owner, 'SHARED_ID_LIMIT');
536
+ return;
537
+ }
538
+ const idBytes = Buffer.byteLength(JSON.stringify(message.id));
539
+ if (owner.retainedBytes + idBytes > this.options.sharedMaxBufferBytes) {
540
+ this.detach(owner, 'SHARED_RETAINED_LIMIT');
541
+ return;
542
+ }
543
+ owner.retainedBytes += idBytes;
544
+ owner.ids.add(message.id);
545
+ if (!this.checkClientInput(owner, message)) return;
546
+ if (message.method === 'initialize') {
547
+ let key;
548
+ try {
549
+ key = this.manager.canonical(message.params);
550
+ } catch {
551
+ this.reject(owner, message.id, 'SHARED_INCOMPATIBLE', -32003);
552
+ return;
553
+ }
554
+ if (owner.phase !== 'attached' ||
555
+ key !== this.initKey) {
556
+ this.reject(owner, message.id, 'SHARED_INCOMPATIBLE', -32003);
557
+ return;
558
+ }
559
+ owner.phase = 'initialized';
560
+ owner.send({ jsonrpc: '2.0', id: message.id, result: this.result });
561
+ return;
562
+ }
563
+ if (owner.phase !== 'ready') {
564
+ this.reject(owner, message.id, 'SHARED_NOT_INITIALIZED');
565
+ return;
566
+ }
567
+ if (this.state !== 'ready') {
568
+ this.reject(owner, message.id, 'SHARED_DRAINING');
569
+ return;
570
+ }
571
+ const allowed = ['tools/list', 'tools/call', 'ping'].includes(message.method);
572
+ const paramsValid = message.params === undefined || this.manager.record(message.params);
573
+ const task = Object.hasOwn(message.params ?? {}, 'task');
574
+ if (!allowed ||
575
+ !paramsValid ||
576
+ task) {
577
+ this.reject(owner, message.id, 'SHARED_UNSUPPORTED_METHOD', -32601);
578
+ return;
579
+ }
580
+ if (message.method.startsWith('tools/') &&
581
+ !this.result.capabilities.tools) {
582
+ this.reject(owner, message.id, 'SHARED_UNSUPPORTED_METHOD', -32601);
583
+ return;
584
+ }
585
+ let request;
586
+ try {
587
+ request = structuredClone(message);
588
+ } catch {
589
+ this.reject(owner, message.id, 'SHARED_CLONE_FAILED', -32602);
590
+ return;
591
+ }
592
+ if (message.method === 'tools/list' &&
593
+ Object.hasOwn(message.params ?? {}, 'cursor')) {
594
+ const cursor = owner.cursors.get(message.params.cursor);
595
+ if (cursor === undefined) {
596
+ this.reject(owner, message.id, 'SHARED_CURSOR_INVALID', -32003);
597
+ return;
598
+ }
599
+ request.params.cursor = cursor;
600
+ }
601
+ const token = message.params?._meta?.progressToken;
602
+ if (token !== undefined &&
603
+ !this.manager.validId(token)) {
604
+ this.reject(owner, message.id, 'SHARED_INVALID_PROGRESS', -32602);
605
+ return;
606
+ }
607
+ if (token !== undefined &&
608
+ [...owner.pending.values()].some((operation) => operation.token === token)) {
609
+ this.reject(owner, message.id, 'SHARED_DUPLICATE_PROGRESS', -32602);
610
+ return;
611
+ }
612
+ const canDispatch = !this.blocked && this.operations.size < this.options.sharedMaxInFlight &&
613
+ this.queued === 0;
614
+ if (!canDispatch &&
615
+ this.queued >= this.options.sharedMaxQueued) {
616
+ this.reject(owner, message.id, 'SHARED_QUEUE_LIMIT', -32004);
617
+ return;
618
+ }
619
+ request.id = this.id('request');
620
+ const operation = { owner, originalId: message.id, request, token, dispatched: false, settled: false };
621
+ if (token !== undefined) {
622
+ operation.progressId = this.id('progress');
623
+ request.params._meta.progressToken = operation.progressId;
624
+ }
625
+ operation.bytes = Buffer.byteLength(JSON.stringify(request)) + 1;
626
+ if (operation.bytes > this.options.sharedMaxLineBytes ||
627
+ operation.bytes > this.options.sharedMaxBufferBytes) {
628
+ this.reject(owner, message.id, 'SHARED_REQUEST_LIMIT', -32004);
629
+ return;
630
+ }
631
+ if (!canDispatch &&
632
+ this.queuedBytes + operation.bytes > this.options.sharedMaxBufferBytes) {
633
+ this.reject(owner, message.id, 'SHARED_QUEUE_LIMIT', -32004);
634
+ return;
635
+ }
636
+ operation.timer = setTimeout(() => this.timeout(operation), this.manager.requestTimeoutMs);
637
+ owner.pending.set(message.id, operation);
638
+ if (canDispatch) {
639
+ this.dispatch(operation);
640
+ } else {
641
+ owner.queue.push(operation);
642
+ this.queued++;
643
+ this.queuedBytes += operation.bytes;
644
+ if (!this.lanes.includes(owner)) this.lanes.push(owner);
645
+ this.pump();
646
+ }
647
+ }
648
+
649
+ checkClientInput(owner, message) {
650
+ let kind;
651
+ if (!this.manager.withinDepth(message)) {
652
+ kind = 'SHARED_DEPTH_LIMIT';
653
+ } else {
654
+ try {
655
+ if (Buffer.byteLength(JSON.stringify(message)) > this.options.sharedMaxLineBytes) {
656
+ kind = 'SHARED_INPUT_LIMIT';
657
+ }
658
+ } catch {
659
+ kind = 'SHARED_SERIALIZATION_FAILED';
660
+ }
661
+ }
662
+ if (!kind) return true;
663
+ if (Object.hasOwn(message, 'id')) this.reject(owner, message.id, kind, -32602);
664
+ else this.detach(owner, kind);
665
+ return false;
666
+ }
667
+
668
+ reject(owner, id, kind, code = -32002) {
669
+ owner.send(this.manager.rpcError(id,
670
+ this.manager.detail(kind, 'admission', 'unsent', this.generation), code));
671
+ }
672
+
673
+ notification(owner, message) {
674
+ if (message.method === 'notifications/initialized') {
675
+ if (owner.phase !== 'initialized') {
676
+ this.detach(owner, 'SHARED_NOT_INITIALIZED');
677
+ return;
678
+ }
679
+ owner.phase = 'ready';
680
+ clearTimeout(owner.initTimer);
681
+ return;
682
+ }
683
+ if (message.method !== 'notifications/cancelled') {
684
+ this.detach(owner, 'SHARED_UNSUPPORTED_NOTIFICATION');
685
+ return;
686
+ }
687
+ const operation = owner.pending.get(message.params?.requestId);
688
+ if (!operation) return;
689
+ if (operation.dispatched) {
690
+ this.cancel(operation);
691
+ this.settle(operation, 'SHARED_CANCELLED', -32800);
692
+ } else {
693
+ this.unqueue(operation);
694
+ this.settle(operation, 'SHARED_CANCELLED', -32800);
695
+ this.pump();
696
+ }
697
+ }
698
+
699
+ dispatch(operation) {
700
+ if (this.child.stdin.writableLength + operation.bytes > this.options.sharedMaxBufferBytes) {
701
+ this.settle(operation, 'SHARED_UPSTREAM_BUFFER_LIMIT', -32004);
702
+ return;
703
+ }
704
+ operation.dispatched = true;
705
+ this.operations.set(operation.request.id, operation);
706
+ if (operation.progressId) this.progress.set(operation.progressId, operation);
707
+ this.write(operation.request);
708
+ }
709
+
710
+ pump() {
711
+ if (this.pumping) return;
712
+ this.pumping = true;
713
+ try {
714
+ while (this.state === 'ready' &&
715
+ !this.blocked &&
716
+ this.operations.size < this.options.sharedMaxInFlight &&
717
+ this.lanes.length) {
718
+ const owner = this.lanes.shift();
719
+ const operation = owner.queue.shift();
720
+ this.queued--;
721
+ this.queuedBytes -= operation.bytes;
722
+ if (owner.queue.length) this.lanes.push(owner);
723
+ this.dispatch(operation);
724
+ }
725
+ } finally {
726
+ this.pumping = false;
727
+ }
728
+ }
729
+
730
+ unqueue(operation) {
731
+ const queue = operation.owner.queue;
732
+ const index = queue.indexOf(operation);
733
+ if (index < 0) return;
734
+ queue.splice(index, 1);
735
+ this.queued--;
736
+ this.queuedBytes -= operation.bytes;
737
+ if (!queue.length) this.lanes = this.lanes.filter((owner) => owner !== operation.owner);
738
+ }
739
+
740
+ settle(operation, kind, code = -32002) {
741
+ if (operation.settled) return;
742
+ operation.settled = true;
743
+ const execution = operation.dispatched ? 'potentially-executed' : 'unsent';
744
+ operation.owner.send(this.manager.rpcError(operation.originalId,
745
+ this.manager.detail(kind, operation.dispatched ? 'execution' : 'queue', execution, this.generation), code));
746
+ if (!operation.dispatched) {
747
+ clearTimeout(operation.timer);
748
+ operation.owner.pending.delete(operation.originalId);
749
+ }
750
+ }
751
+
752
+ cancel(operation) {
753
+ if (operation.cancelled) return;
754
+ operation.cancelled = true;
755
+ this.write({ jsonrpc: '2.0', method: 'notifications/cancelled',
756
+ params: { requestId: operation.request.id } });
757
+ }
758
+
759
+ timeout(operation) {
760
+ if (operation.dispatched) {
761
+ this.settle(operation, 'SHARED_REQUEST_TIMEOUT', -32001);
762
+ this.cancel(operation);
763
+ this.drain('SHARED_REQUEST_TIMEOUT', true);
764
+ } else {
765
+ this.unqueue(operation);
766
+ this.settle(operation, 'SHARED_QUEUE_TIMEOUT', -32001);
767
+ this.pump();
768
+ }
769
+ }
770
+
771
+ childMessage(message) {
772
+ if (this.state === 'stopping' ||
773
+ this.state === 'dead') return;
774
+ if (!this.manager.withinDepth(message)) {
775
+ this.stop('SHARED_UPSTREAM_DEPTH_LIMIT', true);
776
+ return;
777
+ }
778
+ if (!this.manager.record(message) ||
779
+ message.jsonrpc !== '2.0') {
780
+ this.stop('SHARED_INVALID_MESSAGE', true);
781
+ return;
782
+ }
783
+ if (Object.hasOwn(message, 'method')) {
784
+ if (typeof message.method !== 'string' ||
785
+ Object.hasOwn(message, 'result') ||
786
+ Object.hasOwn(message, 'error')) {
787
+ this.stop('SHARED_INVALID_MESSAGE', true);
788
+ return;
789
+ }
790
+ if (Object.hasOwn(message, 'id')) {
791
+ this.callback(message);
792
+ } else {
793
+ this.childNotification(message);
794
+ }
795
+ return;
796
+ }
797
+ const response = this.manager.validId(message.id) &&
798
+ (Object.hasOwn(message, 'result') !== Object.hasOwn(message, 'error'));
799
+ const validError = !Object.hasOwn(message, 'error') ||
800
+ (this.manager.record(message.error) && Number.isSafeInteger(message.error.code) &&
801
+ typeof message.error.message === 'string');
802
+ if (!response ||
803
+ !validError) {
804
+ this.stop('SHARED_INVALID_MESSAGE', true);
805
+ return;
806
+ }
807
+ if (message.id === this.initId &&
808
+ this.state === 'starting') {
809
+ this.initialized(message);
810
+ return;
811
+ }
812
+ const operation = this.operations.get(message.id);
813
+ if (!operation) return;
814
+ this.operations.delete(message.id);
815
+ this.progress.delete(operation.progressId);
816
+ clearTimeout(operation.timer);
817
+ operation.owner.pending.delete(operation.originalId);
818
+ if (!operation.settled &&
819
+ operation.owner.active) {
820
+ const response = { ...message, id: operation.originalId };
821
+ if (operation.request.method === 'tools/list' &&
822
+ this.manager.record(response.result) &&
823
+ Object.hasOwn(response.result, 'nextCursor')) {
824
+ if (typeof response.result.nextCursor !== 'string') {
825
+ this.settle(operation, 'SHARED_CURSOR_INVALID', -32003);
826
+ this.stop('SHARED_CURSOR_INVALID', true);
827
+ return;
828
+ }
829
+ if (operation.owner.cursors.size >= this.options.sharedMaxCursors) {
830
+ this.settle(operation, 'SHARED_CURSOR_LIMIT', -32004);
831
+ } else {
832
+ const cursor = this.id('cursor');
833
+ const bytes = Buffer.byteLength(cursor) + Buffer.byteLength(response.result.nextCursor);
834
+ if (operation.owner.retainedBytes + bytes > this.options.sharedMaxBufferBytes) {
835
+ this.settle(operation, 'SHARED_RETAINED_LIMIT', -32004);
836
+ } else {
837
+ operation.owner.retainedBytes += bytes;
838
+ operation.owner.cursors.set(cursor, response.result.nextCursor);
839
+ response.result = { ...response.result, nextCursor: cursor };
840
+ }
841
+ }
842
+ }
843
+ if (!operation.settled) operation.owner.send(response);
844
+ }
845
+ this.acceptedResponse();
846
+ this.pump();
847
+ this.idle();
848
+ }
849
+
850
+ acceptedResponse() {
851
+ const failed = () => this.manager.log(this.name, 'shared: onResponse callback failed');
852
+ try {
853
+ Promise.resolve(this.manager.onResponse(this.name)).catch(failed);
854
+ } catch {
855
+ failed();
856
+ }
857
+ }
858
+
859
+ initialized(message) {
860
+ if (Object.hasOwn(message, 'error')) {
861
+ const error = this.manager.error('SHARED_INIT_REJECTED', 'initialize', 'unsent', this.generation);
862
+ error.upstreamError = message.error;
863
+ this.rejectReady(error);
864
+ this.stop('SHARED_INIT_REJECTED', true);
865
+ return;
866
+ }
867
+ const result = message.result;
868
+ const valid = this.manager.record(result) && VERSIONS.has(result.protocolVersion) &&
869
+ this.manager.record(result.serverInfo) && typeof result.serverInfo.name === 'string' &&
870
+ typeof result.serverInfo.version === 'string' && this.manager.validCapabilityRecord(result.capabilities);
871
+ if (!valid) {
872
+ this.stop('SHARED_INIT_INCOMPATIBLE', true);
873
+ return;
874
+ }
875
+ this.result = { ...result, capabilities: Object.hasOwn(result.capabilities, 'tools')
876
+ ? { tools: result.capabilities.tools } : {} };
877
+ if (!this.write({ jsonrpc: '2.0', method: 'notifications/initialized' })) return;
878
+ if (this.state !== 'starting') return;
879
+ this.state = 'ready';
880
+ clearTimeout(this.initTimer);
881
+ this.resolveReady();
882
+ this.idle();
883
+ this.sampleInitialization();
884
+ }
885
+
886
+ sampleInitialization() {
887
+ const completedAt = Date.now();
888
+ const spawnedAt = this.child.__spawnedAt;
889
+ if (!Number.isSafeInteger(spawnedAt) ||
890
+ spawnedAt < 0 ||
891
+ spawnedAt > completedAt) {
892
+ this.manager.log(this.name, 'shared: initialization latency unavailable; invalid child __spawnedAt');
893
+ return;
894
+ }
895
+ const failed = () => this.manager.log(this.name, 'shared: onInitialized callback failed');
896
+ try {
897
+ Promise.resolve(this.manager.onInitialized(this.name, completedAt - spawnedAt)).catch(failed);
898
+ } catch {
899
+ failed();
900
+ }
901
+ }
902
+
903
+ callback(message) {
904
+ if (!this.manager.validId(message.id)) {
905
+ this.stop('SHARED_INVALID_ID', true);
906
+ return;
907
+ }
908
+ if (message.method === 'ping') {
909
+ this.write({ jsonrpc: '2.0', id: message.id, result: {} });
910
+ return;
911
+ }
912
+ this.write(this.manager.rpcError(message.id,
913
+ this.manager.detail('SHARED_UNSUPPORTED_CALLBACK', 'callback', 'none', this.generation), -32601));
914
+ this.stop('SHARED_UNSUPPORTED_CALLBACK', true);
915
+ }
916
+
917
+ childNotification(message) {
918
+ if (message.method === 'notifications/progress') {
919
+ const operation = this.progress.get(message.params?.progressToken);
920
+ if (!operation ||
921
+ operation.settled ||
922
+ !operation.owner.active) return;
923
+ const notification = { ...message, params: { ...message.params, progressToken: operation.token } };
924
+ // The HTTP adapter uses this event to bind progress to the original POST, not GET SSE.
925
+ if (operation.owner.listenerCount('sharedProgress') === 0) {
926
+ this.detach(operation.owner, 'SHARED_PROGRESS_UNROUTABLE');
927
+ return;
928
+ }
929
+ try {
930
+ operation.owner.emit('sharedProgress', { requestId: operation.originalId, message: notification });
931
+ } catch {
932
+ this.detach(operation.owner, 'SHARED_PROGRESS_DELIVERY_FAILED');
933
+ }
934
+ return;
935
+ }
936
+ if (message.method === 'notifications/tools/list_changed' &&
937
+ this.result?.capabilities.tools?.listChanged === true) {
938
+ for (const owner of this.members) {
939
+ if (owner.phase === 'ready') owner.send({ jsonrpc: '2.0', method: message.method });
940
+ }
941
+ return;
942
+ }
943
+ if (message.method === 'notifications/message') {
944
+ this.manager.log(this.name, 'shared: unowned log notification dropped');
945
+ return;
946
+ }
947
+ if (message.method === 'notifications/resources/list_changed' ||
948
+ message.method === 'notifications/prompts/list_changed') {
949
+ this.manager.log(this.name, `shared: unexposed catalog notification dropped: ${message.method}`);
950
+ return;
951
+ }
952
+ this.stop('SHARED_UNSUPPORTED_NOTIFICATION', true);
953
+ }
954
+
955
+ detach(owner, kind) {
956
+ if (!owner.active) return false;
957
+ this.members.delete(owner);
958
+ for (const operation of [...owner.queue]) {
959
+ this.unqueue(operation);
960
+ this.settle(operation, kind);
961
+ }
962
+ for (const operation of [...owner.pending.values()]) {
963
+ if (operation.dispatched) {
964
+ this.settle(operation, kind);
965
+ this.cancel(operation);
966
+ }
967
+ }
968
+ owner.finish(kind);
969
+ this.pump();
970
+ this.idle();
971
+ return true;
972
+ }
973
+
974
+ idle() {
975
+ clearTimeout(this.lingerTimer);
976
+ if (this.state === 'draining' &&
977
+ this.operations.size === 0) {
978
+ this.stop(this.reason, this.failed);
979
+ return;
980
+ }
981
+ if (this.state === 'ready' &&
982
+ this.members.size === 0 &&
983
+ this.waiters === 0 &&
984
+ this.operations.size === 0) {
985
+ this.lingerTimer = setTimeout(() => this.stop('SHARED_LINGER_EXPIRED', false), this.options.sharedLingerMs);
986
+ this.lingerTimer.unref();
987
+ }
988
+ }
989
+
990
+ drain(kind, failed = false) {
991
+ if (this.state === 'stopping' ||
992
+ this.state === 'dead' ||
993
+ this.state === 'draining') return;
994
+ if (this.state === 'starting') {
995
+ this.stop(kind, failed);
996
+ return;
997
+ }
998
+ this.state = 'draining';
999
+ this.reason = kind;
1000
+ this.failed = failed;
1001
+ clearTimeout(this.lingerTimer);
1002
+ for (const owner of this.members) {
1003
+ for (const operation of [...owner.queue]) {
1004
+ this.unqueue(operation);
1005
+ this.settle(operation, 'SHARED_DRAINING');
1006
+ }
1007
+ }
1008
+ this.drainTimer = setTimeout(() => this.stop('SHARED_DRAIN_TIMEOUT', true),
1009
+ this.options.sharedDrainTimeoutMs);
1010
+ this.manager.emit('draining', { name: this.name, ...this.inspect(), reason: kind });
1011
+ this.idle();
1012
+ }
1013
+
1014
+ report(kind) {
1015
+ if (this.state === 'dead') return;
1016
+ this.failed = true;
1017
+ if (this.reported) return;
1018
+ this.reported = true;
1019
+ const detail = this.manager.detail(kind, this.state, 'potentially-executed', this.generation);
1020
+ this.manager.onFailure(this.name, detail);
1021
+ this.manager.emit('failure', { name: this.name, ...detail });
1022
+ }
1023
+
1024
+ stop(kind, failed) {
1025
+ if (this.state === 'dead' ||
1026
+ this.state === 'stopping') return;
1027
+ this.state = 'stopping';
1028
+ this.reason = kind;
1029
+ clearTimeout(this.initTimer);
1030
+ clearTimeout(this.lingerTimer);
1031
+ clearTimeout(this.drainTimer);
1032
+ this.lines.close();
1033
+ this.rejectReady(this.manager.error(kind, 'initialize', 'unsent', this.generation));
1034
+ for (const owner of this.members) {
1035
+ for (const operation of [...owner.queue]) {
1036
+ this.unqueue(operation);
1037
+ this.settle(operation, kind);
1038
+ }
1039
+ }
1040
+ for (const operation of this.operations.values()) {
1041
+ clearTimeout(operation.timer);
1042
+ this.settle(operation, kind);
1043
+ }
1044
+ this.operations.clear();
1045
+ this.progress.clear();
1046
+ for (const owner of this.members) owner.finish(kind);
1047
+ this.members.clear();
1048
+ if (failed) this.report(kind);
1049
+ this.kill();
1050
+ }
1051
+
1052
+ kill() {
1053
+ if (!this.child ||
1054
+ this.killRequested ||
1055
+ this.state === 'dead') return;
1056
+ this.killRequested = true;
1057
+ if (this.child.exitCode != null ||
1058
+ this.child.signalCode != null) {
1059
+ this.dead();
1060
+ return;
1061
+ }
1062
+ this.stopTimer = setTimeout(() => this.report('SHARED_KILL_TIMEOUT'), this.options.sharedDrainTimeoutMs);
1063
+ this.stopTimer.unref();
1064
+ try {
1065
+ Promise.resolve(this.manager.kill(this.child)).catch(() => this.report('SHARED_KILL_FAILED'));
1066
+ } catch {
1067
+ this.report('SHARED_KILL_FAILED');
1068
+ }
1069
+ }
1070
+
1071
+ exited() {
1072
+ if (this.state === 'dead') return;
1073
+ if (this.state !== 'stopping') this.stop('SHARED_CHILD_EXIT', true);
1074
+ this.dead();
1075
+ }
1076
+
1077
+ dead() {
1078
+ if (this.state === 'dead') return;
1079
+ clearTimeout(this.stopTimer);
1080
+ this.state = 'dead';
1081
+ this.manager.retired(this);
1082
+ }
1083
+ }