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
package/CHANGELOG.md ADDED
@@ -0,0 +1,452 @@
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
+ - Resolve native directory watcher paths before watching logs and test control files,
13
+ avoiding Windows short-path watcher crashes without changing the selected files.
14
+
15
+ ### Changed
16
+
17
+ - Prepare 2.0.1 for a separate npm publication, with exact-version guidance from the current
18
+ major's maintained channel. Channel rollback and backend adoption are not automatic upgrades.
19
+ - Document durable service replacement, explicit legacy maintenance, platform prerequisites,
20
+ and the difference between npm provenance and Windows executable signing.
21
+
22
+ ### Added
23
+
24
+ - Source-bound, manual npm preparation/staging tooling, bundled third-party notices, and
25
+ fresh-consumer package checks.
26
+ - Explicit patch compatibility pairs alongside the unchanged 1.3.0/2.0.0 historical pair;
27
+ maintenance-branch CI and Node 24 coverage.
28
+
29
+ ## [2.0.0] - 2026-09-12
30
+
31
+ ### Breaking
32
+
33
+ - Version 2.0 changes the pooling save contract from immediate application to queued batches.
34
+ API clients must send `x-mcp-pooling-batch: 1` and distinguish HTTP 202 acceptance from
35
+ activation. Older clients are rejected without staging a change.
36
+ - The `prewarm` CLI returns after staging, not after activation. Scripts must observe the batch
37
+ result before relying on the new settings. Undo restores the whole batch, not one server.
38
+ - Update the CLI and reload open dashboard pages when upgrading the bridge from 1.3.0.
39
+ Before downgrading, use 2.0 to resolve any pending or interrupted configuration transaction.
40
+
41
+ ### Changed
42
+
43
+ - Pooling changes stage a complete configuration copy and reload five seconds after the last
44
+ accepted change. The UI and CLI distinguish Pending, Applying and completed activation;
45
+ Reload now flushes a batch, and Cancel/Undo explicitly applies to the whole batch.
46
+ - Batch-capable clients identify the asynchronous save protocol. Older clients are rejected
47
+ before staging instead of reporting an accepted change as already enabled.
48
+ - Windows configuration saves use directory-inherited auditing without requiring audit-read
49
+ privilege. A non-blocking notice explains that custom per-file audit rules may not carry
50
+ forward; ordinary access and supported integrity protections remain required.
51
+ - Windows saves retain an existing owner that matches the caller user or token default owner,
52
+ preserving supported legacy writes after downgrade. Other owners still use checked transfer,
53
+ and source write access remains required.
54
+
55
+ ### Added
56
+
57
+ - Permanent real-version compatibility gates for **1.3.0 and 2.0.0 only**, using pinned
58
+ published 1.3.0 code and an actual packed 2.0.0 candidate. CI covers CLI/API pairs on
59
+ Windows/Linux/macOS with Node 20/22 and built dashboard pairs in the Linux Chromium job:
60
+ immediate legacy saves/Undo, fresh-nonce protocol refusal, actual old-tab restart/refresh,
61
+ queued save/Cancel/Undo, settled upgrade/downgrade configuration bytes, and same-authority
62
+ legacy write/Undo capability after downgrade (or the unchanged known Windows audit refusal).
63
+
64
+ ### Fixed
65
+
66
+ - Preserve recovery checkpoints after uncertain post-placement failures without replaying
67
+ writes or accepting corrupt journals.
68
+ - Avoid dispatching process-tree termination for children already known to have exited,
69
+ including failed session resumes.
70
+
71
+ ### Security
72
+
73
+ - Raise the `smol-toml` minimum and locked version from 1.7.0 to 1.8.0 to address
74
+ [CVE-2026-85730](https://github.com/squirrelchat/smol-toml/security/advisories/GHSA-7w5x-hrqm-74c2).
75
+ Malformed TOML ending with a comment in an unclosed array or inline table now produces a
76
+ parse error instead of hanging the CLI while reading Codex configuration.
77
+
78
+ ## [1.3.0] - 2026-09-08
79
+
80
+ ### Added
81
+
82
+ - Cumulative per-server process launch, failed launch, warm adoption, new session and resume
83
+ counters. The status snapshot identifies the bridge instance and interval start so process
84
+ churn can be compared without mixing time windows. Latency samples remain capped separately.
85
+ - One-click pre-warming controls and conflict-safe Undo, with the requested warm count and
86
+ resident-process cost shown before activation. Pooling policy edits preserve active children.
87
+ - A consolidated pre-warming dashboard tab and `mcp-pacemaker prewarm` CLI view, both using
88
+ the same snapshot data. Explicit CLI actions enable, disable or undo one server's settings.
89
+ - `mcp-pacemaker logs` reads the durable log and retained rotation with `--follow`, `--since`,
90
+ `--server` and literal `--grep`. Multiline context, partial UTF-8 writes and rotation are
91
+ handled without treating continuation lines as independent server records.
92
+ - Opt-in shared stdio children for compatible stateless tools sessions. One real initialization
93
+ serves virtual sessions with owner-scoped IDs, progress, cancellation and cursors, bounded
94
+ queues, idle retention and draining. Unsupported features fail explicitly; uncertain tool
95
+ calls are never replayed. UI and CLI distinguish shared children from uninitialized pools.
96
+
97
+ ### Fixed
98
+
99
+ - Reserve session capacity before asynchronous startup, preventing queued cold starts from
100
+ overbooking the configured cap. Concurrent resumes of one session share one handshake.
101
+ - Bind session lookup and deletion to the requested server. A stale exit or deleted resume
102
+ cannot remove or resurrect a replacement session.
103
+ - Distinguish bridge-generated transport errors from identical numeric codes returned by an
104
+ upstream, so a legitimate server response is not mislabeled as a bridge timeout.
105
+ - Guard shared JSON before serialization in both directions, including on Node 20, and bound
106
+ combined response sizes. Excessive depth or output produces controlled errors rather than
107
+ crashing the bridge or silently replaying tool calls.
108
+ - Identify changed metadata fields when a pooling write is refused by the conflict guard.
109
+ - Avoid false Windows pooling conflicts from unrelated timestamp updates while retaining
110
+ audit, integrity-label, permission and file-identity checks. Refuse automatic writes when
111
+ audit policy cannot be verified or preserved.
112
+ - Run configuration writes in a bounded, serialized worker so Windows permission inspection
113
+ does not block the bridge's request processing.
114
+ - Replace per-write PowerShell startup and C# compilation with a bundled, reproducibly built
115
+ Windows security helper while retaining the same permission-preservation checks.
116
+ - Bound configuration-write queueing and execution with shared cancellation. Report uncertain
117
+ or late-committed replacements honestly and reload saved state after a worker exits, even
118
+ with config watching disabled. Never replay a timed-out operation.
119
+ - Update the UI build's `browserslist` dependency to 4.28.9 to address its published memory-growth
120
+ and custom-stats advisories.
121
+
122
+ ## [1.2.0] - 2026-09-04
123
+
124
+ ### Added
125
+
126
+ - **Cold start is measured.** The bridge spawns every child, so it is the only thing positioned
127
+ to know what starting a server actually costs — and it was throwing that away. `/api/status`
128
+ and the dashboard now report p50/p95/max spawn time per server, timed from `spawn()` to the
129
+ server answering `initialize`, because a process existing says nothing about the package
130
+ manager work that follows it. On the author's machine one server measured a 20s cold start
131
+ while the pre-warm pool sat empty and a server that starts in 200ms had a full one.
132
+
133
+ - **A gate on concurrent cold starts** (`MCP_MAX_CONCURRENT_SPAWNS`, default 2). Servers launched
134
+ through `npx`, `uvx` or `dnx` share one package cache, and starting several at once can corrupt
135
+ it: 17 simultaneous `npx` invocations here produced `npm error code ECOMPROMISED / Lock
136
+ compromised` and failed every one of those sessions. No server definition can fix that — it is
137
+ a property of the concurrency — so the bridge queues cold starts instead. The slot is held
138
+ until the child first speaks, which is when the expensive part is over.
139
+
140
+ Only servers that actually use a package manager are gated; a plain `node server.js` shares no
141
+ cache and was never at risk. Detection is from the command, and per-server
142
+ `sharedPackageCache` forces it either way for a wrapper script the heuristic cannot see.
143
+ Waiting for a slot is bounded by `MCP_SPAWN_GATE_WAIT_MS` (15s) — past that the spawn proceeds
144
+ ungated, because a corrupted cache is a risk while a hung client request is a certainty.
145
+
146
+ - **Pooling advice, never pooling by default.** When a server's measured cold start passes
147
+ `MCP_POOL_ADVICE_MS` (2s), `doctor` and the dashboard name the number and print the exact
148
+ config that would hide it, sized from observed peak concurrency. It is not applied: a warm pool
149
+ costs a resident process per slot, and that is the user's call to make. Cold start is otherwise
150
+ invisible to the person paying for it, who just experiences a slow tool.
151
+
152
+ - **Pools size themselves once you opt in.** `sharing: "pool"` with no `minWarm` now targets peak
153
+ concurrent sessions over the last hour, capped by `MCP_WARM_MAX` (8). An explicit `minWarm`
154
+ still wins. Defaulting to one warm child is how a burst of 17 sessions ended up paying 16 cold
155
+ starts on a server that was configured to pool.
156
+
157
+ - **Restart reporting.** `/api/status` carries how long ago the bridge restarted and how many
158
+ clients held a session before it and have not come back; `status` warns and the dashboard shows
159
+ a banner. The bridge re-establishes those sessions correctly, but some clients treat a single
160
+ connection failure as permanent and need an MCP reload — this says who.
161
+
162
+ - `cappedSessions` alongside `sessions`, so a server's count matches the cap actually enforced.
163
+ The cap counts Streamable HTTP sessions while the display summed those and classic SSE, which
164
+ is how a server showed `33/32`.
165
+
166
+ ### Changed
167
+
168
+ - **A 401 or 403 only counts against health when the bridge holds the credential.** For a server
169
+ with `auth: {type: none}` the *client* authenticates, so an opening 401 is the handshake
170
+ working as designed. Counting it flipped a healthy server to `failing` every 60 seconds. The
171
+ rule is now the one that matters: failing means the tool actually failed.
172
+
173
+ - **The bridge's own 503 counts as a failure.** Refusing a session at the concurrency cap is the
174
+ most client-visible failure the bridge produces, and it was invisible to health — an operator
175
+ saw a healthy server while the bridge was the thing saying no.
176
+
177
+ - A terminated session still answers **404**, per the transport spec, which makes re-initializing
178
+ on 404 a client MUST. An earlier revision of this work returned 410 for sessions known to be
179
+ gone; it reads better to a human and strands a compliant client, since 410 has no defined
180
+ recovery behaviour. The distinction moved to the response body and the log, where a client is
181
+ not allowed to care about it.
182
+
183
+ ### Fixed
184
+
185
+ - **A warm pool never refilled after a child died on its own.** The exit handler removed the
186
+ corpse and stopped; refill only ran on boot, on take, on recycle and on reload. A pooled server
187
+ that went quiet drained to empty and stayed there, silently degrading to a cold spawn per
188
+ session — the exact failure pooling exists to prevent, and invisible because an empty pool
189
+ looks like one nobody has asked for anything yet. Seen here as one server sitting at `warm: 0`
190
+ for three hours while another with identical config stayed full purely because it was busy.
191
+ Refill now happens on an unattended exit, with backoff so a child that dies instantly cannot
192
+ become a spawn loop.
193
+
194
+ - **A pooled server counted every failure twice.** A warm child adopted into a session kept the
195
+ pool's exit handler and gained the session's, and both reported the exit — so an identical
196
+ server lost health twice as fast for having been pooled.
197
+
198
+ - **A pool refilled one child per take,** so a burst that emptied it recovered long after the
199
+ burst was over. Refill now runs toward the target in one pass, bounded by the spawn gate.
200
+
201
+ - A static `authorization` header did not mark a server as bridge-authenticated, so its 401 read
202
+ as `unknown` forever and its challenge was relayed to a client that could not act on it.
203
+
204
+ - The health probe no longer calls servers the bridge does not authenticate.
205
+
206
+ - SSE streams are closed on shutdown instead of dropped. A clean close reads as "reconnect" to a
207
+ client; a dropped TCP connection reads as a transport fault, and some clients latch on that.
208
+
209
+ - A failed request names the credential source it actually used — `audience X`, `auth.command`,
210
+ or the configured `authorization` header — rather than a generic message.
211
+
212
+ - `staleClients` counts only clients that were active near the restart. It counted every resume
213
+ record predating the restart, and the resume file holds a day of churn — one polling client
214
+ left hundreds of dead ids, so a restart reported 326 stale clients where there were three.
215
+
216
+ ## [1.1.0] - 2026-09-03
217
+
218
+ ### Added
219
+
220
+ - **Edit `servers.json` without restarting.** Save the file and the bridge picks it up, or run
221
+ `mcp-pacemaker reload` (also `POST /admin/reload`). The reload is a diff: servers whose
222
+ definition is unchanged keep their processes and their live sessions, so adding one server no
223
+ longer costs an outage on every other one. A file that does not parse, or a server with neither
224
+ `command` nor `url`, is rejected whole — the bridge logs why and keeps serving the last good
225
+ config, so a half-written editor save cannot take anything down. `MCP_CONFIG_WATCH=0` requires
226
+ an explicit reload instead of watching the file.
227
+
228
+ - **Per-server health.** `/api/status`, `doctor`, `top` and the dashboard now report each server
229
+ as `ok`, `failing` (with a count of consecutive failures), or `unknown`. Previously the only
230
+ signal was `lastError`, which is sticky and only ever accumulates: a server that failed once
231
+ last week looked exactly like one failing right now, and one that had recovered still showed
232
+ the old error. A server here returned 401 on every call for hours while the dashboard showed
233
+ nothing wrong. `unknown` is deliberately distinct from `ok` — a server nobody has called is not
234
+ claimed to be working.
235
+
236
+ Health is derived from traffic already being proxied, so it costs nothing. A JSON-RPC error
237
+ *from* a server counts as healthy, because the server answered; a 401/403, a 5xx, a spawn
238
+ failure or a non-zero exit counts as failing.
239
+
240
+ - **Optional health probing.** `MCP_HEALTH_INTERVAL_MS` (or per-server `healthIntervalMinutes`)
241
+ periodically calls an HTTP server so an idle one gets a verdict before a client needs it —
242
+ a credential that expires overnight is otherwise found by a failed tool call. Off by default,
243
+ since a probe is a real request to somebody else's service. stdio servers are not probed: that
244
+ would mean spawning a process, which costs more than the request it would pre-empt.
245
+
246
+ - **A durable log.** The bridge writes `bridge.log` alongside its config and rolls it at
247
+ `MCP_LOG_MAX_BYTES` (default 5 MB, keeping one previous file). Set it to `0` to disable.
248
+ Previously the only history was an in-memory buffer served by `/api/logs`, which holds a few
249
+ hundred lines: a client that reconnects every 30 seconds fills that in about two minutes, so
250
+ nothing that happened overnight could be explained the next morning. Redirecting stderr was
251
+ not an alternative, because the supervisor starts the bridge without a redirect. See
252
+ [SECURITY.md](SECURITY.md) for what the file does and does not contain.
253
+
254
+ ### Fixed
255
+
256
+ Everything since 1.0.0, including two defects that combined to send a client to the browser to
257
+ sign in repeatedly, for a server it was never responsible for authenticating.
258
+
259
+ - **A credential is cached for no longer than it is actually valid.** The configured
260
+ `refreshMinutes` was applied as a flat window, but an auth command usually returns a JWT served
261
+ from the provider's own cache — `az account get-access-token`, for instance, hands back a token
262
+ it minted earlier, which can already be most of the way through its life. The bridge then went
263
+ on presenting a dead token for the rest of the window while reporting it as healthy, and every
264
+ request to that server returned 401. A JWT's `exp` claim now bounds the cache entry, a token
265
+ that arrives already expired is not cached at all, and an opaque (non-JWT) credential still
266
+ falls back to `refreshMinutes`.
267
+
268
+ - **An upstream 401 is no longer turned into a login prompt for a server the bridge
269
+ authenticates.** The challenge was relayed for every proxied server, so when the bridge's own
270
+ credential was refused, the client was told to authenticate — a login it cannot win, because
271
+ the bridge overwrites whatever token the client returns with its own on the next request. The
272
+ effect was a client sent back to the browser to sign in over and over, for a server it was
273
+ never responsible for authenticating. A 401 on a server with `audience` or `auth.command` is
274
+ now reported as the bridge-side failure it is (`lastError`, plus a log line), the rejected
275
+ credential is discarded rather than served for the rest of its window, and no challenge is
276
+ passed on. Servers with `auth: none`, where the client really is the one authenticating, are
277
+ unchanged.
278
+
279
+ - **A bare `audience` is expanded into its token command in exactly one place.** The expansion
280
+ was duplicated, and the expanded string doubles as the token cache key, so the two copies
281
+ drifting apart would have minted under one key and looked it up under another — silently
282
+ disabling the cache.
283
+
284
+ - **The session retention window is now enforced while the bridge runs**, not only when
285
+ `sessions.json` is read at startup. A bridge left running for days kept accepting session ids
286
+ well past `MCP_RESUME_TTL_MS` and kept rewriting them to disk, so the file grew without bound
287
+ — on a bridge up for 28 hours, records 45 hours old were still being honoured under a 24-hour
288
+ window. Expired records are now refused at resume time and dropped as the file is written.
289
+
290
+ - **A server named after one of the bridge's own routes is now reported.** `api`, `admin`, `ui`,
291
+ `status` and `.well-known` are answered by the bridge before it consults the server table, so a
292
+ server with one of those names was unreachable and nothing said so — the client simply got the
293
+ bridge's own reply. `doctor` and the dashboard Health page now flag it.
294
+
295
+ - **A crash keeps its own error message.** When a server exited and the in-flight request then
296
+ timed out, the generic timeout overwrote the exit code and stderr tail that explained why —
297
+ replacing the cause with the symptom. A timeout is now only recorded while the child is still
298
+ alive.
299
+
300
+ - **A scheduled recycle added by a config edit now takes effect.** The recycle interval was
301
+ computed once at startup from the servers that had `recycleMinutes` then, so a server given one
302
+ later would never be recycled. The schedule is rebuilt on every reload.
303
+
304
+ ### Internal
305
+
306
+ Changes that affect contributors rather than users.
307
+
308
+ - **The test suite leaked 24 processes per run.** Tests tore the bridge down with `child.kill()`,
309
+ which on Windows is `TerminateProcess` — the bridge's `SIGTERM` handler never ran, so every
310
+ server child it had spawned was orphaned. Running the suite a few times inside the fixture's
311
+ 120s self-destruct window loaded the machine enough that teardown elsewhere missed its
312
+ deadline, which surfaced as an intermittent failure in the kill-tree test that looked unrelated.
313
+ Tests now tear down the whole process tree via `test/helpers/kill-bridge.mjs`; the leak is zero
314
+ and the suite passes 8 consecutive runs.
315
+
316
+ - **The suite checks itself.** `npm test` names its files explicitly, which is portable across
317
+ the Node versions in CI but drifts silently — two new test files were written, passed when run
318
+ directly, and did not run under `npm test` at all. `test/suite-integrity.test.mjs` now fails if
319
+ a test file is missing from the list, if the list names a file that no longer exists, or if two
320
+ files claim the same port.
321
+
322
+ - The `stubborn-mcp-server` test fixture deliberately outlives its stdin closing, and its
323
+ self-destruct timer was scoped to the `--heartbeat` flag. Fixtures spawned without that flag
324
+ therefore survived the run and accumulated across runs. The timer is now unconditional.
325
+ - The test port allocation, recorded at the top of `test/auth-token.test.mjs`, had drifted from
326
+ the assignments actually in use. It has been rebuilt from real usage and now lists every file.
327
+ A duplicated port fails only under the parallel suite and only intermittently, so the note is
328
+ worth keeping accurate.
329
+
330
+ ## [1.0.0] - 2026-08-30
331
+
332
+ First public release. It starts at 1.0.0 rather than 0.x because the bridge, its config format
333
+ and its HTTP surface have been running a real 15-server setup daily rather than only a test
334
+ suite, and the items below are the defects that shook out of that. Treating the config schema and
335
+ the `/<name>/mcp` endpoints as a stable contract from the first tag is honest about that, and
336
+ means later changes to them have to be versioned properly.
337
+
338
+ ### Added
339
+
340
+ - **Bridge** (`mcp-bridge`): a zero-dependency local HTTP endpoint that keeps MCP servers
341
+ running independently of any editor or CLI. stdio servers are exposed over both SSE
342
+ (`/<name>/sse`) and Streamable HTTP (`/<name>/mcp`); remote servers are reverse-proxied at
343
+ `/<name>` with credentials injected.
344
+ - **Setup CLI** (`mcp-pacemaker`): `init` detects installed MCP hosts, imports their servers,
345
+ wires them to the bridge, registers auto-start and starts it. `import`, `plan`, `install`,
346
+ `emit`, `upgrade`, `status`, `doctor`, `start`, `stop` and `uninstall` cover the rest of the
347
+ lifecycle. Every config write is backed up first, and `plan` is a dry run.
348
+ - **Host adapters**: VS Code, Cursor, Claude Desktop, Copilot CLI, Gemini CLI, Codex CLI and
349
+ Claude Code, each written in that host's own config format and location.
350
+ - **Pluggable auth**: `command` (e.g. `az account get-access-token`), `env`, `static` and
351
+ `none`, with a per-key token cache and configurable refresh window.
352
+ - **Auto-start and supervision**: a per-user scheduled task on Windows (logon plus workstation
353
+ unlock, no admin required) and a supervisor that restarts the bridge if it exits. A bridge
354
+ started against a port another pacemaker already serves adopts it and exits cleanly instead
355
+ of thrashing.
356
+ - **Dashboards**: a web UI at `/ui` and a live terminal UI via `mcp-pacemaker top`, both
357
+ showing sessions, warm pool, token expiry and recent events. The web dashboard streams over
358
+ SSE and can recycle a server; its `Health` tab runs the same checks as `doctor`.
359
+ - **HTTP API**: everything the dashboards show is plain JSON on the same loopback port —
360
+ `/status`, `/api/status`, `/api/doctor`, and the `/api/events` + `/api/logs` SSE streams.
361
+ `POST /admin/recycle/<name>` restarts a server and is guarded by loopback plus a per-start
362
+ nonce written next to the config.
363
+ - **Session controls**: warm pooling, `maxSessions` caps, idle reaping and per-server recycle.
364
+ Idle reaping frees the server process without ending the client's session — a client that comes
365
+ back is served transparently against a freshly started server.
366
+ - **Resumable sessions**: a client's session id survives the bridge restarting, a recycle, or an
367
+ idle reap. The bridge records the client's `initialize` and replays it against a fresh server
368
+ process, so the client keeps using the id it already holds. Disable with `MCP_RESUME=0`; the
369
+ retention window is `MCP_RESUME_TTL_MS` (default 24h). An explicit `DELETE` is final.
370
+ - **Scheduled recycle**: per-server `recycleMinutes` (or `MCP_RECYCLE_MINUTES`) restarts servers
371
+ that hold a credential they can only refresh interactively. Because a recycle is now invisible
372
+ to connected clients, the bridge does this itself rather than needing an editor extension.
373
+ - **Timeouts**: `MCP_REQUEST_TIMEOUT_MS` (default 30s) for ordinary calls, and a separate
374
+ `MCP_INIT_TIMEOUT_MS` (default 180s) for `initialize`, which also pays for spawning the server.
375
+ - **Proactive credential refresh**: a cached token is renewed shortly before it expires
376
+ (`MCP_TOKEN_REFRESH_LEAD_MS`, default 5 minutes) instead of the expiry being discovered by a
377
+ request. Only credentials already in use are refreshed.
378
+ - **OAuth pass-through for proxied servers**: the bridge answers
379
+ `/.well-known/oauth-protected-resource/<name>` from the upstream, so a client authenticating
380
+ against the bridge can discover where to log in. The `resource` field is rewritten to the
381
+ bridge's own URL for that server, because RFC 9728 has the client check it against the URL it
382
+ is addressing and reject a document that names anything else; a 401 challenge is rewritten the
383
+ same way so it points at the bridge's copy. `authorization_servers` and `scopes_supported` are
384
+ relayed untouched, and those are what determine the audience, so the token stays valid for the
385
+ upstream behind the bridge.
386
+ - **Queue at the concurrency cap**: at `maxSessions`, a new session waits briefly
387
+ (`MCP_QUEUE_TIMEOUT_MS`, default 10s) for a slot before being refused, so a burst is absorbed
388
+ rather than failed outright.
389
+
390
+ ### Hardening
391
+
392
+ There is nothing to fix relative to a previous release, but the following were found and fixed
393
+ by running this against a live 15-server setup before publishing. They are recorded because each
394
+ one is a trap for anyone building something similar.
395
+
396
+ - Tear down the whole process tree on Windows. Bare commands (`npx`, `uvx`, and `.cmd` shims)
397
+ spawn under a `cmd.exe` wrapper, so `child.kill()` signalled only the wrapper and left the
398
+ real server orphaned on every recycle, idle reap, session close and shutdown. The orphans
399
+ also held their tokens, so a recycle could leave two servers competing.
400
+ - Hold keep-alive sockets open past Node's 5 second default. A client busy across that window
401
+ never processes the server's `FIN`, so the socket stayed pooled and the next request failed
402
+ with `ECONNRESET`.
403
+ - Never cache an empty auth-command result. A token tool can exit 0 while printing nothing;
404
+ caching that served an empty credential for the full refresh window, so every upstream
405
+ request returned 401 until it expired.
406
+ - Mint one token per key. On startup every server requested its token at once and each spawned
407
+ its own token process, which is what made those concurrent calls return nothing.
408
+ - Run auth commands through a shell so quoted arguments survive. Constructing `cmd /c <command>`
409
+ by hand re-escaped embedded quotes and broke any command quoting a path with spaces.
410
+ - Wire VS Code over Streamable HTTP instead of SSE, the transport behind `-32001` reconnect
411
+ failures.
412
+ - Rewrite an existing entry in place when a host already lists a bridged server under a
413
+ different key, instead of adding a duplicate registration of the same server.
414
+ - Report warnings in the `doctor` summary rather than printing `all good.` beneath them.
415
+ - Resolve a server's relative command and arguments against a working directory it carries in
416
+ its own config, instead of depending on how the bridge was launched. A server defined as
417
+ `node .vscode/mcp/<name>/index.js` failed with `MODULE_NOT_FOUND` whenever the bridge started
418
+ without the matching `--cwd`.
419
+ - Report a server that starts and then exits non-zero, including the tail of its stderr.
420
+ `lastError` was only set when the process failed to spawn, so a server crashing on startup
421
+ left `/api/status` and `doctor` looking healthy while every session died.
422
+ - Give `initialize` its own timeout. Sharing the 30s call timeout meant a server whose first run
423
+ downloads itself returned `-32001 upstream timeout` and appeared broken.
424
+ - Reap idle sessions by default (`MCP_IDLE_TIMEOUT_MS`, 30 minutes). Clients are not obliged to
425
+ send `DELETE` and some never do; one observed client opened a session per run and left 241
426
+ server processes running within an hour. Reaping is safe because a returning client's session
427
+ is transparently re-established.
428
+ - Do not let a killed child's late `exit` event delete the session that replaced it. `taskkill /F
429
+ /T` takes about a second on Windows, so a client returning inside that window was resumed onto
430
+ a fresh server and then silently dropped when the old exit finally arrived — the failure that
431
+ session resume exists to prevent. The mapping is now torn down only if it still points at the
432
+ child that exited.
433
+ - Do not reap a session that has a request in flight, and measure the idle clock from when a
434
+ request finished rather than when it started. A call slower than the idle timeout previously
435
+ had its server killed underneath it, leaving the caller waiting for a reply that could never
436
+ arrive.
437
+ - Report a server whose relative script path cannot resolve. `doctor` and the dashboard health
438
+ page now flag a stdio server whose arguments name a path that does not exist from the directory
439
+ it would run in, and warn when a relative path is used with no `cwd` at all. This failure was
440
+ otherwise silent: the server was listed as configured and only died when a client first called
441
+ it. Package specs passed to a runner (`npx @scope/pkg`) are not mistaken for paths.
442
+ - Strip hop-by-hop headers from proxied responses (RFC 9110 §7.6.1). A header the upstream named
443
+ in its own `Connection` header, or a `trailer` it announced, is connection-scoped and was being
444
+ relayed to the client, where it is meaningless or misleading.
445
+ - Do not report a teardown the bridge initiated as a crash. `taskkill /F` exits non-zero, so
446
+ every recycle, idle reap and session close was recorded as a failure, burying real ones.
447
+
448
+ [2.0.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v2.0.0
449
+ [1.3.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v1.3.0
450
+ [1.2.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v1.2.0
451
+ [1.1.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v1.1.0
452
+ [1.0.0]: https://github.com/girishkvs/mcp-pacemaker/releases/tag/v1.0.0