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.
- package/CHANGELOG.md +452 -0
- package/CONTRIBUTING.md +222 -0
- package/LICENSE +21 -0
- package/README.md +621 -0
- package/SECURITY.md +120 -0
- package/THIRD_PARTY_NOTICES.txt +366 -0
- package/autostart/windows/launcher.vbs +11 -0
- package/autostart/windows/register-task.ps1 +63 -0
- package/bin/cli.mjs +880 -0
- package/bin/config-checks.mjs +62 -0
- package/bin/logs.mjs +465 -0
- package/bin/mcp-bridge.mjs +2162 -0
- package/bin/pooling-batch-scheduler.mjs +129 -0
- package/bin/pooling-batches.mjs +270 -0
- package/bin/pooling-config.mjs +577 -0
- package/bin/pooling-editor.mjs +128 -0
- package/bin/pooling-errors.mjs +23 -0
- package/bin/pooling-execution.mjs +104 -0
- package/bin/pooling-files.mjs +541 -0
- package/bin/pooling-trace.mjs +218 -0
- package/bin/pooling-writer.mjs +242 -0
- package/bin/prewarm.mjs +233 -0
- package/bin/server-metrics.mjs +69 -0
- package/bin/service-control.mjs +124 -0
- package/bin/shared-sessions.mjs +1083 -0
- package/bin/top.mjs +115 -0
- package/bin/update-check.mjs +153 -0
- package/bin/windows/PoolingSecurityHelper.build.json +42 -0
- package/bin/windows/PoolingSecurityHelper.exe +0 -0
- package/bin/windows/src/AssemblyInfo.cs +11 -0
- package/bin/windows/src/PoolingNativeFiles.cs +678 -0
- package/bin/windows/src/PoolingSecurityHelper.cs +178 -0
- package/bin/windows/src/PoolingSecurityReader.cs +50 -0
- package/docs/shared-sessions.md +181 -0
- package/examples/servers.example.json +31 -0
- package/package.json +77 -0
- package/supervisor/bridge-child.mjs +14 -0
- package/supervisor/supervise.mjs +172 -0
- package/supervisor/supervise.ps1 +23 -0
- package/supervisor/supervise.sh +11 -0
- package/tools/windows-security-helper/README.md +238 -0
- package/tools/windows-security-helper/build.ps1 +262 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +366 -0
- package/ui/dist/assets/index-CaLPg0c-.css +1 -0
- package/ui/dist/assets/index-CgPruWYi.js +9 -0
- package/ui/dist/index.html +14 -0
- 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
|