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.
- package/CHANGELOG.md +424 -0
- package/CONTRIBUTING.md +156 -0
- package/LICENSE +21 -0
- package/README.md +620 -0
- package/SECURITY.md +110 -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 +2113 -0
- package/bin/pooling-config.mjs +553 -0
- package/bin/pooling-execution.mjs +91 -0
- package/bin/pooling-writer.mjs +169 -0
- package/bin/prewarm.mjs +152 -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 +39 -0
- package/bin/windows/PoolingSecurityHelper.exe +0 -0
- package/bin/windows/src/AssemblyInfo.cs +11 -0
- package/bin/windows/src/PoolingSecurityHelper.cs +165 -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 +95 -0
- package/tools/windows-security-helper/build.ps1 +262 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +366 -0
- package/ui/dist/assets/index-CIy7uxfd.css +1 -0
- package/ui/dist/assets/index-CtaaQsB9.js +8 -0
- package/ui/dist/index.html +14 -0
- 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
|
package/CONTRIBUTING.md
ADDED
|
@@ -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.
|