mcp-pacemaker 1.3.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 (40) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/CONTRIBUTING.md +156 -0
  3. package/LICENSE +21 -0
  4. package/README.md +620 -0
  5. package/SECURITY.md +110 -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 +2113 -0
  13. package/bin/pooling-config.mjs +553 -0
  14. package/bin/pooling-execution.mjs +91 -0
  15. package/bin/pooling-writer.mjs +169 -0
  16. package/bin/prewarm.mjs +152 -0
  17. package/bin/server-metrics.mjs +69 -0
  18. package/bin/service-control.mjs +124 -0
  19. package/bin/shared-sessions.mjs +1083 -0
  20. package/bin/top.mjs +115 -0
  21. package/bin/update-check.mjs +153 -0
  22. package/bin/windows/PoolingSecurityHelper.build.json +39 -0
  23. package/bin/windows/PoolingSecurityHelper.exe +0 -0
  24. package/bin/windows/src/AssemblyInfo.cs +11 -0
  25. package/bin/windows/src/PoolingSecurityHelper.cs +165 -0
  26. package/bin/windows/src/PoolingSecurityReader.cs +50 -0
  27. package/docs/shared-sessions.md +181 -0
  28. package/examples/servers.example.json +31 -0
  29. package/package.json +77 -0
  30. package/supervisor/bridge-child.mjs +14 -0
  31. package/supervisor/supervise.mjs +172 -0
  32. package/supervisor/supervise.ps1 +23 -0
  33. package/supervisor/supervise.sh +11 -0
  34. package/tools/windows-security-helper/README.md +95 -0
  35. package/tools/windows-security-helper/build.ps1 +262 -0
  36. package/ui/dist/THIRD_PARTY_NOTICES.txt +366 -0
  37. package/ui/dist/assets/index-CIy7uxfd.css +1 -0
  38. package/ui/dist/assets/index-CtaaQsB9.js +8 -0
  39. package/ui/dist/index.html +14 -0
  40. package/ui/dist/third-party-manifest.json +579 -0
@@ -0,0 +1,181 @@
1
+ # Shared stdio sessions
2
+
3
+ Status: 1.3.0 implementation contract; not a claim that every server is compatible.
4
+
5
+ ## Configuration
6
+
7
+ Add `"sharing": "shared"` to a stdio definition only after confirming the profile below.
8
+ Use `/<name>/mcp`: classic SSE endpoints explicitly reject shared mode.
9
+ Pooling buttons never enable shared mode or replace an existing shared policy.
10
+
11
+ | Setting | Default | Purpose |
12
+ |---|---|---|
13
+ | `sharedLingerMs` | 1800000 | Keep the initialized child between compatible sessions. |
14
+ | `sharedDrainTimeoutMs` | 30000 | Maximum drain time before terminating unresolved work. |
15
+ | `sharedRetryDelayMs` | 1000 | Backoff after a failed generation. |
16
+ | `sharedMaxSessions` | 128 | Virtual members plus joining clients per group. |
17
+ | `sharedMaxInFlight` | 8 | Dispatched operations per child. |
18
+ | `sharedMaxQueued` | 32 | Unsent operations waiting for dispatch. |
19
+ | `sharedMaxIds` | 4096 | Retained request IDs per virtual session. |
20
+ | `sharedMaxCursors` | 256 | Retained tools-list cursors per virtual session. |
21
+ | `sharedMaxLineBytes` | 1048576 | Maximum JSON-line size, including translated fields. |
22
+ | `sharedMaxBufferBytes` | 4194304 | Maximum buffered protocol data per stream. |
23
+
24
+ All settings require bounded integers; invalid values fail explicitly. The bridge's existing
25
+ session cap and request/initialization timeouts also apply. A slot is reserved before waiting
26
+ for shared initialization or the spawn gate. Concurrent resumes of one external session ID
27
+ share one handshake.
28
+ Client and upstream JSON have a maximum depth of 64, counting the root as depth 0.
29
+ An upstream depth violation terminates its generation with explicit failure metadata,
30
+ not an uncaught serializer exception; no business call is replayed.
31
+
32
+ ## Purpose and approval
33
+
34
+ `sharing: "shared"` reuses one initialized stdio child across compatible virtual HTTP sessions.
35
+ This differs from `pool`: a pooled child is uninitialized and belongs exclusively to its first
36
+ session. Both modes remain opt-in. No latency recommendation enables either mode automatically.
37
+
38
+ Shared mode is for a stateless-tools profile: tools operate on explicit inputs, the configured
39
+ credential context is common, and the server has no implicit per-client workspace, conversation,
40
+ account or subscription state. Enabling the mode asserts that profile. Matching capabilities
41
+ alone does not prove the implementation is stateless.
42
+
43
+ The default remains `isolated`. Unsupported clients receive a clear compatibility error before
44
+ their business request is dispatched. There is no silent sharing downgrade, guessed callback
45
+ recipient, or automatic replay on another process.
46
+
47
+ ## Compatibility and initialization
48
+
49
+ One group per configured server and configuration generation is allowed. Its compatibility key
50
+ contains the complete, canonically ordered initialization parameters and configuration. Preserve
51
+ array order, absent properties and all client identity fields. The JSON-RPC request ID is not
52
+ part of the initialization parameters.
53
+
54
+ For the first profile:
55
+
56
+ - Support released protocol versions `2025-03-26`, `2025-06-18` and `2025-11-25`.
57
+ - Require empty client capabilities. Roots, sampling, elicitation, tasks and unknown extensions
58
+ cannot safely be routed from an arbitrary stdio callback to an individual client.
59
+ - Support `tools/list`, `tools/call` and `ping`.
60
+ - Reject stateful protocol methods, subscriptions, logging changes and task-augmented calls.
61
+ - Do not interpret arbitrary tool argument/result fields as request IDs or state handles.
62
+ - Bind tools-list cursors to their originating virtual session and child generation.
63
+ - Retain the actual upstream protocol version, instructions and implementation metadata.
64
+ The upstream may negotiate any of the three supported versions.
65
+ - Expose only the upstream's actual `tools` capability, without inventing flags. This
66
+ explicitly tools-only surface does not advertise optional prompts, resources, logging,
67
+ completions or tasks even if the upstream supports them. An empty `experimental` object
68
+ is harmless; unknown or nonempty experimental extensions are incompatible.
69
+
70
+ Concurrent compatible arrivals share a creation promise installed before awaiting process
71
+ creation. Exactly one upstream `initialize` and one `notifications/initialized` are sent.
72
+ Each virtual session receives the result with its own request ID and must complete its own
73
+ initialized notification before dispatching tools.
74
+
75
+ An incompatible client does not replace or disturb the established group. Failed initialization
76
+ fails all waiters, removes the failed creation promise, and terminates the candidate. A later
77
+ attempt may start a new group with bounded retry behavior.
78
+
79
+ ## Routing
80
+
81
+ Use distinct typed keys: integer `1` and string `"1"` are different client IDs.
82
+
83
+ | Message | Required behavior |
84
+ |---|---|
85
+ | Client request | Translate to a generation-unique upstream ID and retain owner plus original ID. |
86
+ | Child response | Deliver only to its mapped owner, restoring the original ID. |
87
+ | Client progress token | Translate only `_meta.progressToken`; restore it on the owner's progress notifications. |
88
+ | Client cancellation | Translate only that session's request ID. Never cancel another client's request. |
89
+ | Child `ping` | Answer as the bridge peer. It is not a response even if its ID matches an operation. |
90
+ | Other child requests | Reject explicitly; never select the first/recent client or broadcast callbacks. |
91
+ | Tools-list changes | Fan out to members only when the common catalog contract permits it. |
92
+ | Other notifications | Drop unowned logs and unexposed catalog changes with diagnostics. Never broadcast their data; reject unknown extensions. |
93
+
94
+ Classify messages by category (`method`, `result`, `error`) before consulting ID maps. Unknown
95
+ or late responses must not become notifications. Progress associated with a POST must be
96
+ delivered on its originating POST stream; an unrelated GET stream is not a replacement.
97
+
98
+ Bound operations, queued work, input lines, retained IDs/cursors and stream buffers. Reject
99
+ overflow explicitly instead of allocating without limit or silently truncating protocol data.
100
+
101
+ ## Virtual child adapter
102
+
103
+ `bin/shared-sessions.mjs` exports `SharedSessionManager`. It takes:
104
+
105
+ ```js
106
+ {
107
+ spawn: async (name, definition) => actualChild,
108
+ kill: (actualChild) => {},
109
+ onFailure: (name, detail) => {},
110
+ onInitialized: (name, elapsedMs) => {},
111
+ onResponse: (name) => {},
112
+ log: (name, text) => {},
113
+ initTimeoutMs,
114
+ requestTimeoutMs
115
+ }
116
+ ```
117
+
118
+ `await manager.acquire(name, definition, initializeParams)` returns an EventEmitter-compatible
119
+ virtual child with `stdin` accepting newline-delimited JSON-RPC, readable `stdout`, the actual
120
+ group `pid`, `exitCode`, `signalCode`, `__spawnedAt`, `__sharedSession: true`, and `detach()`.
121
+ It plugs into the existing Streamable HTTP per-session response parser without spawning again.
122
+ One actual child owns one stdout parser; virtual stdout contains only that session's messages.
123
+ For already-parsed HTTP JSON, use `child.writeMessage(message)`. It applies the same input,
124
+ depth, ID and ownership guards before serialization. Calling it after detachment throws an
125
+ explicit `SHARED_DETACHED` admission error instead of leaving a request waiting indefinitely.
126
+
127
+ The bridge's process-kill helper must detect a virtual child and detach it instead of killing
128
+ its shared PID. Actual group creation goes through the existing spawn gate and B21 counter.
129
+ Virtual session attachment is not an OS spawn or a cold-initialization sample.
130
+ `onResponse` reports actual upstream responses, not locally generated rejections. Health must
131
+ not classify errors by a numeric JSON-RPC code that an upstream can also legitimately use.
132
+
133
+ Provide `recycle(name)`, `remove(name)`, `shutdown()` and `inspect(name)` methods. Inspection
134
+ exposes only lifecycle facts such as generation, state, members and unresolved operations.
135
+
136
+ ## Lifetime, drain and credentials
137
+
138
+ Deleting one session forgets and tombstones only that session. Remove its unsent work and send
139
+ owner-scoped cancellation for dispatched work. Other clients and their operations remain alive.
140
+ The actual child lingers between compatible sessions, bounded by `sharedLingerMs` (default
141
+ 30 minutes). Unresolved work prevents idle eviction.
142
+
143
+ Cancellation is not proof that execution stopped. Retain execution ownership until the
144
+ response arrives or a bounded failure/drain policy terminates the generation.
145
+
146
+ Recycling marks the generation draining and stops new dispatch to it. Existing work gets a
147
+ bounded opportunity to finish; once drained, terminate promptly rather than waiting for the
148
+ next full recycle interval. A drain deadline fails unresolved work explicitly as potentially
149
+ executed, terminates that generation, and permits a later fresh generation. Never replay those
150
+ business calls. Fence old exit handlers and timers so they cannot delete a replacement.
151
+
152
+ The server retains its existing credential mechanism. The bridge does not freeze a token in
153
+ the shared initialization, infer renewal from a generic cache, or substitute an auth provider.
154
+ Keep configured credential recycling. A synthetic short-expiry test must show: expired call
155
+ fails unchanged; draining/replacement creates a fresh generation; a new call succeeds; the
156
+ failed call was dispatched once. Real provider renewal still requires its own canary.
157
+
158
+ Resume reattaches only compatible stateless sessions. It is not restoration of arbitrary
159
+ application state and does not preserve TCP connections across a bridge restart; that is B14.
160
+
161
+ ## Acceptance gates
162
+
163
+ 1. More than fifty compatible initialize/DELETE cycles reuse one actual child and one upstream
164
+ initialization. Two simultaneously active sessions must also share that same generation.
165
+ 2. Concurrent numeric/string ID collisions, out-of-order results, progress and cancellation
166
+ route to the correct owner. Forged or late messages cannot cross sessions.
167
+ 3. Delete one client while another works, kill a child during a request, and recycle while busy.
168
+ Pending callers fail promptly and dispatched tool calls are never replayed.
169
+ 4. Incompatible initialization and unsupported callbacks/methods fail explicitly. Isolation
170
+ defaults and existing HTTP proxy behavior remain unchanged.
171
+ 5. Counter deltas for identical isolated and shared workloads show actual reduced launches,
172
+ not merely fewer latency samples or a lower instant process count.
173
+ 6. Use an isolated config directory and spare port for real-server and browser/CLI canaries.
174
+ Do not alter another project's client or orchestration configuration.
175
+
176
+ Reference: released MCP
177
+ [lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle),
178
+ [transports](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports),
179
+ [progress](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress), and
180
+ [cancellation](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation).
181
+ Draft transport changes are not applied to clients speaking released versions.
@@ -0,0 +1,31 @@
1
+ {
2
+ "filesystem": {
3
+ "command": "npx",
4
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
5
+ },
6
+ "github": {
7
+ "command": "npx",
8
+ "args": ["-y", "@modelcontextprotocol/server-github"],
9
+ "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "REPLACE_WITH_YOUR_PAT" }
10
+ },
11
+ "example-http-static-token": {
12
+ "type": "http",
13
+ "url": "https://example.com/mcp",
14
+ "auth": { "type": "env", "var": "EXAMPLE_TOKEN" }
15
+ },
16
+ "example-http-cli-token": {
17
+ "type": "http",
18
+ "url": "https://your-service.example.com/mcp",
19
+ "auth": {
20
+ "type": "command",
21
+ "command": "az account get-access-token --resource api://your-resource-id --query accessToken -o tsv",
22
+ "refreshMinutes": 50
23
+ },
24
+ "headers": { "x-example-header": "value" }
25
+ },
26
+ "example-http-no-auth": {
27
+ "type": "http",
28
+ "url": "https://open.example.com/mcp",
29
+ "auth": { "type": "none" }
30
+ }
31
+ }
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "mcp-pacemaker",
3
+ "version": "1.3.1",
4
+ "description": "Keeps your MCP servers alive across editor restarts, reboots, and token expiry — pluggable auth, cross-platform auto-start, one-command setup. Zero-dependency bridge; a small CLI for setup.",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp-pacemaker": "bin/cli.mjs",
8
+ "mcp-bridge": "bin/mcp-bridge.mjs"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "autostart/",
13
+ "supervisor/",
14
+ "tools/windows-security-helper/",
15
+ "examples/",
16
+ "ui/dist/",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "SECURITY.md",
20
+ "docs/shared-sessions.md",
21
+ "LICENSE",
22
+ "THIRD_PARTY_NOTICES.txt",
23
+ "CONTRIBUTING.md"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "license": "MIT",
29
+ "author": "Girish Konda",
30
+ "homepage": "https://github.com/girishkvs/mcp-pacemaker#readme",
31
+ "bugs": {
32
+ "url": "https://github.com/girishkvs/mcp-pacemaker/issues"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/girishkvs/mcp-pacemaker.git"
37
+ },
38
+ "keywords": [
39
+ "mcp",
40
+ "model-context-protocol",
41
+ "sse",
42
+ "bridge",
43
+ "proxy",
44
+ "vscode",
45
+ "cursor",
46
+ "claude-desktop",
47
+ "supervisor",
48
+ "keep-alive"
49
+ ],
50
+ "scripts": {
51
+ "bridge": "node bin/mcp-bridge.mjs",
52
+ "cli": "node bin/cli.mjs",
53
+ "consumer:check": "node tools/npm-consumer/check.mjs",
54
+ "compat:prepare": "node tools/compatibility/prepare.mjs",
55
+ "compat:prepare:baseline": "node tools/compatibility/prepare.mjs --historical",
56
+ "compat:clean": "node tools/compatibility/prepare.mjs --clean",
57
+ "test:compat": "node --test --test-concurrency=1 test/compat/gates.test.mjs test/compat/cli-api.test.mjs",
58
+ "test:compat:browser": "node --test --test-concurrency=1 test/compat/browser.test.mjs",
59
+ "test": "node --test test/bridge.test.mjs test/bridge-controls.test.mjs test/kill-tree.test.mjs test/keepalive.test.mjs test/auth-token.test.mjs test/cold-start.test.mjs test/crash-report.test.mjs test/resume.test.mjs test/config-checks.test.mjs test/config-reload.test.mjs test/health.test.mjs test/cli.test.mjs test/logs.test.mjs test/prewarm.test.mjs test/pooling-config.test.mjs test/pooling-writer.test.mjs test/pooling-api.test.mjs test/pooling-deadlines.test.mjs test/pooling-queue-budget.test.mjs test/windows-security-helper.test.mjs test/tui.test.mjs test/warm-pool.test.mjs test/server-metrics.test.mjs test/admission.test.mjs test/shared-sessions.test.mjs test/shared-http.test.mjs test/cross-host.test.mjs test/suite-integrity.test.mjs test/consumer-package.test.mjs test/notices.test.mjs test/notices-runtime.test.mjs test/npm-publication.test.mjs test/npm-publication-proof.test.mjs test/update-check.test.mjs test/service-lifecycle.test.mjs test/service-supervisor.test.mjs test/npm-publication-gates.test.mjs test/publication-scanners-advisories.test.mjs test/publication-scanners.test.mjs test/service-replacement.test.mjs test/consumer-environment.test.mjs test/npm-publication-external.test.mjs test/npm-publication-matrix.test.mjs test/npm-publication-peer.test.mjs test/publication-scanners-diagnostics.test.mjs test/publication-scanners-publication.test.mjs test/publication-scanners-paths.test.mjs test/npm-publication-local-enforcement.test.mjs test/publication-scanners-admission.test.mjs",
60
+ "notices:check": "node tools/third-party-notices/check.mjs",
61
+ "publication:source-gates": "node tools/npm-publication/source-gates.mjs",
62
+ "publication:artifact-gates": "node tools/npm-publication/artifact-gates.mjs",
63
+ "service:replacement:check": "node tools/service-replacement/check.mjs",
64
+ "publication:external-gates": "node tools/npm-publication/external-gates.mjs",
65
+ "publication:local-gate": "node tools/npm-publication/local-windows.mjs",
66
+ "publication:verify-local": "node tools/npm-publication/verify-local.mjs"
67
+ },
68
+ "dependencies": {
69
+ "@clack/prompts": "^1.7.0",
70
+ "commander": "^15.0.0",
71
+ "ink": "^7.1.1",
72
+ "picocolors": "^1.1.1",
73
+ "react": "^19.2.7",
74
+ "semver": "^7.7.3",
75
+ "smol-toml": "^1.8.0"
76
+ }
77
+ }
@@ -0,0 +1,14 @@
1
+ // IPC reaches the bridge's existing graceful shutdown handler on Windows too.
2
+ let loaded = false;
3
+ let stopRequested = false;
4
+ function shutdown() {
5
+ stopRequested = true;
6
+ if (loaded) process.emit('SIGTERM');
7
+ }
8
+ process.on('message', (message) => {
9
+ if (message === 'stop') shutdown();
10
+ });
11
+ process.on('disconnect', shutdown);
12
+ await import('../bin/mcp-bridge.mjs');
13
+ loaded = true;
14
+ if (stopRequested) shutdown();
@@ -0,0 +1,172 @@
1
+ import { fork } from 'node:child_process';
2
+ import { randomBytes, randomUUID } from 'node:crypto';
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
4
+ import { createServer } from 'node:net';
5
+ import { homedir } from 'node:os';
6
+ import { dirname, resolve, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { parseArgs } from 'node:util';
9
+ import { servicePaths, assertDurableRoot } from '../bin/service-control.mjs';
10
+
11
+ const { values } = parseArgs({
12
+ options: { port: { type: 'string', default: '8791' }, config: { type: 'string' } },
13
+ });
14
+ const port = Number(values.port);
15
+ const root = realpathSync(resolve(dirname(fileURLToPath(import.meta.url)), '..'));
16
+ const config = resolve(values.config || join(homedir(), '.mcp-pacemaker', 'servers.json'));
17
+ const paths = servicePaths(config, port);
18
+ assertDurableRoot(root);
19
+ if (Number(process.versions.node.split('.')[0]) < 20) throw new Error('Node.js >=20 is required.');
20
+
21
+ if (existsSync(paths.hold)) {
22
+ console.log(`Service :${port} is held stopped. Use the matching CLI start --port ${port} to resume deliberately.`);
23
+ } else {
24
+ await supervise();
25
+ }
26
+
27
+ async function supervise() {
28
+ const identity = {
29
+ protocol: 1, root, config, port, socket: paths.socket, pid: process.pid, execPath: process.execPath,
30
+ id: randomUUID(), token: randomBytes(32).toString('hex'),
31
+ version: JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version,
32
+ };
33
+ let child;
34
+ let childExit;
35
+ let retry;
36
+ let stopping;
37
+ let stopRequested = false;
38
+ let holdRequested = false;
39
+ const clients = new Set();
40
+ const server = createServer((socket) => {
41
+ clients.add(socket);
42
+ socket.on('close', () => clients.delete(socket));
43
+ socket.on('error', () => socket.destroy());
44
+ socket.setTimeout(15000, () => socket.destroy());
45
+ let data = '';
46
+ let handled = false;
47
+ socket.on('data', (chunk) => {
48
+ if (handled) return;
49
+ data += chunk;
50
+ if (data.length > 4096) { socket.destroy(); return; }
51
+ if (!data.includes('\n')) return;
52
+ handled = true;
53
+ let request;
54
+ try { request = JSON.parse(data.slice(0, data.indexOf('\n'))); }
55
+ catch { socket.destroy(); return; }
56
+ if (request.id !== identity.id ||
57
+ request.token !== identity.token ||
58
+ request.action !== 'stop') {
59
+ socket.destroy();
60
+ return;
61
+ }
62
+ stop().then(() => {
63
+ socket.end(JSON.stringify({ id: identity.id, stopped: true, autostartHeld: true }) + '\n');
64
+ }, (error) => {
65
+ console.error(error.message);
66
+ socket.end(JSON.stringify({ id: identity.id, stopped: false }) + '\n');
67
+ });
68
+ });
69
+ });
70
+
71
+ mkdirSync(dirname(config), { recursive: true, mode: 0o700 });
72
+ await new Promise((resolveListen, reject) => {
73
+ server.once('error', reject);
74
+ server.listen(paths.socket, resolveListen);
75
+ });
76
+ server.on('error', (error) => { console.error(error.message); process.exitCode = 1; });
77
+ try {
78
+ writeFileSync(paths.record, JSON.stringify(identity) + '\n', { mode: 0o600 });
79
+ if (existsSync(paths.hold)) {
80
+ server.close();
81
+ return;
82
+ }
83
+ launch();
84
+ } catch (error) {
85
+ server.close();
86
+ throw error;
87
+ }
88
+ process.on('SIGINT', onSignal);
89
+ process.on('SIGTERM', onSignal);
90
+
91
+ function launch() {
92
+ if (stopRequested ||
93
+ existsSync(paths.hold)) {
94
+ server.close();
95
+ return;
96
+ }
97
+ child = fork(join(root, 'supervisor', 'bridge-child.mjs'), ['--port', String(port), '--config', config], {
98
+ execPath: process.execPath, execArgv: [], stdio: ['ignore', 'inherit', 'inherit', 'ipc'], windowsHide: true,
99
+ });
100
+ childExit = new Promise((resolveExit, reject) => {
101
+ child.once('error', reject);
102
+ child.once('exit', (code) => {
103
+ child = undefined;
104
+ resolveExit();
105
+ if (stopRequested) return;
106
+ if (code === 3) server.close();
107
+ else retry = setTimeout(launch, 2000);
108
+ });
109
+ });
110
+ childExit.catch((error) => {
111
+ console.error(`Bridge launch failed: ${error.message}`);
112
+ process.exitCode = 1;
113
+ server.close();
114
+ });
115
+ }
116
+
117
+ async function stop(hold = true) {
118
+ // Persist before touching the child so logon/unlock and OS retries cannot restart it.
119
+ if (hold &&
120
+ !holdRequested) {
121
+ writeFileSync(paths.hold, JSON.stringify({ id: identity.id, root, config, port }) + '\n', { mode: 0o600 });
122
+ holdRequested = true;
123
+ }
124
+ if (stopping) return stopping;
125
+ stopRequested = true;
126
+ clearTimeout(retry);
127
+ stopping = (async () => {
128
+ if (child) {
129
+ if (child.connected) {
130
+ try {
131
+ await new Promise((resolveSend, reject) => child.send('stop', (error) => error ? reject(error) : resolveSend()));
132
+ } catch (error) {
133
+ if (error.code !== 'ERR_IPC_CHANNEL_CLOSED' &&
134
+ error.code !== 'EPIPE') throw error;
135
+ }
136
+ }
137
+ let timeout;
138
+ try {
139
+ await Promise.race([
140
+ childExit,
141
+ new Promise((_, reject) => {
142
+ timeout = setTimeout(() => reject(new Error('Bridge did not exit; do not replace its root. No force-kill attempted.')), 10000);
143
+ }),
144
+ ]);
145
+ } finally {
146
+ clearTimeout(timeout);
147
+ }
148
+ }
149
+ if (holdRequested) {
150
+ writeFileSync(paths.hold, JSON.stringify({ id: identity.id, root, config, port, stopped: true }) + '\n', { mode: 0o600 });
151
+ }
152
+ server.close();
153
+ process.removeListener('SIGINT', onSignal);
154
+ process.removeListener('SIGTERM', onSignal);
155
+ })().catch((error) => {
156
+ // Keep restart suppressed, but let a later explicit stop verify the real child exit.
157
+ // A timeout must not write a completed receipt or permanently poison reconciliation.
158
+ stopping = undefined;
159
+ throw error;
160
+ });
161
+ return stopping;
162
+ }
163
+
164
+ function onSignal() {
165
+ stop(false).then(() => {
166
+ for (const socket of clients) socket.destroy();
167
+ }, (error) => {
168
+ console.error(error.message);
169
+ process.exitCode = 1;
170
+ });
171
+ }
172
+ }
@@ -0,0 +1,23 @@
1
+ <#
2
+ .SYNOPSIS Supervisor for the mcp-pacemaker bridge (Windows). Keeps it alive across crashes.
3
+ .DESCRIPTION
4
+ Runs the managed supervisor. A targeted CLI stop holds automatic restarts until an
5
+ explicit CLI start. Node.js 20 or newer must be on PATH.
6
+ .PARAMETER Port Port the bridge listens on (default 8791). Must match your client config.
7
+ .OUTPUTS
8
+ Supervisor diagnostics.
9
+ .EXAMPLE
10
+ .\supervise.ps1 -Port 8791
11
+ #>
12
+ [CmdletBinding()]
13
+ param([ValidateRange(1, 65535)][int]$Port = 8791)
14
+
15
+ $ErrorActionPreference = 'Stop'
16
+ $supervisor = Join-Path -Path $PSScriptRoot -ChildPath 'supervise.mjs'
17
+
18
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
19
+ throw "node is not on PATH. Install Node.js (>=20) and retry."
20
+ }
21
+
22
+ & node $supervisor --port $Port
23
+ exit $LASTEXITCODE
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ # Starts the managed supervisor. Arg 1 = port.
3
+ PORT="${1:-8791}"
4
+ DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
+
6
+ if ! command -v node >/dev/null 2>&1; then
7
+ echo "node is not on PATH. Install Node.js (>=20) and retry." >&2
8
+ exit 1
9
+ fi
10
+
11
+ exec node "$DIR/supervisor/supervise.mjs" --port "$PORT"
@@ -0,0 +1,95 @@
1
+ # Windows security helper
2
+
3
+ `bin/windows/PoolingSecurityHelper.exe` is an own-source, AnyCPU .NET Framework
4
+ console program. It contains both the native security reader and the operation
5
+ code; there is no companion reader DLL or third-party runtime dependency.
6
+ The `bin/windows/src/` source and build metadata ship alongside the executable.
7
+
8
+ ## Runtime
9
+
10
+ Requires Windows with .NET Framework 4.6.2 or later. Windows 10 1607+, Windows 11,
11
+ and Windows Server 2016+ include that version or newer. Windows PowerShell 5.1
12
+ alone on older Windows does not establish this prerequisite.
13
+ PowerShell is **not** launched by the helper.
14
+ There is no runtime compilation, download, elevation, or policy change.
15
+ AnyCPU uses the installed Framework's process architecture; native calls use
16
+ pointer-sized handles. Windows ARM64 execution has not been validated.
17
+
18
+ The Node caller must launch the package-relative executable directly (no shell)
19
+ with exactly one argument, `inspect` or `copy`, and supply:
20
+
21
+ | Environment variable | Used by | Value |
22
+ | --- | --- | --- |
23
+ | `MCP_POOL_SOURCE` | Both | Source config path |
24
+ | `MCP_POOL_SECURITY` | Copy | Previously inspected fingerprint |
25
+ | `MCP_POOL_TEMP` | Copy | Existing empty replacement file |
26
+ | `MCP_POOL_BACKUP` | Copy | Existing empty backup file |
27
+
28
+ `inspect` writes one line: `F:<base64 SHA256>` for complete security or
29
+ `P:<base64 SHA256>` when audit read is denied. These are comparison values,
30
+ not log fields. The caller must refuse automatic writes for partial (`P:`)
31
+ state. Successful `copy` writes nothing to stdout.
32
+
33
+ | Exit code | Meaning |
34
+ | --- | --- |
35
+ | 0 | Inspection or copy/verification succeeded |
36
+ | 3 | Source fingerprint changed before or after copy; caller reports conflict |
37
+ | 1 | Invalid input, I/O failure, or security cannot be preserved |
38
+
39
+ On failure, stderr contains only `MCPERR type=<exception type>` and, for native
40
+ errors, `MCPERR nativeError=<integer>`. No path, descriptor, token, config bytes,
41
+ or exception message is logged. Caller owns process timeout and error mapping.
42
+
43
+ Fingerprint framing is unchanged: canonical native descriptor (mask `0x1f7`),
44
+ separate managed audit descriptor, and integer file attributes, written with
45
+ `BinaryWriter` length prefixes before SHA256. Copy preserves owner/group/DACL
46
+ through SDDL and verifies the entire fingerprint on both staging files, then
47
+ rechecks the source. It does not copy unsupported audit/integrity/attribute
48
+ state or weaken it to force success. It never writes config contents.
49
+
50
+ ## Build and verify
51
+
52
+ Prerequisites, installed by the developer or CI image:
53
+
54
+ 1. Visual Studio or Build Tools with MSBuild and its Roslyn `csc.exe`, supporting
55
+ `/deterministic` and `/pathmap`. `vswhere` discovers current releases, including
56
+ Visual Studio 2026; an explicit compiler path is also supported.
57
+ 2. The .NET Framework **4.6.2 targeting pack** (reference assemblies, not a new
58
+ runtime). The script reports missing prerequisites; it installs nothing.
59
+ 3. Windows PowerShell 5.1 or PowerShell 7 for the build script only.
60
+
61
+ From the repository root:
62
+
63
+ ```powershell
64
+ # Update executable and metadata after source changes.
65
+ .\tools\windows-security-helper\build.ps1
66
+
67
+ # Read-only source/toolchain/hash check plus byte-for-byte rebuild.
68
+ .\tools\windows-security-helper\build.ps1 -Verify
69
+
70
+ # Select an already-installed compiler explicitly when reproducing recorded bytes.
71
+ .\tools\windows-security-helper\build.ps1 -Verify -CompilerPath 'C:\path\to\Roslyn\csc.exe'
72
+ ```
73
+
74
+ The build uses `/noconfig /nostdlib+` with explicit `mscorlib.dll` and `System.dll`
75
+ references, fixed options, LF-normalized UTF-8 source, and mapped source paths.
76
+ It compiles twice under different temporary paths and refuses unequal bytes.
77
+ Temporary build files are removed by explicit path. No `dotnet build`, runtime
78
+ Framework compiler, NuGet restore, or external binary is involved.
79
+
80
+ `PoolingSecurityHelper.build.json` records compiler version/hash, reference
81
+ hashes, options, normalized source/build-script hashes, and executable SHA256.
82
+ `-Verify` checks all inputs and requires rebuilt bytes to match the packaged
83
+ binary. Verification never updates the binary or metadata.
84
+
85
+ Reproducibility is scoped to the **recorded compiler and references**. A newer
86
+ CI image/compiler may produce different bytes; do not treat a changed toolchain
87
+ as verification of the old binary. Use the recorded installed toolchain or
88
+ explicitly rebuild, review, and validate the updated assets. This is integrity
89
+ checking, not code signing or protection against someone replacing all assets.
90
+
91
+ The published executable must contain only the production sources above.
92
+ Audit-denial/delay test variants belong in disposable directories outside `bin/`
93
+ and must never be included in an npm tarball. Release validation must also run
94
+ the real package on supported Windows/Node versions and verify packaged file
95
+ inclusion. This build alone does not establish those platform results.