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
package/CHANGELOG.md ADDED
@@ -0,0 +1,424 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Fixed
11
+
12
+ - Require a complete, bounded owner stage-list observation before recording no
13
+ pending stages; keep owner authentication, ledger review and approval separate.
14
+ - Require a stable, safe `total_count` on every GitHub jobs/artifacts page and
15
+ reject count drift or overshoot before admitting incomplete publication evidence.
16
+ - Require the original stage job's numeric attempt `1` when verifying captured
17
+ stage evidence; the requested API attempt alone does not establish it.
18
+ - Check each patch's actual runtime layout during npm publication. Legacy 1.3.1
19
+ does not require 2.0-only editor/file modules, while shared execution and the
20
+ current line's error module remain required.
21
+ - Accept GitHub's exact unexpanded consumer-matrix job name during source-secret
22
+ collection validation, while still requiring skipped status and no executed steps.
23
+ - Compare collected report and native-receipt completion against the precision
24
+ of GitHub's step timestamp without admitting execution in the next second.
25
+ - Preserve exact positive source-CI attempts and accept bounded canonical npm repair tags.
26
+ - Let the 1.3.1 publishing finalizer use approved immutable published 2.0.1 bytes
27
+ for T32 comparison without a hosted seed, fabricated preparation evidence or
28
+ stage authority. Candidate consumer, compatibility and publication gates remain required.
29
+ - Resolve native directory watcher paths before watching logs and test control files,
30
+ avoiding Windows short-path watcher crashes without changing the selected files.
31
+
32
+ ### Security
33
+
34
+ - Prepare 1.3.1 with a `smol-toml` minimum and locked version of 1.8.0. Bounded malformed-TOML
35
+ regressions cover the EOF-comment hang fixed by CVE-2026-85730 without changing legacy
36
+ pooling, audit-policy, or immediate-save behavior.
37
+
38
+ ### Changed
39
+
40
+ - Resolve the explicit `legacy` npm channel for same-major, exact-version update guidance.
41
+ Channel rollback and adoption of an existing backend are not automatic upgrades.
42
+ - Limit legacy maintenance to the newest 1.3.x patch through March 31, 2027; document durable
43
+ service replacement and platform prerequisites.
44
+
45
+ ### Added
46
+
47
+ - Source-bound, manual npm preparation/staging tooling, bundled third-party notices, and
48
+ fresh-consumer package checks.
49
+ - Exact legacy-patch/current-release compatibility gates and an explicit two-patch mode,
50
+ while retaining the immutable 1.3.0/2.0.0 historical pair. Add maintenance-branch and Node 24 CI.
51
+
52
+ ## [1.3.0] - 2026-09-08
53
+
54
+ ### Added
55
+
56
+ - Cumulative per-server process launch, failed launch, warm adoption, new session and resume
57
+ counters. The status snapshot identifies the bridge instance and interval start so process
58
+ churn can be compared without mixing time windows. Latency samples remain capped separately.
59
+ - One-click pre-warming controls and conflict-safe Undo, with the requested warm count and
60
+ resident-process cost shown before activation. Pooling policy edits preserve active children.
61
+ - A consolidated pre-warming dashboard tab and `mcp-pacemaker prewarm` CLI view, both using
62
+ the same snapshot data. Explicit CLI actions enable, disable or undo one server's settings.
63
+ - `mcp-pacemaker logs` reads the durable log and retained rotation with `--follow`, `--since`,
64
+ `--server` and literal `--grep`. Multiline context, partial UTF-8 writes and rotation are
65
+ handled without treating continuation lines as independent server records.
66
+ - Opt-in shared stdio children for compatible stateless tools sessions. One real initialization
67
+ serves virtual sessions with owner-scoped IDs, progress, cancellation and cursors, bounded
68
+ queues, idle retention and draining. Unsupported features fail explicitly; uncertain tool
69
+ calls are never replayed. UI and CLI distinguish shared children from uninitialized pools.
70
+
71
+ ### Fixed
72
+
73
+ - Reserve session capacity before asynchronous startup, preventing queued cold starts from
74
+ overbooking the configured cap. Concurrent resumes of one session share one handshake.
75
+ - Bind session lookup and deletion to the requested server. A stale exit or deleted resume
76
+ cannot remove or resurrect a replacement session.
77
+ - Distinguish bridge-generated transport errors from identical numeric codes returned by an
78
+ upstream, so a legitimate server response is not mislabeled as a bridge timeout.
79
+ - Guard shared JSON before serialization in both directions, including on Node 20, and bound
80
+ combined response sizes. Excessive depth or output produces controlled errors rather than
81
+ crashing the bridge or silently replaying tool calls.
82
+ - Identify changed metadata fields when a pooling write is refused by the conflict guard.
83
+ - Avoid false Windows pooling conflicts from unrelated timestamp updates while retaining
84
+ audit, integrity-label, permission and file-identity checks. Refuse automatic writes when
85
+ audit policy cannot be verified or preserved.
86
+ - Run configuration writes in a bounded, serialized worker so Windows permission inspection
87
+ does not block the bridge's request processing.
88
+ - Replace per-write PowerShell startup and C# compilation with a bundled, reproducibly built
89
+ Windows security helper while retaining the same permission-preservation checks.
90
+ - Bound configuration-write queueing and execution with shared cancellation. Report uncertain
91
+ or late-committed replacements honestly and reload saved state after a worker exits, even
92
+ with config watching disabled. Never replay a timed-out operation.
93
+ - Update the UI build's `browserslist` dependency to 4.28.9 to address its published memory-growth
94
+ and custom-stats advisories.
95
+
96
+ ## [1.2.0] - 2026-09-04
97
+
98
+ ### Added
99
+
100
+ - **Cold start is measured.** The bridge spawns every child, so it is the only thing positioned
101
+ to know what starting a server actually costs — and it was throwing that away. `/api/status`
102
+ and the dashboard now report p50/p95/max spawn time per server, timed from `spawn()` to the
103
+ server answering `initialize`, because a process existing says nothing about the package
104
+ manager work that follows it. On the author's machine one server measured a 20s cold start
105
+ while the pre-warm pool sat empty and a server that starts in 200ms had a full one.
106
+
107
+ - **A gate on concurrent cold starts** (`MCP_MAX_CONCURRENT_SPAWNS`, default 2). Servers launched
108
+ through `npx`, `uvx` or `dnx` share one package cache, and starting several at once can corrupt
109
+ it: 17 simultaneous `npx` invocations here produced `npm error code ECOMPROMISED / Lock
110
+ compromised` and failed every one of those sessions. No server definition can fix that — it is
111
+ a property of the concurrency — so the bridge queues cold starts instead. The slot is held
112
+ until the child first speaks, which is when the expensive part is over.
113
+
114
+ Only servers that actually use a package manager are gated; a plain `node server.js` shares no
115
+ cache and was never at risk. Detection is from the command, and per-server
116
+ `sharedPackageCache` forces it either way for a wrapper script the heuristic cannot see.
117
+ Waiting for a slot is bounded by `MCP_SPAWN_GATE_WAIT_MS` (15s) — past that the spawn proceeds
118
+ ungated, because a corrupted cache is a risk while a hung client request is a certainty.
119
+
120
+ - **Pooling advice, never pooling by default.** When a server's measured cold start passes
121
+ `MCP_POOL_ADVICE_MS` (2s), `doctor` and the dashboard name the number and print the exact
122
+ config that would hide it, sized from observed peak concurrency. It is not applied: a warm pool
123
+ costs a resident process per slot, and that is the user's call to make. Cold start is otherwise
124
+ invisible to the person paying for it, who just experiences a slow tool.
125
+
126
+ - **Pools size themselves once you opt in.** `sharing: "pool"` with no `minWarm` now targets peak
127
+ concurrent sessions over the last hour, capped by `MCP_WARM_MAX` (8). An explicit `minWarm`
128
+ still wins. Defaulting to one warm child is how a burst of 17 sessions ended up paying 16 cold
129
+ starts on a server that was configured to pool.
130
+
131
+ - **Restart reporting.** `/api/status` carries how long ago the bridge restarted and how many
132
+ clients held a session before it and have not come back; `status` warns and the dashboard shows
133
+ a banner. The bridge re-establishes those sessions correctly, but some clients treat a single
134
+ connection failure as permanent and need an MCP reload — this says who.
135
+
136
+ - `cappedSessions` alongside `sessions`, so a server's count matches the cap actually enforced.
137
+ The cap counts Streamable HTTP sessions while the display summed those and classic SSE, which
138
+ is how a server showed `33/32`.
139
+
140
+ ### Changed
141
+
142
+ - **A 401 or 403 only counts against health when the bridge holds the credential.** For a server
143
+ with `auth: {type: none}` the *client* authenticates, so an opening 401 is the handshake
144
+ working as designed. Counting it flipped a healthy server to `failing` every 60 seconds. The
145
+ rule is now the one that matters: failing means the tool actually failed.
146
+
147
+ - **The bridge's own 503 counts as a failure.** Refusing a session at the concurrency cap is the
148
+ most client-visible failure the bridge produces, and it was invisible to health — an operator
149
+ saw a healthy server while the bridge was the thing saying no.
150
+
151
+ - A terminated session still answers **404**, per the transport spec, which makes re-initializing
152
+ on 404 a client MUST. An earlier revision of this work returned 410 for sessions known to be
153
+ gone; it reads better to a human and strands a compliant client, since 410 has no defined
154
+ recovery behaviour. The distinction moved to the response body and the log, where a client is
155
+ not allowed to care about it.
156
+
157
+ ### Fixed
158
+
159
+ - **A warm pool never refilled after a child died on its own.** The exit handler removed the
160
+ corpse and stopped; refill only ran on boot, on take, on recycle and on reload. A pooled server
161
+ that went quiet drained to empty and stayed there, silently degrading to a cold spawn per
162
+ session — the exact failure pooling exists to prevent, and invisible because an empty pool
163
+ looks like one nobody has asked for anything yet. Seen here as one server sitting at `warm: 0`
164
+ for three hours while another with identical config stayed full purely because it was busy.
165
+ Refill now happens on an unattended exit, with backoff so a child that dies instantly cannot
166
+ become a spawn loop.
167
+
168
+ - **A pooled server counted every failure twice.** A warm child adopted into a session kept the
169
+ pool's exit handler and gained the session's, and both reported the exit — so an identical
170
+ server lost health twice as fast for having been pooled.
171
+
172
+ - **A pool refilled one child per take,** so a burst that emptied it recovered long after the
173
+ burst was over. Refill now runs toward the target in one pass, bounded by the spawn gate.
174
+
175
+ - A static `authorization` header did not mark a server as bridge-authenticated, so its 401 read
176
+ as `unknown` forever and its challenge was relayed to a client that could not act on it.
177
+
178
+ - The health probe no longer calls servers the bridge does not authenticate.
179
+
180
+ - SSE streams are closed on shutdown instead of dropped. A clean close reads as "reconnect" to a
181
+ client; a dropped TCP connection reads as a transport fault, and some clients latch on that.
182
+
183
+ - A failed request names the credential source it actually used — `audience X`, `auth.command`,
184
+ or the configured `authorization` header — rather than a generic message.
185
+
186
+ - `staleClients` counts only clients that were active near the restart. It counted every resume
187
+ record predating the restart, and the resume file holds a day of churn — one polling client
188
+ left hundreds of dead ids, so a restart reported 326 stale clients where there were three.
189
+
190
+ ## [1.1.0] - 2026-09-03
191
+
192
+ ### Added
193
+
194
+ - **Edit `servers.json` without restarting.** Save the file and the bridge picks it up, or run
195
+ `mcp-pacemaker reload` (also `POST /admin/reload`). The reload is a diff: servers whose
196
+ definition is unchanged keep their processes and their live sessions, so adding one server no
197
+ longer costs an outage on every other one. A file that does not parse, or a server with neither
198
+ `command` nor `url`, is rejected whole — the bridge logs why and keeps serving the last good
199
+ config, so a half-written editor save cannot take anything down. `MCP_CONFIG_WATCH=0` requires
200
+ an explicit reload instead of watching the file.
201
+
202
+ - **Per-server health.** `/api/status`, `doctor`, `top` and the dashboard now report each server
203
+ as `ok`, `failing` (with a count of consecutive failures), or `unknown`. Previously the only
204
+ signal was `lastError`, which is sticky and only ever accumulates: a server that failed once
205
+ last week looked exactly like one failing right now, and one that had recovered still showed
206
+ the old error. A server here returned 401 on every call for hours while the dashboard showed
207
+ nothing wrong. `unknown` is deliberately distinct from `ok` — a server nobody has called is not
208
+ claimed to be working.
209
+
210
+ Health is derived from traffic already being proxied, so it costs nothing. A JSON-RPC error
211
+ *from* a server counts as healthy, because the server answered; a 401/403, a 5xx, a spawn
212
+ failure or a non-zero exit counts as failing.
213
+
214
+ - **Optional health probing.** `MCP_HEALTH_INTERVAL_MS` (or per-server `healthIntervalMinutes`)
215
+ periodically calls an HTTP server so an idle one gets a verdict before a client needs it —
216
+ a credential that expires overnight is otherwise found by a failed tool call. Off by default,
217
+ since a probe is a real request to somebody else's service. stdio servers are not probed: that
218
+ would mean spawning a process, which costs more than the request it would pre-empt.
219
+
220
+ - **A durable log.** The bridge writes `bridge.log` alongside its config and rolls it at
221
+ `MCP_LOG_MAX_BYTES` (default 5 MB, keeping one previous file). Set it to `0` to disable.
222
+ Previously the only history was an in-memory buffer served by `/api/logs`, which holds a few
223
+ hundred lines: a client that reconnects every 30 seconds fills that in about two minutes, so
224
+ nothing that happened overnight could be explained the next morning. Redirecting stderr was
225
+ not an alternative, because the supervisor starts the bridge without a redirect. See
226
+ [SECURITY.md](SECURITY.md) for what the file does and does not contain.
227
+
228
+ ### Fixed
229
+
230
+ Everything since 1.0.0, including two defects that combined to send a client to the browser to
231
+ sign in repeatedly, for a server it was never responsible for authenticating.
232
+
233
+ - **A credential is cached for no longer than it is actually valid.** The configured
234
+ `refreshMinutes` was applied as a flat window, but an auth command usually returns a JWT served
235
+ from the provider's own cache — `az account get-access-token`, for instance, hands back a token
236
+ it minted earlier, which can already be most of the way through its life. The bridge then went
237
+ on presenting a dead token for the rest of the window while reporting it as healthy, and every
238
+ request to that server returned 401. A JWT's `exp` claim now bounds the cache entry, a token
239
+ that arrives already expired is not cached at all, and an opaque (non-JWT) credential still
240
+ falls back to `refreshMinutes`.
241
+
242
+ - **An upstream 401 is no longer turned into a login prompt for a server the bridge
243
+ authenticates.** The challenge was relayed for every proxied server, so when the bridge's own
244
+ credential was refused, the client was told to authenticate — a login it cannot win, because
245
+ the bridge overwrites whatever token the client returns with its own on the next request. The
246
+ effect was a client sent back to the browser to sign in over and over, for a server it was
247
+ never responsible for authenticating. A 401 on a server with `audience` or `auth.command` is
248
+ now reported as the bridge-side failure it is (`lastError`, plus a log line), the rejected
249
+ credential is discarded rather than served for the rest of its window, and no challenge is
250
+ passed on. Servers with `auth: none`, where the client really is the one authenticating, are
251
+ unchanged.
252
+
253
+ - **A bare `audience` is expanded into its token command in exactly one place.** The expansion
254
+ was duplicated, and the expanded string doubles as the token cache key, so the two copies
255
+ drifting apart would have minted under one key and looked it up under another — silently
256
+ disabling the cache.
257
+
258
+ - **The session retention window is now enforced while the bridge runs**, not only when
259
+ `sessions.json` is read at startup. A bridge left running for days kept accepting session ids
260
+ well past `MCP_RESUME_TTL_MS` and kept rewriting them to disk, so the file grew without bound
261
+ — on a bridge up for 28 hours, records 45 hours old were still being honoured under a 24-hour
262
+ window. Expired records are now refused at resume time and dropped as the file is written.
263
+
264
+ - **A server named after one of the bridge's own routes is now reported.** `api`, `admin`, `ui`,
265
+ `status` and `.well-known` are answered by the bridge before it consults the server table, so a
266
+ server with one of those names was unreachable and nothing said so — the client simply got the
267
+ bridge's own reply. `doctor` and the dashboard Health page now flag it.
268
+
269
+ - **A crash keeps its own error message.** When a server exited and the in-flight request then
270
+ timed out, the generic timeout overwrote the exit code and stderr tail that explained why —
271
+ replacing the cause with the symptom. A timeout is now only recorded while the child is still
272
+ alive.
273
+
274
+ - **A scheduled recycle added by a config edit now takes effect.** The recycle interval was
275
+ computed once at startup from the servers that had `recycleMinutes` then, so a server given one
276
+ later would never be recycled. The schedule is rebuilt on every reload.
277
+
278
+ ### Internal
279
+
280
+ Changes that affect contributors rather than users.
281
+
282
+ - **The test suite leaked 24 processes per run.** Tests tore the bridge down with `child.kill()`,
283
+ which on Windows is `TerminateProcess` — the bridge's `SIGTERM` handler never ran, so every
284
+ server child it had spawned was orphaned. Running the suite a few times inside the fixture's
285
+ 120s self-destruct window loaded the machine enough that teardown elsewhere missed its
286
+ deadline, which surfaced as an intermittent failure in the kill-tree test that looked unrelated.
287
+ Tests now tear down the whole process tree via `test/helpers/kill-bridge.mjs`; the leak is zero
288
+ and the suite passes 8 consecutive runs.
289
+
290
+ - **The suite checks itself.** `npm test` names its files explicitly, which is portable across
291
+ the Node versions in CI but drifts silently — two new test files were written, passed when run
292
+ directly, and did not run under `npm test` at all. `test/suite-integrity.test.mjs` now fails if
293
+ a test file is missing from the list, if the list names a file that no longer exists, or if two
294
+ files claim the same port.
295
+
296
+ - The `stubborn-mcp-server` test fixture deliberately outlives its stdin closing, and its
297
+ self-destruct timer was scoped to the `--heartbeat` flag. Fixtures spawned without that flag
298
+ therefore survived the run and accumulated across runs. The timer is now unconditional.
299
+ - The test port allocation, recorded at the top of `test/auth-token.test.mjs`, had drifted from
300
+ the assignments actually in use. It has been rebuilt from real usage and now lists every file.
301
+ A duplicated port fails only under the parallel suite and only intermittently, so the note is
302
+ worth keeping accurate.
303
+
304
+ ## [1.0.0] - 2026-08-30
305
+
306
+ First public release. It starts at 1.0.0 rather than 0.x because the bridge, its config format
307
+ and its HTTP surface have been running a real 15-server setup daily rather than only a test
308
+ suite, and the items below are the defects that shook out of that. Treating the config schema and
309
+ the `/<name>/mcp` endpoints as a stable contract from the first tag is honest about that, and
310
+ means later changes to them have to be versioned properly.
311
+
312
+ ### Added
313
+
314
+ - **Bridge** (`mcp-bridge`): a zero-dependency local HTTP endpoint that keeps MCP servers
315
+ running independently of any editor or CLI. stdio servers are exposed over both SSE
316
+ (`/<name>/sse`) and Streamable HTTP (`/<name>/mcp`); remote servers are reverse-proxied at
317
+ `/<name>` with credentials injected.
318
+ - **Setup CLI** (`mcp-pacemaker`): `init` detects installed MCP hosts, imports their servers,
319
+ wires them to the bridge, registers auto-start and starts it. `import`, `plan`, `install`,
320
+ `emit`, `upgrade`, `status`, `doctor`, `start`, `stop` and `uninstall` cover the rest of the
321
+ lifecycle. Every config write is backed up first, and `plan` is a dry run.
322
+ - **Host adapters**: VS Code, Cursor, Claude Desktop, Copilot CLI, Gemini CLI, Codex CLI and
323
+ Claude Code, each written in that host's own config format and location.
324
+ - **Pluggable auth**: `command` (e.g. `az account get-access-token`), `env`, `static` and
325
+ `none`, with a per-key token cache and configurable refresh window.
326
+ - **Auto-start and supervision**: a per-user scheduled task on Windows (logon plus workstation
327
+ unlock, no admin required) and a supervisor that restarts the bridge if it exits. A bridge
328
+ started against a port another pacemaker already serves adopts it and exits cleanly instead
329
+ of thrashing.
330
+ - **Dashboards**: a web UI at `/ui` and a live terminal UI via `mcp-pacemaker top`, both
331
+ showing sessions, warm pool, token expiry and recent events. The web dashboard streams over
332
+ SSE and can recycle a server; its `Health` tab runs the same checks as `doctor`.
333
+ - **HTTP API**: everything the dashboards show is plain JSON on the same loopback port —
334
+ `/status`, `/api/status`, `/api/doctor`, and the `/api/events` + `/api/logs` SSE streams.
335
+ `POST /admin/recycle/<name>` restarts a server and is guarded by loopback plus a per-start
336
+ nonce written next to the config.
337
+ - **Session controls**: warm pooling, `maxSessions` caps, idle reaping and per-server recycle.
338
+ Idle reaping frees the server process without ending the client's session — a client that comes
339
+ back is served transparently against a freshly started server.
340
+ - **Resumable sessions**: a client's session id survives the bridge restarting, a recycle, or an
341
+ idle reap. The bridge records the client's `initialize` and replays it against a fresh server
342
+ process, so the client keeps using the id it already holds. Disable with `MCP_RESUME=0`; the
343
+ retention window is `MCP_RESUME_TTL_MS` (default 24h). An explicit `DELETE` is final.
344
+ - **Scheduled recycle**: per-server `recycleMinutes` (or `MCP_RECYCLE_MINUTES`) restarts servers
345
+ that hold a credential they can only refresh interactively. Because a recycle is now invisible
346
+ to connected clients, the bridge does this itself rather than needing an editor extension.
347
+ - **Timeouts**: `MCP_REQUEST_TIMEOUT_MS` (default 30s) for ordinary calls, and a separate
348
+ `MCP_INIT_TIMEOUT_MS` (default 180s) for `initialize`, which also pays for spawning the server.
349
+ - **Proactive credential refresh**: a cached token is renewed shortly before it expires
350
+ (`MCP_TOKEN_REFRESH_LEAD_MS`, default 5 minutes) instead of the expiry being discovered by a
351
+ request. Only credentials already in use are refreshed.
352
+ - **OAuth pass-through for proxied servers**: the bridge answers
353
+ `/.well-known/oauth-protected-resource/<name>` from the upstream, so a client authenticating
354
+ against the bridge can discover where to log in. The `resource` field is rewritten to the
355
+ bridge's own URL for that server, because RFC 9728 has the client check it against the URL it
356
+ is addressing and reject a document that names anything else; a 401 challenge is rewritten the
357
+ same way so it points at the bridge's copy. `authorization_servers` and `scopes_supported` are
358
+ relayed untouched, and those are what determine the audience, so the token stays valid for the
359
+ upstream behind the bridge.
360
+ - **Queue at the concurrency cap**: at `maxSessions`, a new session waits briefly
361
+ (`MCP_QUEUE_TIMEOUT_MS`, default 10s) for a slot before being refused, so a burst is absorbed
362
+ rather than failed outright.
363
+
364
+ ### Hardening
365
+
366
+ There is nothing to fix relative to a previous release, but the following were found and fixed
367
+ by running this against a live 15-server setup before publishing. They are recorded because each
368
+ one is a trap for anyone building something similar.
369
+
370
+ - Tear down the whole process tree on Windows. Bare commands (`npx`, `uvx`, and `.cmd` shims)
371
+ spawn under a `cmd.exe` wrapper, so `child.kill()` signalled only the wrapper and left the
372
+ real server orphaned on every recycle, idle reap, session close and shutdown. The orphans
373
+ also held their tokens, so a recycle could leave two servers competing.
374
+ - Hold keep-alive sockets open past Node's 5 second default. A client busy across that window
375
+ never processes the server's `FIN`, so the socket stayed pooled and the next request failed
376
+ with `ECONNRESET`.
377
+ - Never cache an empty auth-command result. A token tool can exit 0 while printing nothing;
378
+ caching that served an empty credential for the full refresh window, so every upstream
379
+ request returned 401 until it expired.
380
+ - Mint one token per key. On startup every server requested its token at once and each spawned
381
+ its own token process, which is what made those concurrent calls return nothing.
382
+ - Run auth commands through a shell so quoted arguments survive. Constructing `cmd /c <command>`
383
+ by hand re-escaped embedded quotes and broke any command quoting a path with spaces.
384
+ - Wire VS Code over Streamable HTTP instead of SSE, the transport behind `-32001` reconnect
385
+ failures.
386
+ - Rewrite an existing entry in place when a host already lists a bridged server under a
387
+ different key, instead of adding a duplicate registration of the same server.
388
+ - Report warnings in the `doctor` summary rather than printing `all good.` beneath them.
389
+ - Resolve a server's relative command and arguments against a working directory it carries in
390
+ its own config, instead of depending on how the bridge was launched. A server defined as
391
+ `node .vscode/mcp/<name>/index.js` failed with `MODULE_NOT_FOUND` whenever the bridge started
392
+ without the matching `--cwd`.
393
+ - Report a server that starts and then exits non-zero, including the tail of its stderr.
394
+ `lastError` was only set when the process failed to spawn, so a server crashing on startup
395
+ left `/api/status` and `doctor` looking healthy while every session died.
396
+ - Give `initialize` its own timeout. Sharing the 30s call timeout meant a server whose first run
397
+ downloads itself returned `-32001 upstream timeout` and appeared broken.
398
+ - Reap idle sessions by default (`MCP_IDLE_TIMEOUT_MS`, 30 minutes). Clients are not obliged to
399
+ send `DELETE` and some never do; one observed client opened a session per run and left 241
400
+ server processes running within an hour. Reaping is safe because a returning client's session
401
+ is transparently re-established.
402
+ - Do not let a killed child's late `exit` event delete the session that replaced it. `taskkill /F
403
+ /T` takes about a second on Windows, so a client returning inside that window was resumed onto
404
+ a fresh server and then silently dropped when the old exit finally arrived — the failure that
405
+ session resume exists to prevent. The mapping is now torn down only if it still points at the
406
+ child that exited.
407
+ - Do not reap a session that has a request in flight, and measure the idle clock from when a
408
+ request finished rather than when it started. A call slower than the idle timeout previously
409
+ had its server killed underneath it, leaving the caller waiting for a reply that could never
410
+ arrive.
411
+ - Report a server whose relative script path cannot resolve. `doctor` and the dashboard health
412
+ page now flag a stdio server whose arguments name a path that does not exist from the directory
413
+ it would run in, and warn when a relative path is used with no `cwd` at all. This failure was
414
+ otherwise silent: the server was listed as configured and only died when a client first called
415
+ it. Package specs passed to a runner (`npx @scope/pkg`) are not mistaken for paths.
416
+ - Strip hop-by-hop headers from proxied responses (RFC 9110 §7.6.1). A header the upstream named
417
+ in its own `Connection` header, or a `trailer` it announced, is connection-scoped and was being
418
+ relayed to the client, where it is meaningless or misleading.
419
+ - Do not report a teardown the bridge initiated as a crash. `taskkill /F` exits non-zero, so
420
+ every recycle, idle reap and session close was recorded as a failure, burying real ones.
421
+
422
+ [1.2.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v1.2.0
423
+ [1.1.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v1.1.0
424
+ [1.0.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v1.0.0
@@ -0,0 +1,156 @@
1
+ # Contributing
2
+
3
+ Thanks for taking a look. This is a small, deliberately dependency-light project, so a few
4
+ constraints matter more than they might elsewhere.
5
+
6
+ ## Getting set up
7
+
8
+ ```bash
9
+ git clone https://github.com/girishkvs/mcp-pacemaker
10
+ cd mcp-pacemaker
11
+ npm ci
12
+ npm test
13
+ ```
14
+
15
+ Node 20 or newer. The bridge itself (`bin/mcp-bridge.mjs`) has **zero npm runtime dependencies**
16
+ and must stay that way; it runs 24/7 on a developer's machine and its dependency surface is
17
+ part of its security posture. The setup CLI and the dashboard may use dependencies.
18
+
19
+ ## Lockfiles must resolve to the public registry
20
+
21
+ CI fails any lockfile containing a `resolved` URL outside `registry.npmjs.org`. If you work
22
+ behind a private mirror, re-resolve before committing:
23
+
24
+ ```bash
25
+ npm install --registry https://registry.npmjs.org
26
+ ```
27
+
28
+ ## Tests
29
+
30
+ ```bash
31
+ npm test
32
+ ```
33
+
34
+ Tests are `node:test` only, no framework. Each test file that starts a bridge owns its own
35
+ ports, because files run in parallel; the current allocation is listed at the top of
36
+ `test/auth-token.test.mjs`. Pick unused ones for a new file.
37
+
38
+ Before submitting, run the complete `npm test` command with its normal file concurrency.
39
+ Focused or serialized runs help diagnose failures but do not replace that run. Record the
40
+ exact Node and npm versions, OS, architecture, and command; matching only the Node major is
41
+ not enough to reproduce a CI failure.
42
+
43
+ Filesystem coverage must include native POSIX absolute paths, macOS temporary-directory
44
+ aliases, and Windows 8.3 temporary paths as well as canonical paths. Preserve those inputs
45
+ when reproducing a failure instead of normalizing the whole test environment. Canonicalize
46
+ owned scratch roots and native watcher inputs at their boundaries; keep rejecting unexpected
47
+ links, replaced directories, and files outside the approved inventory. A Windows-only pass
48
+ does not establish Linux or macOS correctness; the hosted platform matrix remains required.
49
+
50
+ The complete Windows suite needs audit-read rights for successful configuration-write cases.
51
+ Security-specific fixtures modify only their own temporary files. Automatic writes deliberately
52
+ fail closed when audit policy is unreadable; the suite also exercises that restricted path.
53
+
54
+ Windows automatic edits use the packaged .NET Framework 4.6.2 helper. Changes to its source
55
+ must rebuild the executable and metadata with
56
+ `tools/windows-security-helper/build.ps1`, then pass `-Verify` with the recorded compiler
57
+ and reference assemblies. See the [helper build instructions](tools/windows-security-helper/README.md).
58
+ The portable Node suite checks normalized source/build-script and binary hashes on every CI
59
+ platform; Windows also exercises the real executable. Those checks do not replace the
60
+ byte-for-byte rebuild, and a rolling CI image may not contain the recorded compiler.
61
+ No compiler or PowerShell host runs in the production config-write path.
62
+
63
+ **A regression test must be shown to fail without its fix.** Revert the fix, watch the test
64
+ fail, restore it, watch it pass. Several bugs in this repo were originally "covered" by tests
65
+ that passed against the broken code — a test that cannot fail is worse than no test, because
66
+ it also stops anyone else from looking. If a bug involves process lifetime or timing, prefer
67
+ observing a real signal (a heartbeat file, a recorded request) over enumerating processes or
68
+ sleeping a fixed interval.
69
+
70
+ ## Real-version compatibility gates
71
+
72
+ These gates run actual legacy/current CLI, API and built dashboard code, not mocked backend
73
+ responses. Scope is explicitly **1.3.0, 1.3.1, 2.0.0 and 2.0.1**. The maintained branches use
74
+ the same harness, with exact version assertions and separate historical evidence.
75
+
76
+ | Preparation | Selected legacy / current pair |
77
+ |---|---|
78
+ | Default in this 1.3.1 checkout | Packed 1.3.1 / immutable 2.0.0 |
79
+ | Default in the 2.0.1 checkout | Immutable 1.3.0 / packed 2.0.1 |
80
+ | `npm run compat:prepare:baseline` | Immutable 1.3.0 / immutable 2.0.0 |
81
+ | `npm run compat:prepare -- --peer-root <opposite-patch-checkout>` | Packed 1.3.1 / packed 2.0.1 |
82
+
83
+ ```bash
84
+ npm run compat:prepare
85
+ npm run test:compat
86
+ npm run compat:clean
87
+
88
+ npm --prefix ui run build
89
+ # Install the existing Playwright Chromium browser from the ui directory for CI.
90
+ # A local run can instead set MCP_COMPAT_BROWSER_CHANNEL=chrome.
91
+ npm run compat:prepare
92
+ npm run test:compat:browser
93
+ npm run compat:clean
94
+ ```
95
+
96
+ Run both gates for the explicit patch pair before publishing the complete dual-major set;
97
+ the default per-branch pairs alone do not establish that result. Run cleanup before selecting
98
+ another pair or performing `npm ci`. CI covers Windows/Linux/macOS with Node 20/22/24 and a
99
+ Linux/Node 22 Chromium lane. Node 20 is legacy runtime compatibility, not a supported secure runtime.
100
+
101
+ Publication artifact gates add `--candidate-tarball <absolute.tgz> --candidate-sha256 <digest>`.
102
+ This supplies the checkout's own candidate role without repacking it: here that is the legacy
103
+ 1.3.1 role. Complete input hashes and the archive digest must match; historical mode cannot
104
+ substitute a patch. The manifest also records the exact npm CLI version.
105
+
106
+ Historical sources are pinned to `2d525f4ced01b978a5aeb83aef69145e96cced05` (1.3.0) and
107
+ `db4812a1cbfb8546c814a656397b9a48ccf0f32c` (2.0.0). Missing commits are fetched read-only by
108
+ exact SHA into an owned temporary repository. Archives honor canonical Git line endings and
109
+ committed attributes. Local candidates are packed without lifecycle scripts, compared against
110
+ their source files, and restored using their own unchanged producer locks. Caller-resolved
111
+ registry/cache settings are honored without a fallback; locks must retain public URLs.
112
+
113
+ The manifest records selection mode, exact versions and references, each archive/file hash,
114
+ candidate inputs and Node major. Editing either candidate invalidates preparation. Owned
115
+ temporary HOME/config/ports keep the user's state isolated. Identity-checked cleanup never
116
+ follows links or claims another directory. Failed cleanup remains retryable.
117
+
118
+ The selected 2.x client must preserve immediate 1.x HTTP 200 saves/Undo. The selected 1.x
119
+ client gets HTTP 409 from 2.x with a fresh nonce, or HTTP 401 after a real restart with a stale
120
+ nonce; refresh must load the selected new assets. The 2.x cases require HTTP 202 pending
121
+ receipts, actual activation, cancellation and whole-batch Undo. Settled downgrade checks
122
+ active bytes, instance changes and unchanged caller authority. Known Windows audit-unavailable
123
+ legacy HTTP 403 must remain unchanged; it is not counted as a successful legacy write.
124
+ Unresolved transactions, arbitrary ACL changes and OS-service installation are separate gates.
125
+
126
+ ## npm consumer validation
127
+
128
+ `npm run consumer:check -- --tarball <absolute-file> --sha256 <digest> --version 1.3.1 --name mcp-pacemaker`
129
+ uses an owned disposable project and a separate global prefix. It does not copy the producer
130
+ lock: it records fresh dependency resolution, checks the installed command shim and exercises
131
+ the packaged bridge, CLI and UI assets with synthetic servers/private state.
132
+
133
+ Run with normal npm install-script policy and again with `--ignore-scripts`. Caller registry,
134
+ cache and offline settings are honored, but user credentials and real host configuration are
135
+ not used. Registry signatures and provenance remain separate post-publication checks.
136
+
137
+ ## Changing how a host is wired
138
+
139
+ Host adapters live in `HOSTS` in `bin/cli.mjs`. Each one declares where that host's config
140
+ lives, the JSON key its servers sit under, and how a bridged entry is serialized. When you add
141
+ or change one:
142
+
143
+ - Add a case to `test/cli.test.mjs` asserting the exact entry shape.
144
+ - Never write a host config without backing it up first, and make sure `plan` reports the
145
+ change accurately — it is what people rely on before letting the tool touch their setup.
146
+ - Re-run `mcp-pacemaker plan` against a real config afterwards. It should report no changes
147
+ for a setup that is already correctly wired; anything else means the adapter and the
148
+ reconciliation logic disagree.
149
+
150
+ ## Pull requests
151
+
152
+ - One concern per PR.
153
+ - Explain what broke and how you proved it fixed, not just what you changed.
154
+ - Comment only what needs clarifying — why something is done a particular way, not what the
155
+ line does.
156
+ - Update `CHANGELOG.md` under an `Unreleased` heading.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Girish Konda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.