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/bin/top.mjs ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * mcp-pacemaker top — live terminal dashboard (Ink). Same data as the web /ui.
4
+ * Reads the bridge port from ~/.mcp-pacemaker/state.json (or --port) and the admin
5
+ * nonce from ~/.mcp-pacemaker/admin.nonce for the Recycle keybind. No JSX (uses createElement).
6
+ */
7
+ import { createElement as h, useState, useEffect } from 'react';
8
+ import { render, Box, Text, useApp, useInput } from 'ink';
9
+ import http from 'node:http';
10
+ import { readFileSync } from 'node:fs';
11
+ import { resolve } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+
14
+ const HOME = resolve(homedir(), '.mcp-pacemaker');
15
+
16
+ export function fetchSnapshot(port) {
17
+ return new Promise((res) => {
18
+ const r = http.get({ host: '127.0.0.1', port, path: '/api/status', timeout: 2000 }, (resp) => {
19
+ let d = ''; resp.on('data', (c) => (d += c)); resp.on('end', () => { try { res(JSON.parse(d)); } catch { res(null); } });
20
+ });
21
+ r.on('error', () => res(null));
22
+ r.on('timeout', () => { r.destroy(); res(null); });
23
+ });
24
+ }
25
+
26
+ export function recycleServer(port, name, nonce) {
27
+ return new Promise((res) => {
28
+ const r = http.request({ host: '127.0.0.1', port, path: `/admin/recycle/${encodeURIComponent(name)}`, method: 'POST', headers: { 'x-mcp-nonce': nonce }, timeout: 3000 }, (resp) => {
29
+ let d = ''; resp.on('data', (c) => (d += c)); resp.on('end', () => { let body = null; try { body = JSON.parse(d || 'null'); } catch { /* noop */ } res({ status: resp.statusCode, body }); });
30
+ });
31
+ r.on('error', () => res(null));
32
+ r.on('timeout', () => { r.destroy(); res(null); });
33
+ r.end();
34
+ });
35
+ }
36
+
37
+ const readNonce = () => { try { return readFileSync(resolve(HOME, 'admin.nonce'), 'utf8').trim(); } catch { return ''; } };
38
+ // Pure: pick the bridge port from a parsed state object (list shape wins, legacy {port} fallback).
39
+ export function portFromState(state) {
40
+ if (state && Array.isArray(state.hosts)) return state.hosts[0]?.port || 8791;
41
+ return (state && state.port) || 8791;
42
+ }
43
+ function resolvePort(port) {
44
+ if (port) return port;
45
+ try { return portFromState(JSON.parse(readFileSync(resolve(HOME, 'state.json'), 'utf8'))); } catch { return 8791; }
46
+ }
47
+
48
+ const pad = (s, n) => { s = String(s ?? ''); return s.length >= n ? s.slice(0, n - 1) + ' ' : s.padEnd(n); };
49
+ const fmtTok = (sec) => (sec == null ? '-' : sec <= 0 ? 'exp' : `${Math.round(sec / 60)}m`);
50
+ // 'unknown' has to be visibly distinct from 'ok': the point of the health column is that a
51
+ // server nobody has called is not claimed to be working.
52
+ const HEALTH = { ok: { text: 'ok', color: 'green' }, failing: { text: 'FAIL', color: 'red' }, unknown: { text: '?', color: 'gray' } };
53
+ const healthCell = (s) => {
54
+ const st = s.health?.state ?? 'unknown';
55
+ const h = HEALTH[st] ?? HEALTH.unknown;
56
+ return { text: st === 'failing' && s.health.consecutiveFailures > 1 ? `FAIL×${s.health.consecutiveFailures}` : h.text, color: h.color };
57
+ };
58
+
59
+ function App({ port }) {
60
+ const { exit } = useApp();
61
+ const [snap, setSnap] = useState(null);
62
+ const [sel, setSel] = useState(0);
63
+ const [msg, setMsg] = useState('');
64
+
65
+ useEffect(() => {
66
+ let alive = true;
67
+ const tick = async () => { const s = await fetchSnapshot(port); if (alive) setSnap(s); };
68
+ tick();
69
+ const t = setInterval(tick, 1500);
70
+ return () => { alive = false; clearInterval(t); };
71
+ }, [port]);
72
+
73
+ const servers = snap?.servers ?? [];
74
+
75
+ useInput((input, key) => {
76
+ if (input === 'q' || (key.ctrl && input === 'c')) return exit();
77
+ if (key.upArrow) setSel((i) => Math.max(0, i - 1));
78
+ else if (key.downArrow) setSel((i) => Math.min(Math.max(0, servers.length - 1), i + 1));
79
+ else if (input === 'r') {
80
+ const s = servers[sel];
81
+ if (!s) return;
82
+ if (s.type === 'http') { setMsg('http servers have no child to recycle'); return; }
83
+ setMsg(`recycling ${s.name}…`);
84
+ recycleServer(port, s.name, readNonce()).then((r) =>
85
+ setMsg(r && r.status === 200 ? `recycled ${s.name} (${r.body?.recycled ?? 0})` : `recycle failed (${r?.status ?? 'no nonce/bridge'})`),
86
+ );
87
+ }
88
+ });
89
+
90
+ return h(
91
+ Box,
92
+ { flexDirection: 'column', padding: 1 },
93
+ h(
94
+ Box,
95
+ { justifyContent: 'space-between' },
96
+ h(Text, { bold: true, color: 'cyan' }, '🫀 mcp-pacemaker top'),
97
+ h(Text, { color: snap ? 'green' : 'red' }, snap ? `● :${snap.port} · up ${snap.uptimeSec}s · v${snap.version}` : '● connecting…'),
98
+ ),
99
+ h(Text, { dimColor: true }, pad('SERVER', 18) + pad('TYPE', 7) + pad('HEALTH', 8) + pad('SESS', 6) + pad('WARM', 6) + pad('REQ', 7) + pad('PID', 10) + pad('TOKEN', 8) + 'ERROR / CLIENTS'),
100
+ servers.length === 0
101
+ ? h(Text, { dimColor: true }, snap ? 'no servers configured' : 'connecting…')
102
+ : servers.map((s, i) =>
103
+ h(
104
+ Text,
105
+ { key: s.name, inverse: i === sel, color: s.health?.state === 'failing' ? 'red' : undefined },
106
+ pad(s.name, 18) + pad(s.type, 7) + pad(healthCell(s).text, 8) + pad(s.sessions, 6) + pad(s.sharing === 'pool' ? `${s.warm}/${s.minWarm ?? 1}` : '-', 6) + pad(s.requests, 7) + pad(s.pids.join(',') || '-', 10) + pad(fmtTok(s.tokenExpiresIn), 8) + (s.lastError ? String(s.lastError).slice(0, 24) : (s.clients && s.clients.length ? '\u21c4 ' + s.clients.join(',') : '')),
107
+ ),
108
+ ),
109
+ h(Text, { dimColor: true }, `↑↓ select · r recycle · q quit ${msg}`),
110
+ );
111
+ }
112
+
113
+ export function runTop(port) {
114
+ render(h(App, { port: resolvePort(port) }));
115
+ }
@@ -0,0 +1,153 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync, mkdtempSync, realpathSync, rmSync } from 'node:fs';
3
+ import { dirname, join, resolve, delimiter } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { promisify } from 'node:util';
6
+ import semver from 'semver';
7
+
8
+ const runFile = promisify(execFile);
9
+
10
+ export function updateChannel(pkg) {
11
+ if (typeof pkg.name !== 'string' ||
12
+ !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(pkg.name)) {
13
+ throw new Error('Invalid installed package name.');
14
+ }
15
+ if (typeof pkg.version !== 'string' ||
16
+ !semver.valid(pkg.version) ||
17
+ semver.prerelease(pkg.version)) {
18
+ throw new Error('The installed CLI must have a stable SemVer version.');
19
+ }
20
+ const major = semver.major(pkg.version);
21
+ if (major !== 1 &&
22
+ major !== 2) {
23
+ throw new Error(`No update channel is defined for major ${major}.`);
24
+ }
25
+ return major === 1 ? 'legacy' : 'latest';
26
+ }
27
+
28
+ export function channelTarget(pkg, tags) {
29
+ const channel = updateChannel(pkg);
30
+ const target = tags?.[channel];
31
+ if (typeof target !== 'string' ||
32
+ semver.valid(target) !== target ||
33
+ semver.prerelease(target) ||
34
+ semver.major(target) !== semver.major(pkg.version)) {
35
+ throw new Error(`Channel "${channel}" has no valid stable same-major target. No fallback selected.`);
36
+ }
37
+ return target;
38
+ }
39
+
40
+ export function selectUpdate(pkg, tags, release) {
41
+ const channel = updateChannel(pkg);
42
+ const target = channelTarget(pkg, tags);
43
+ if (!release ||
44
+ Array.isArray(release) ||
45
+ release.name !== pkg.name ||
46
+ release.version !== target) {
47
+ throw new Error(`The exact "${channel}" target could not be verified.`);
48
+ }
49
+ if (release.deprecated !== undefined &&
50
+ release.deprecated !== '') {
51
+ throw new Error(`The "${channel}" target is deprecated. No fallback selected.`);
52
+ }
53
+ const order = semver.compare(target, pkg.version);
54
+ return {
55
+ name: pkg.name,
56
+ current: pkg.version,
57
+ channel,
58
+ target,
59
+ status: order > 0 ? 'update-available' : order < 0 ? 'installed-ahead' : 'current',
60
+ updateAvailable: order > 0,
61
+ rollbackRequired: order < 0,
62
+ };
63
+ }
64
+
65
+ export function validateRegistry(registry) {
66
+ if (registry === undefined) return undefined;
67
+ const url = new URL(registry);
68
+ if (!['https:', 'http:'].includes(url.protocol) ||
69
+ url.username ||
70
+ url.password ||
71
+ url.search ||
72
+ url.hash) {
73
+ throw new Error('Registry must be an HTTP(S) URL without credentials, query, or fragment. Use npm configuration for authentication.');
74
+ }
75
+ return url.href;
76
+ }
77
+
78
+ function npmCliPath() {
79
+ const candidates = [];
80
+ if (process.env.npm_execpath) candidates.push(process.env.npm_execpath);
81
+ candidates.push(join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
82
+ for (const directory of (process.env.PATH || '').split(delimiter)) {
83
+ if (!directory) continue;
84
+ candidates.push(join(directory, 'node_modules', 'npm', 'bin', 'npm-cli.js'));
85
+ const launcher = join(directory, 'npm');
86
+ if (existsSync(launcher)) candidates.push(realpathSync(launcher));
87
+ }
88
+ const path = candidates.find((candidate) => candidate.endsWith('npm-cli.js') && existsSync(candidate));
89
+ if (!path) throw new Error('npm CLI not found. Install npm with Node.js or run through npm; no registry fallback was attempted.');
90
+ return resolve(path);
91
+ }
92
+
93
+ export async function readNpmMetadata(spec, field, { registry, cache, run = runFile } = {}) {
94
+ const args = [npmCliPath(), 'view', spec];
95
+ if (field) args.push(field);
96
+ args.push('--json', '--prefer-online', '--ignore-scripts', '--fetch-retries=0', '--fetch-timeout=8000', '--cache', cache);
97
+ if (registry !== undefined) args.push('--registry', validateRegistry(registry));
98
+ let stdout;
99
+ try {
100
+ ({ stdout } = await run(process.execPath, args, {
101
+ encoding: 'utf8', timeout: 12000, maxBuffer: 4 * 1024 * 1024, windowsHide: true,
102
+ }));
103
+ } catch {
104
+ // npm's diagnostics can contain private registry URLs and authentication details.
105
+ throw new Error('Registry metadata unavailable (offline, denied, missing, or npm failed). Check your approved npm configuration; no fallback selected.');
106
+ }
107
+ try {
108
+ return JSON.parse(stdout);
109
+ } catch {
110
+ throw new Error('npm returned invalid metadata. No fallback selected.');
111
+ }
112
+ }
113
+
114
+ export async function checkForUpdate(pkg, { registry, read = readNpmMetadata } = {}) {
115
+ updateChannel(pkg);
116
+ registry = validateRegistry(registry);
117
+ // An empty cache prevents an offline/stale response from certifying channel state.
118
+ const cache = mkdtempSync(join(tmpdir(), 'mcp-update-'));
119
+ try {
120
+ const options = { registry, cache };
121
+ const tags = await read(pkg.name, 'dist-tags', options);
122
+ const target = channelTarget(pkg, tags);
123
+ const release = await read(`${pkg.name}@${target}`, undefined, options);
124
+ return selectUpdate(pkg, tags, release);
125
+ } finally {
126
+ rmSync(cache, { recursive: true, force: true });
127
+ }
128
+ }
129
+
130
+ export function updateGuidance(result, { registry, platform = process.platform } = {}) {
131
+ const quote = (value) => platform === 'win32'
132
+ ? `'${value.replaceAll("'", "''")}'`
133
+ : `'${value.replaceAll("'", "'\\''")}'`;
134
+ const registryOption = registry === undefined ? '' : ` --registry ${quote(validateRegistry(registry))}`;
135
+ const action = result.rollbackRequired
136
+ ? 'Installed CLI is ahead of its channel. This is a deliberate rollback, NOT a normal update.'
137
+ : result.updateAvailable ? 'A same-major CLI update is available.' : 'Installed CLI matches its channel.';
138
+ return [
139
+ `${result.name}: installed CLI ${result.current}; ${result.channel} -> ${result.target}. ${action}`,
140
+ 'Guidance only: no package, service, autostart, or host configuration was changed.',
141
+ 'Before replacement: identify every service using the installation root, its supervisor, autostart, port and config directory.',
142
+ 'For a 2.x downgrade, settle/cancel batches and recover interrupted transactions with 2.x first. Keep protected backups; never delete transaction files.',
143
+ 'Use the matching installation CLI to stop the selected managed service: mcp-pacemaker stop --port <port>.',
144
+ 'A successful managed stop holds its supervisor/autostart until an explicit start. Older/unmanaged supervisors require separately verified OS-service shutdown and disabled autostart.',
145
+ 'Do not overwrite ANY root still used by a bridge or supervisor: workers, helpers and UI load files later.',
146
+ 'Only after shutdown is verified, and after a deliberate rollback decision if installed-ahead:',
147
+ ` npm install --global ${result.name}@${result.target}${registryOption}`,
148
+ 'Use the same approved npm registry configuration. Prefer a separate durable version root; one global prefix replaces the shared CLI bins.',
149
+ 'Re-register the intended durable autostart path if it changed, start deliberately, and verify backend version and a new instance ID. Refresh dashboard tabs and check active config, permissions and tool calls.',
150
+ 'Moving to another major is a separate migration; no cross-major target was selected.',
151
+ 'Side-by-side services need separate durable roots, config directories, HOME/host state and ports. import/init --config is an input host config, not a runtime profile.',
152
+ ].join('\n');
153
+ }
@@ -0,0 +1,42 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "binary": "PoolingSecurityHelper.exe",
4
+ "binarySha256": "db026df3593cf4b9467aa1a6a445f2dd4f3290198becb5bbdbfb484ce6b0ccc0",
5
+ "inputs": {
6
+ "targetFramework": ".NETFramework,Version=v4.6.2",
7
+ "platform": "AnyCPU",
8
+ "compilerVersion": "5.9.0-1.26357.3 (35d9211b841e7613c1d2f8f5af6d628ace696c4c)",
9
+ "compilerSha256": "3aafb7b9c54fa31a7092af35148971ee616965e7f9a0b80fb7fdc4bdd1d1a555",
10
+ "compilerArguments": [
11
+ "/nologo",
12
+ "/noconfig",
13
+ "/nostdlib+",
14
+ "/target:exe",
15
+ "/platform:anycpu",
16
+ "/langversion:7.3",
17
+ "/optimize+",
18
+ "/debug-",
19
+ "/deterministic+",
20
+ "/checked-",
21
+ "/unsafe-",
22
+ "/warn:4",
23
+ "/warnaserror+",
24
+ "/utf8output"
25
+ ],
26
+ "pathMap": "/_/mcp-pacemaker/windows-security-helper",
27
+ "sourceEncoding": "UTF-8 without BOM, LF line endings",
28
+ "sourceSha256": {
29
+ "AssemblyInfo.cs": "e81e107352fbcb82818d8265dfcdf392eac8d25df37e6d65ca7f02d1e63a6c9a",
30
+ "PoolingSecurityReader.cs": "91620ba5e0b3c6fcc673319f59c96466115a6be74fd8b93808f08f07087b7038",
31
+ "PoolingSecurityHelper.cs": "1d14768fbf602368f20302bbe382e6d195e12db6008edb3aa95be0de88d75935",
32
+ "PoolingNativeFiles.cs": "1234fd12fb41910981385010a071358476b6a8a4c42827714f3f270f05f23c1d"
33
+ },
34
+ "buildScriptSha256": "8413c3ea3239a474379103cbf456f52a0a3bc75fcdb23b5f769474abe4a1eea0",
35
+ "referenceSha256": {
36
+ "mscorlib.dll": "7118fe658567606fa0538f7b5a2071e4606756c07e9062e1e5b24fa49e97d1f6",
37
+ "System.dll": "b76822f9c08aff597120e3167b3e0fbda836d6bc6f14bc4bc2eeffab5b2c6125",
38
+ "System.Core.dll": "774e965a6c25891a96d6d3b838ae10226ed9963a73b09e4be7c2ec275ffb5886",
39
+ "System.Web.Extensions.dll": "7a32bf3263aebed93c3a0847294aebdde15f7427541ed223a4cc4eb93c36d556"
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,11 @@
1
+ using System.Reflection;
2
+ using System.Runtime.InteropServices;
3
+ using System.Runtime.Versioning;
4
+
5
+ [assembly: AssemblyTitle("mcp-pacemaker Windows security helper")]
6
+ [assembly: AssemblyDescription("Inspects and preserves configuration file security.")]
7
+ [assembly: AssemblyProduct("mcp-pacemaker")]
8
+ [assembly: AssemblyVersion("1.0.0.0")]
9
+ [assembly: AssemblyFileVersion("1.0.0.0")]
10
+ [assembly: ComVisible(false)]
11
+ [assembly: TargetFramework(".NETFramework,Version=v4.6.2")]