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,129 @@
1
+ export const POOLING_RELOAD_DELAY_MS = 5000;
2
+
3
+ export class PoolingBatchScheduler {
4
+ #reload;
5
+ #onError;
6
+ #clock;
7
+ #pending;
8
+ #running;
9
+ #timer;
10
+ #closed = false;
11
+
12
+ constructor({ reload, onError, clock = {
13
+ now: () => performance.now(),
14
+ wallNow: () => Date.now(),
15
+ setTimeout,
16
+ clearTimeout,
17
+ } }) {
18
+ if (typeof reload !== 'function' ||
19
+ typeof onError !== 'function') {
20
+ throw new TypeError('Pooling reload and error callbacks are required.');
21
+ }
22
+ this.#reload = reload;
23
+ this.#onError = onError;
24
+ this.#clock = clock;
25
+ }
26
+
27
+ schedule(generation) {
28
+ if (this.#closed) throw new Error('Pooling batch scheduler is closed.');
29
+ if (generation === undefined ||
30
+ generation === null) {
31
+ throw new TypeError('An accepted pending generation is required.');
32
+ }
33
+ this.#pending = {
34
+ generation,
35
+ deadline: this.#clock.now() + POOLING_RELOAD_DELAY_MS,
36
+ };
37
+ this.#arm();
38
+ return this.snapshot();
39
+ }
40
+
41
+ snapshot() {
42
+ const remainingMs = this.#pending
43
+ ? Math.max(0, Math.ceil(this.#pending.deadline - this.#clock.now())) : null;
44
+ return {
45
+ pending: this.#pending ? {
46
+ generation: this.#pending.generation,
47
+ remainingMs,
48
+ applyAt: this.#clock.wallNow() + remainingMs,
49
+ } : null,
50
+ applying: this.#running?.generation ?? null,
51
+ };
52
+ }
53
+
54
+ cancelPending() {
55
+ const cancelled = Boolean(this.#pending);
56
+ this.#clearTimer();
57
+ this.#pending = undefined;
58
+ return cancelled;
59
+ }
60
+
61
+ reloadNow() {
62
+ if (this.#closed) return Promise.reject(new Error('Pooling batch scheduler is closed.'));
63
+ this.#clearTimer();
64
+ if (this.#running) {
65
+ return this.#pending
66
+ ? this.#running.promise.then(() => this.reloadNow()) : this.#running.promise;
67
+ }
68
+ return this.#pending ? this.#startReload() : Promise.resolve(null);
69
+ }
70
+
71
+ close() {
72
+ this.#closed = true;
73
+ this.cancelPending();
74
+ return this.#running?.promise ?? Promise.resolve(null);
75
+ }
76
+
77
+ #clearTimer() {
78
+ if (this.#timer !== undefined) {
79
+ this.#clock.clearTimeout(this.#timer);
80
+ this.#timer = undefined;
81
+ }
82
+ }
83
+
84
+ #arm() {
85
+ this.#clearTimer();
86
+ if (this.#closed ||
87
+ !this.#pending) return;
88
+ const pending = this.#pending;
89
+ this.#timer = this.#clock.setTimeout(() => {
90
+ if (this.#closed ||
91
+ this.#pending !== pending) return;
92
+ this.#timer = undefined;
93
+ if (this.#running) return;
94
+ // Timed reloads report through onError; explicit reload callers also receive the rejection.
95
+ this.#startReload().catch(() => {});
96
+ }, Math.max(0, pending.deadline - this.#clock.now()));
97
+ this.#timer?.unref?.();
98
+ }
99
+
100
+ #startReload() {
101
+ const { generation } = this.#pending;
102
+ this.#clearTimer();
103
+ this.#pending = undefined;
104
+ const running = { generation, promise: undefined };
105
+ this.#running = running;
106
+ running.promise = Promise.resolve()
107
+ .then(() => this.#reload(generation))
108
+ .catch((error) => {
109
+ this.#reportError(error, generation);
110
+ throw error;
111
+ })
112
+ .finally(() => {
113
+ this.#running = undefined;
114
+ this.#arm();
115
+ });
116
+ return running.promise;
117
+ }
118
+
119
+ #reportError(error, generation) {
120
+ const reportFailure = () => process.emitWarning('Pooling reload error observer failed.', {
121
+ code: 'MCP_POOLING_ERROR_OBSERVER_FAILED',
122
+ });
123
+ try {
124
+ Promise.resolve(this.#onError(error, generation)).catch(reportFailure);
125
+ } catch {
126
+ reportFailure();
127
+ }
128
+ }
129
+ }
@@ -0,0 +1,270 @@
1
+ import { PoolingConfigError, POOLING_SAVE_WARNING } from './pooling-config.mjs';
2
+ import { PoolingConfigWriter } from './pooling-writer.mjs';
3
+ import { PoolingBatchScheduler, POOLING_RELOAD_DELAY_MS } from './pooling-batch-scheduler.mjs';
4
+ import { POOLING_BUDGET_MS } from './pooling-execution.mjs';
5
+
6
+ export class PoolingBatches {
7
+ #writer;
8
+ #reload;
9
+ #revision;
10
+ #notify;
11
+ #captureSnapshot;
12
+ #onError;
13
+ #scheduler;
14
+ #tail = Promise.resolve();
15
+ #queued = 0;
16
+ #records = new Map();
17
+ #pending;
18
+ #applying;
19
+ #closed = false;
20
+ #closing;
21
+ #watchReload;
22
+
23
+ constructor({ configPath, reload, revision, notify, captureSnapshot, onError }) {
24
+ this.#writer = new PoolingConfigWriter(configPath);
25
+ this.#reload = reload;
26
+ this.#revision = revision;
27
+ this.#notify = notify;
28
+ this.#captureSnapshot = captureSnapshot;
29
+ this.#onError = onError;
30
+ this.#scheduler = new PoolingBatchScheduler({
31
+ reload: (generation) => this.#enqueue(() => this.#commit(generation)),
32
+ onError: (error) => this.#onError(error),
33
+ });
34
+ }
35
+
36
+ snapshot() {
37
+ const scheduled = this.#scheduler.snapshot();
38
+ const batches = [...this.#records.values()].reverse().map((record) => ({
39
+ ...record,
40
+ changes: record.changes.map((change) => ({ ...change })),
41
+ applyAt: record.status === 'pending' &&
42
+ record.id === this.#pending?.batchId ? scheduled.pending?.applyAt ?? null : null,
43
+ }));
44
+ return {
45
+ batchDelayMs: POOLING_RELOAD_DELAY_MS, batches,
46
+ ...(process.platform === 'win32' ? { saveWarning: POOLING_SAVE_WARNING } : {}),
47
+ };
48
+ }
49
+
50
+ stage(request, options = {}) {
51
+ if (this.#closed) return Promise.reject(this.#closedError());
52
+ if (this.#queued >= 16) {
53
+ return Promise.reject(new PoolingConfigError(503, 'WRITER_BUSY', 'Too many pending pooling changes.'));
54
+ }
55
+ const deadline = options.deadline ??
56
+ process.hrtime.bigint() + BigInt(POOLING_BUDGET_MS) * 1000000n;
57
+ const arrivingDuring = this.#applying;
58
+ options.trace?.record('queue-enter', { queued: this.#queued, applying: Boolean(this.#applying) });
59
+ this.#queued++;
60
+ return this.#enqueue(async () => {
61
+ options.trace?.record('queue-leave');
62
+ if (this.#closed) throw this.#closedError();
63
+ let submitted = request;
64
+ if (!Object.hasOwn(request, 'undoId') &&
65
+ arrivingDuring?.committedRevision &&
66
+ request.revision === arrivingDuring.revision) {
67
+ submitted = { ...request, revision: arrivingDuring.committedRevision };
68
+ }
69
+ const method = Object.hasOwn(request, 'undoId') ? 'stageUndo' : 'stageApply';
70
+ let result;
71
+ try {
72
+ result = await this.#writer[method](submitted, {
73
+ ...options, deadline,
74
+ onLateSettlement: (settlement) => {
75
+ this.#enqueue(() => this.#lateStage(settlement))
76
+ .catch((error) => this.#onError(error));
77
+ options.onLateSettlement?.(settlement);
78
+ },
79
+ });
80
+ } catch (error) {
81
+ if (error.cancelledBatchId) {
82
+ this.#cancelledBatch(error.cancelledBatchId);
83
+ this.#notify();
84
+ }
85
+ throw error;
86
+ }
87
+ if (result.pending &&
88
+ result.accepted !== false &&
89
+ options.signal?.aborted) {
90
+ await this.#writer.rejectStage({ generation: result.generation });
91
+ throw new PoolingConfigError(408, 'WRITER_CANCELLED',
92
+ 'Pooling change was cancelled before config commit.');
93
+ }
94
+ if (this.#closed) throw this.#closedError();
95
+ if (result.cancelled) {
96
+ this.#cancelledBatch(result.batchId);
97
+ } else if (result.pending) {
98
+ this.#pending = {
99
+ batchId: result.batchId, generation: result.generation,
100
+ revision: result.revision, draftRevision: result.draftRevision,
101
+ };
102
+ this.#records.delete(result.batchId);
103
+ this.#records.set(result.batchId, {
104
+ id: result.batchId, status: 'pending', revision: result.revision,
105
+ changes: result.changes.map((change) => ({ ...change })),
106
+ });
107
+ if (result.accepted !== false) this.#scheduler.schedule(result.generation);
108
+ this.#trim();
109
+ }
110
+ this.#notify();
111
+ const { generation, draftRevision, changes, accepted, ...receipt } = result;
112
+ return this.#captureSnapshot
113
+ ? { ...receipt, snapshot: this.#captureSnapshot() }
114
+ : receipt;
115
+ }).finally(() => { this.#queued--; });
116
+ }
117
+
118
+ async reloadNow(reason = 'admin request') {
119
+ if (this.#closed) throw this.#closedError();
120
+ const deadline = process.hrtime.bigint() + BigInt(POOLING_BUDGET_MS) * 1000000n;
121
+ await this.#tail;
122
+ let result;
123
+ do {
124
+ if (process.hrtime.bigint() >= deadline) {
125
+ throw new PoolingConfigError(504, 'WRITER_DEADLINE',
126
+ 'Reload could not start within its request budget. Reread batch status.');
127
+ }
128
+ result = await this.#scheduler.reloadNow();
129
+ } while (!result && this.#pending);
130
+ return result ?? this.#enqueue(() => this.#reloadSaved(reason));
131
+ }
132
+
133
+ reloadSaved(reason) {
134
+ if (this.#closed) return Promise.resolve(null);
135
+ if (!this.#watchReload) {
136
+ this.#watchReload = this.#enqueue(() => this.#reloadSaved(reason))
137
+ .finally(() => { this.#watchReload = undefined; });
138
+ }
139
+ return this.#watchReload;
140
+ }
141
+
142
+ close() {
143
+ if (this.#closing) return this.#closing;
144
+ this.#closed = true;
145
+ this.#closing = Promise.allSettled([this.#scheduler.close(), this.#tail])
146
+ .then(() => this.#writer.close());
147
+ return this.#closing;
148
+ }
149
+
150
+ #enqueue(operation) {
151
+ const next = this.#tail.then(operation);
152
+ this.#tail = next.catch(() => {});
153
+ return next;
154
+ }
155
+
156
+ #closedError() {
157
+ return new PoolingConfigError(503, 'WRITER_CLOSED', 'Pooling changes are shutting down.');
158
+ }
159
+
160
+ #trim() {
161
+ while (this.#records.size > 32) {
162
+ const oldest = [...this.#records].find(([, record]) =>
163
+ record.status !== 'pending' && record.status !== 'applying');
164
+ if (!oldest) break;
165
+ this.#records.delete(oldest[0]);
166
+ }
167
+ }
168
+
169
+ #reloadSaved(reason) {
170
+ const result = this.#reload(reason);
171
+ if (!result.ok) {
172
+ throw new PoolingConfigError(result.statusCode ?? 500, result.code ?? 'RELOAD_FAILED',
173
+ result.publicError ?? 'Unable to activate the saved configuration.');
174
+ }
175
+ this.#notify();
176
+ return result;
177
+ }
178
+
179
+ #cancelledBatch(batchId) {
180
+ if (this.#pending?.batchId === batchId) {
181
+ this.#scheduler.cancelPending();
182
+ this.#pending = undefined;
183
+ }
184
+ const record = this.#records.get(batchId);
185
+ if (record) record.status = 'cancelled';
186
+ }
187
+
188
+ #lateStage({ cancelledBatchId, error }) {
189
+ if (cancelledBatchId) {
190
+ this.#cancelledBatch(cancelledBatchId);
191
+ this.#notify();
192
+ }
193
+ if (error &&
194
+ error.code !== 'WRITER_CANCELLED' &&
195
+ error.code !== 'WRITER_DEADLINE') {
196
+ this.#onError(error);
197
+ }
198
+ }
199
+
200
+ async #commit(generation) {
201
+ if (this.#closed ||
202
+ this.#pending?.generation !== generation) return null;
203
+ const pending = this.#pending;
204
+ const record = this.#records.get(pending.batchId);
205
+ this.#pending = undefined;
206
+ this.#applying = pending;
207
+ record.status = 'applying';
208
+ this.#notify();
209
+ try {
210
+ const committed = await this.#writer.commitBatch({
211
+ batchId: pending.batchId, generation,
212
+ }, {
213
+ onLateSettlement: (settlement) => {
214
+ this.#enqueue(() => this.#lateCommit(pending, settlement))
215
+ .catch((error) => this.#onError(error));
216
+ },
217
+ });
218
+ pending.committedRevision = committed.revision;
219
+ let reloaded;
220
+ try {
221
+ reloaded = this.#reloadSaved('pooling batch');
222
+ } catch (error) {
223
+ error.commitState = 'committed';
224
+ throw error;
225
+ }
226
+ record.status = 'applied';
227
+ record.revision = committed.revision;
228
+ this.#notify();
229
+ return reloaded;
230
+ } catch (error) {
231
+ this.#failedCommit(pending, error);
232
+ throw error;
233
+ } finally {
234
+ this.#applying = undefined;
235
+ }
236
+ }
237
+
238
+ #failedCommit(pending, error) {
239
+ let state = error.commitState ?? 'not-committed';
240
+ if (state !== 'not-committed') {
241
+ try {
242
+ this.#reloadSaved('pooling commit reconciliation');
243
+ if (this.#revision() === pending.draftRevision) {
244
+ state = 'committed';
245
+ pending.committedRevision = pending.draftRevision;
246
+ }
247
+ } catch {
248
+ this.#onError(new PoolingConfigError(500, 'RECONCILIATION_FAILED',
249
+ 'Config commit could not be reconciled. Reread batch status before any further change.'));
250
+ }
251
+ }
252
+ error.commitState = state;
253
+ const record = this.#records.get(pending.batchId);
254
+ if (record) {
255
+ record.status = 'failed';
256
+ record.commitState = state;
257
+ record.error = error instanceof PoolingConfigError
258
+ ? error.message : 'Unable to apply the pending configuration.';
259
+ if (state === 'committed') record.revision = pending.draftRevision;
260
+ }
261
+ this.#notify();
262
+ }
263
+
264
+ #lateCommit(pending, { error, result, commitStarted }) {
265
+ const failure = error ?? new PoolingConfigError(504, 'WRITER_DEADLINE_COMMITTED',
266
+ 'Config commit completed without a normal response. Reread active settings; do not replay it.');
267
+ failure.commitState ??= result?.commitState ?? (commitStarted ? 'unknown' : 'not-committed');
268
+ this.#failedCommit(pending, failure);
269
+ }
270
+ }