@anma-labs/mcpgaze 1.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/dist/index.js ADDED
@@ -0,0 +1,2148 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { spawn as spawn3 } from "child_process";
5
+ import { existsSync } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname as dirname3, join } from "path";
8
+
9
+ // src/logger.ts
10
+ import { createWriteStream, mkdirSync } from "fs";
11
+ import { dirname } from "path";
12
+
13
+ // src/jsonrpc.ts
14
+ function hasId(msg) {
15
+ return msg.id !== void 0 && msg.id !== null;
16
+ }
17
+ function classify(msg) {
18
+ const id = hasId(msg);
19
+ if (msg.method !== void 0 && id) return "request";
20
+ if (msg.method !== void 0 && !id) return "notification";
21
+ if (msg.method === void 0 && id && msg.error !== void 0) return "error";
22
+ if (msg.method === void 0 && id && msg.result !== void 0) return "response";
23
+ return "unknown";
24
+ }
25
+
26
+ // src/colors.ts
27
+ var enabled = !process.env.NO_COLOR && (Boolean(process.stderr.isTTY) || Boolean(process.stdout.isTTY));
28
+ function paint(code) {
29
+ return (s) => enabled ? `\x1B[${code}m${s}\x1B[0m` : s;
30
+ }
31
+ var color = {
32
+ red: paint(31),
33
+ green: paint(32),
34
+ yellow: paint(33),
35
+ blue: paint(34),
36
+ cyan: paint(36),
37
+ gray: paint(90),
38
+ dim: paint(2),
39
+ bold: paint(1)
40
+ };
41
+
42
+ // src/redact.ts
43
+ var MASK = "***REDACTED***";
44
+ var SECRET_KEY = /(pass(word|wd)?|secret|api[-_]?key|access[-_]?key|token|authorization|auth|bearer|cookie|session|credential|client[-_]?secret|private[-_]?key)/i;
45
+ var VALUE_PATTERNS = [
46
+ // Provider API keys: sk-... / sk-ant-... / AKIA... / ghp_... / xoxb-...
47
+ [/\b(sk-[a-z]+-)?[A-Za-z0-9_-]{16,}\b(?=)/g, ""]
48
+ // placeholder; real patterns below
49
+ ];
50
+ VALUE_PATTERNS.length = 0;
51
+ VALUE_PATTERNS.push(
52
+ // user:pass@host inside a DSN/URL — keep the user + host, drop the password.
53
+ [/([a-z][a-z0-9+.-]*:\/\/[^\s:@/]+:)[^\s@/]+(@)/gi, `$1${MASK}$2`],
54
+ // sk-ant-…, sk-…, and similar prefixed provider keys.
55
+ [/\bsk-[A-Za-z0-9-]{8,}\b/g, MASK],
56
+ // AWS-style access key ids and their secret siblings.
57
+ [/\bAKIA[0-9A-Z]{8,}\b/g, MASK],
58
+ [/\b(?:ASIA|AKIA|AGPA|AIDA|AROA|ANPA|ANVA)[0-9A-Z_-]{8,}\b/g, MASK],
59
+ // GitHub / Slack / generic prefixed tokens.
60
+ [/\b(?:gh[pousr]|xox[baprs])[-_][A-Za-z0-9-]{10,}\b/g, MASK],
61
+ // Bearer <token>.
62
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, `Bearer ${MASK}`],
63
+ // JWT-ish three-segment base64url blobs.
64
+ [/\beyJ[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/g, MASK]
65
+ );
66
+ var MAX_DEPTH = 200;
67
+ function redactText(s) {
68
+ if (typeof s !== "string" || s.length === 0) return s;
69
+ try {
70
+ let out = s;
71
+ for (const [re, repl] of VALUE_PATTERNS) out = out.replace(re, repl);
72
+ return out;
73
+ } catch {
74
+ return s;
75
+ }
76
+ }
77
+ function redactValue(value, depth = 0) {
78
+ if (depth >= MAX_DEPTH) return value;
79
+ if (value === null) return null;
80
+ if (typeof value === "string") return redactText(value);
81
+ if (typeof value !== "object") return value;
82
+ if (Array.isArray(value)) return value.map((v) => redactValue(v, depth + 1));
83
+ const obj = value;
84
+ const out = {};
85
+ for (const k of Object.keys(obj)) {
86
+ out[k] = SECRET_KEY.test(k) ? MASK : redactValue(obj[k], depth + 1);
87
+ }
88
+ return out;
89
+ }
90
+ function redactRawJson(raw) {
91
+ if (typeof raw !== "string" || raw.length === 0) return raw;
92
+ try {
93
+ const parsed = JSON.parse(raw);
94
+ return JSON.stringify(redactValue(parsed));
95
+ } catch {
96
+ return redactText(raw);
97
+ }
98
+ }
99
+
100
+ // src/logger.ts
101
+ var Logger = class {
102
+ file;
103
+ pretty;
104
+ out;
105
+ onEvent;
106
+ redact;
107
+ constructor(opts) {
108
+ this.pretty = Boolean(opts.pretty);
109
+ this.out = opts.prettyStream ?? process.stderr;
110
+ this.onEvent = opts.onEvent;
111
+ this.redact = Boolean(opts.redact);
112
+ if (opts.jsonlPath) {
113
+ mkdirSync(dirname(opts.jsonlPath), { recursive: true });
114
+ this.file = createWriteStream(opts.jsonlPath, { flags: "a", mode: 384 });
115
+ this.file.on("error", () => {
116
+ });
117
+ }
118
+ }
119
+ write(ev) {
120
+ this.file?.write(JSON.stringify(ev) + "\n");
121
+ this.onEvent?.(ev);
122
+ }
123
+ message(f, latencyMs) {
124
+ const kind2 = f.msg ? classify(f.msg) : "unparsed";
125
+ const ev = {
126
+ t: (/* @__PURE__ */ new Date()).toISOString(),
127
+ type: "message",
128
+ dir: f.direction,
129
+ kind: kind2,
130
+ id: f.msg?.id ?? null,
131
+ method: f.msg?.method ?? null,
132
+ latencyMs: latencyMs ?? null,
133
+ parseError: f.parseError ?? null,
134
+ // redact masks credential-shaped params at rest; the wire already carried
135
+ // f.raw byte-exact, so this only touches the on-disk observation.
136
+ raw: this.redact ? redactRawJson(f.raw) : f.raw
137
+ };
138
+ this.write(ev);
139
+ if (this.pretty) this.renderMessage(ev, kind2);
140
+ }
141
+ serverStderr(text) {
142
+ const t = this.redact ? redactText(text) : text;
143
+ this.write({ t: (/* @__PURE__ */ new Date()).toISOString(), type: "server_stderr", text: t });
144
+ }
145
+ note(code, detail) {
146
+ this.write({ t: (/* @__PURE__ */ new Date()).toISOString(), type: "note", code, detail });
147
+ if (this.pretty) this.out.write(color.dim(`\u2022 ${code}: ${detail}
148
+ `));
149
+ }
150
+ renderMessage(ev, kind2) {
151
+ const arrow = ev.dir === "c2s" ? color.cyan("\u2192 to server ") : color.green("\u2190 to client ");
152
+ const tag = kind2 === "error" ? color.red("ERROR") : kind2 === "request" ? color.bold("req") : kind2 === "response" ? "res" : kind2 === "notification" ? color.gray("notif") : color.yellow(kind2);
153
+ const id = ev.id !== null ? color.dim(`#${String(ev.id)} `) : "";
154
+ const method = ev.method ? String(ev.method) : "";
155
+ const lat = typeof ev.latencyMs === "number" ? color.dim(` (${ev.latencyMs.toFixed(1)}ms)`) : "";
156
+ const pe = ev.parseError ? color.red(` !parse: ${String(ev.parseError)}`) : "";
157
+ this.out.write(`${arrow}${tag} ${id}${method}${lat}${pe}
158
+ `);
159
+ }
160
+ close() {
161
+ this.file?.end();
162
+ }
163
+ };
164
+
165
+ // src/proxy.ts
166
+ import { spawn } from "child_process";
167
+ import { performance } from "perf_hooks";
168
+
169
+ // src/framer.ts
170
+ import { StringDecoder } from "string_decoder";
171
+ var MAX_LINE = 64 * 1024 * 1024;
172
+ var LineFramer = class {
173
+ constructor(onMessage, direction) {
174
+ this.onMessage = onMessage;
175
+ this.direction = direction;
176
+ }
177
+ onMessage;
178
+ direction;
179
+ buf = "";
180
+ overflow = false;
181
+ // discarding an over-long, newline-free line until the next "\n"
182
+ decoder = new StringDecoder("utf8");
183
+ push(chunk) {
184
+ const text = typeof chunk === "string" ? chunk : this.decoder.write(chunk);
185
+ if (this.overflow) {
186
+ const nl2 = text.indexOf("\n");
187
+ if (nl2 < 0) return;
188
+ this.overflow = false;
189
+ this.buf = text.slice(nl2 + 1);
190
+ } else {
191
+ this.buf += text;
192
+ }
193
+ let nl;
194
+ while ((nl = this.buf.indexOf("\n")) >= 0) {
195
+ const line = this.buf.slice(0, nl);
196
+ this.buf = this.buf.slice(nl + 1);
197
+ const trimmed = line.trim();
198
+ if (trimmed === "") continue;
199
+ this.emit(trimmed);
200
+ }
201
+ if (this.buf.length > MAX_LINE) {
202
+ this.overflow = true;
203
+ this.buf = "";
204
+ }
205
+ }
206
+ emit(raw) {
207
+ let msg = null;
208
+ let parseError;
209
+ try {
210
+ msg = JSON.parse(raw);
211
+ } catch (e) {
212
+ parseError = e.message;
213
+ }
214
+ this.onMessage({ direction: this.direction, raw, msg, parseError });
215
+ }
216
+ };
217
+
218
+ // src/proxy.ts
219
+ var Correlator = class {
220
+ constructor(logger, onPair) {
221
+ this.logger = logger;
222
+ this.onPair = onPair;
223
+ }
224
+ logger;
225
+ onPair;
226
+ pending = /* @__PURE__ */ new Map();
227
+ onClientToServer(f) {
228
+ this.logger.message(f);
229
+ if (f.msg && classify(f.msg) === "request" && hasId(f.msg)) {
230
+ this.pending.set(String(f.msg.id), {
231
+ method: f.msg.method ?? "",
232
+ params: f.msg.params,
233
+ at: performance.now()
234
+ });
235
+ }
236
+ }
237
+ onServerToClient(f) {
238
+ let latency;
239
+ if (f.msg && hasId(f.msg) && f.msg.method === void 0) {
240
+ const key = String(f.msg.id);
241
+ const p = this.pending.get(key);
242
+ if (p) {
243
+ latency = performance.now() - p.at;
244
+ this.pending.delete(key);
245
+ this.onPair?.(
246
+ { method: p.method, params: p.params },
247
+ { result: f.msg.result, error: f.msg.error }
248
+ );
249
+ }
250
+ }
251
+ this.logger.message(f, latency);
252
+ }
253
+ reportOrphans() {
254
+ for (const [id, p] of this.pending) {
255
+ this.logger.note(
256
+ "orphan-request",
257
+ `id=${id} method=${p.method} never received a response`
258
+ );
259
+ }
260
+ }
261
+ };
262
+ function runProxy(opts) {
263
+ return new Promise((resolve) => {
264
+ const child = spawn(opts.command, opts.args, {
265
+ stdio: ["pipe", "pipe", "pipe"]
266
+ });
267
+ const correlator = new Correlator(
268
+ opts.logger,
269
+ opts.onInteraction ? (request, response) => opts.onInteraction({ request, response }) : void 0
270
+ );
271
+ child.stdin.on("error", () => {
272
+ });
273
+ process.stdout.on("error", () => {
274
+ });
275
+ process.stderr.on("error", () => {
276
+ });
277
+ process.stdin.pipe(child.stdin);
278
+ const c2s = new LineFramer((m) => correlator.onClientToServer(m), "c2s");
279
+ process.stdin.on("data", (chunk) => {
280
+ try {
281
+ c2s.push(chunk);
282
+ } catch (e) {
283
+ opts.logger.note("observer-error", `c2s ${e.message}`);
284
+ }
285
+ });
286
+ child.stdout.pipe(process.stdout, { end: false });
287
+ const s2c = new LineFramer((m) => correlator.onServerToClient(m), "s2c");
288
+ child.stdout.on("data", (chunk) => {
289
+ try {
290
+ s2c.push(chunk);
291
+ } catch (e) {
292
+ opts.logger.note("observer-error", `s2c ${e.message}`);
293
+ }
294
+ });
295
+ child.stderr.on("data", (chunk) => {
296
+ if (opts.mirrorStderr !== false) {
297
+ try {
298
+ process.stderr.write(chunk);
299
+ } catch (e) {
300
+ opts.logger.note("observer-error", `stderr-mirror ${e.message}`);
301
+ }
302
+ }
303
+ opts.logger.serverStderr(chunk.toString("utf8"));
304
+ });
305
+ process.stdin.on("end", () => child.stdin.end());
306
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
307
+ process.on(sig, () => child.kill(sig));
308
+ }
309
+ child.on("error", (err) => {
310
+ opts.logger.note("spawn-error", err.message);
311
+ opts.logger.close();
312
+ resolve(127);
313
+ });
314
+ child.on("exit", (code, signal) => {
315
+ correlator.reportOrphans();
316
+ opts.logger.note("server-exit", `code=${String(code)} signal=${String(signal)}`);
317
+ opts.logger.close();
318
+ resolve(code ?? (signal ? 1 : 0));
319
+ });
320
+ });
321
+ }
322
+
323
+ // src/http-proxy.ts
324
+ import { createServer } from "http";
325
+
326
+ // src/sse.ts
327
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
328
+ var SseParser = class {
329
+ constructor(onEvent) {
330
+ this.onEvent = onEvent;
331
+ }
332
+ onEvent;
333
+ buf = "";
334
+ dataLines = [];
335
+ sawCr = false;
336
+ // last line ended on a CR that was the final byte of a chunk
337
+ decoder = new StringDecoder2("utf8");
338
+ push(chunk) {
339
+ this.buf += typeof chunk === "string" ? chunk : this.decoder.write(chunk);
340
+ if (this.sawCr) {
341
+ this.sawCr = false;
342
+ if (this.buf.startsWith("\n")) this.buf = this.buf.slice(1);
343
+ }
344
+ for (; ; ) {
345
+ const lf = this.buf.indexOf("\n");
346
+ const cr = this.buf.indexOf("\r");
347
+ if (lf === -1 && cr === -1) break;
348
+ if (cr !== -1 && (lf === -1 || cr < lf)) {
349
+ if (cr === this.buf.length - 1) {
350
+ const line2 = this.buf.slice(0, cr);
351
+ this.buf = "";
352
+ this.sawCr = true;
353
+ this.handleLine(line2);
354
+ break;
355
+ }
356
+ const line = this.buf.slice(0, cr);
357
+ const skip = this.buf[cr + 1] === "\n" ? 2 : 1;
358
+ this.buf = this.buf.slice(cr + skip);
359
+ this.handleLine(line);
360
+ } else {
361
+ const line = this.buf.slice(0, lf);
362
+ this.buf = this.buf.slice(lf + 1);
363
+ this.handleLine(line);
364
+ }
365
+ }
366
+ }
367
+ handleLine(line) {
368
+ if (line === "") {
369
+ this.dispatch();
370
+ return;
371
+ }
372
+ if (line.startsWith(":")) return;
373
+ const colon = line.indexOf(":");
374
+ const field = colon === -1 ? line : line.slice(0, colon);
375
+ let value = colon === -1 ? "" : line.slice(colon + 1);
376
+ if (value.startsWith(" ")) value = value.slice(1);
377
+ if (field === "data") this.dataLines.push(value);
378
+ }
379
+ dispatch() {
380
+ if (this.dataLines.length === 0) return;
381
+ const data = this.dataLines.join("\n");
382
+ this.dataLines = [];
383
+ if (data.trim() !== "") this.onEvent(data);
384
+ }
385
+ };
386
+
387
+ // src/http-proxy.ts
388
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
389
+ function isAllowedOrigin(origin, allowed) {
390
+ if (!origin) return true;
391
+ if (allowed) return allowed.includes(origin);
392
+ try {
393
+ return LOCAL_HOSTS.has(new URL(origin).hostname);
394
+ } catch {
395
+ return false;
396
+ }
397
+ }
398
+ function matchRemainder(prefix, pathname) {
399
+ if (prefix === "/") return pathname === "/" ? "" : pathname;
400
+ if (pathname === prefix) return "";
401
+ if (pathname.startsWith(prefix + "/")) return pathname.slice(prefix.length);
402
+ return null;
403
+ }
404
+ function resolveRoute(routes, pathname) {
405
+ let best = null;
406
+ for (const r of routes) {
407
+ const remainder = matchRemainder(r.prefix, pathname);
408
+ if (remainder === null) continue;
409
+ if (!best || r.prefix.length > best.prefix.length) {
410
+ best = { upstream: r.upstream, remainder, prefix: r.prefix, forwardCredentials: Boolean(r.forwardCredentials) };
411
+ }
412
+ }
413
+ return best;
414
+ }
415
+ function buildTarget(upstream, remainder, search) {
416
+ const u = new URL(upstream);
417
+ if (remainder) u.pathname = u.pathname.replace(/\/+$/, "") + remainder;
418
+ if (search) u.search = search;
419
+ return u.toString();
420
+ }
421
+ function routeFromUpstream(upstream, forwardCredentials = true) {
422
+ const p = new URL(upstream).pathname;
423
+ return { prefix: p && p !== "" ? p : "/", upstream, forwardCredentials };
424
+ }
425
+ function buildRoutes(upstream, routeSpecs, opts = {}) {
426
+ const credsPrefixes = new Set(opts.credsPrefixes ?? []);
427
+ const routes = [];
428
+ for (const spec of routeSpecs) {
429
+ const eq = spec.indexOf("=");
430
+ if (eq < 0) throw new Error(`bad --route "${spec}" (expected prefix=url)`);
431
+ const prefix = spec.slice(0, eq);
432
+ const url = spec.slice(eq + 1);
433
+ if (!prefix.startsWith("/")) throw new Error(`route prefix must start with "/": "${prefix}"`);
434
+ new URL(url);
435
+ routes.push({ prefix, upstream: url, forwardCredentials: false });
436
+ }
437
+ if (upstream) {
438
+ new URL(upstream);
439
+ routes.push(routeFromUpstream(upstream, true));
440
+ }
441
+ if (routes.length === 0) throw new Error("no upstream configured (use --upstream or --route)");
442
+ if (opts.noForwardCredentials) {
443
+ for (const r of routes) r.forwardCredentials = false;
444
+ } else if (routes.length === 1) {
445
+ routes[0].forwardCredentials = true;
446
+ } else {
447
+ for (const r of routes) {
448
+ r.forwardCredentials = Boolean(opts.forwardCredentials) || credsPrefixes.has(r.prefix);
449
+ }
450
+ }
451
+ return routes;
452
+ }
453
+ function readBody(req) {
454
+ return new Promise((resolve, reject) => {
455
+ const chunks = [];
456
+ req.on("data", (c) => chunks.push(c));
457
+ req.on("end", () => resolve(Buffer.concat(chunks)));
458
+ req.on("error", reject);
459
+ });
460
+ }
461
+ var HOP_BY_HOP = ["host", "connection", "content-length", "accept-encoding"];
462
+ var CREDENTIAL_HEADERS = ["authorization", "cookie"];
463
+ function forwardHeaders(req, forwardCredentials) {
464
+ const h = new Headers();
465
+ for (const [k, v] of Object.entries(req.headers)) {
466
+ const lk = k.toLowerCase();
467
+ if (HOP_BY_HOP.includes(lk)) continue;
468
+ if (!forwardCredentials && CREDENTIAL_HEADERS.includes(lk)) continue;
469
+ if (typeof v === "string") h.set(k, v);
470
+ else if (Array.isArray(v)) h.set(k, v.join(", "));
471
+ }
472
+ h.set("accept-encoding", "identity");
473
+ return h;
474
+ }
475
+ function observe(raw, dir, correlator) {
476
+ let parsed;
477
+ try {
478
+ parsed = JSON.parse(raw);
479
+ } catch (e) {
480
+ const f = { direction: dir, raw, msg: null, parseError: e.message };
481
+ dir === "c2s" ? correlator.onClientToServer(f) : correlator.onServerToClient(f);
482
+ return;
483
+ }
484
+ const list = Array.isArray(parsed) ? parsed : [parsed];
485
+ for (const m of list) {
486
+ const f = { direction: dir, raw, msg: m };
487
+ dir === "c2s" ? correlator.onClientToServer(f) : correlator.onServerToClient(f);
488
+ }
489
+ }
490
+ function runHttpProxy(opts) {
491
+ const correlator = new Correlator(opts.logger);
492
+ const seenSessions = /* @__PURE__ */ new Set();
493
+ const seenTargets = /* @__PURE__ */ new Set();
494
+ const server = createServer((req, res) => {
495
+ res.on("error", () => {
496
+ });
497
+ void handle(req, res).catch((e) => {
498
+ opts.logger.note("proxy-error", e.message);
499
+ if (res.writableEnded) return;
500
+ if (res.headersSent) {
501
+ res.end();
502
+ return;
503
+ }
504
+ res.writeHead(502, { "content-type": "text/plain" });
505
+ res.end("mcpgaze: upstream error");
506
+ });
507
+ });
508
+ async function handle(req, res) {
509
+ if (!isAllowedOrigin(req.headers.origin, opts.allowedOrigins)) {
510
+ opts.logger.note("origin-rejected", String(req.headers.origin));
511
+ res.writeHead(403, { "content-type": "text/plain" });
512
+ res.end("forbidden origin");
513
+ return;
514
+ }
515
+ const reqUrl = new URL(req.url ?? "/", "http://localhost");
516
+ const match = resolveRoute(opts.routes, reqUrl.pathname);
517
+ if (!match) {
518
+ opts.logger.note("no-route", reqUrl.pathname);
519
+ res.writeHead(404, { "content-type": "text/plain" });
520
+ res.end(`mcpgaze: no route for ${reqUrl.pathname}`);
521
+ return;
522
+ }
523
+ const target = buildTarget(match.upstream, match.remainder, reqUrl.search);
524
+ if (!seenTargets.has(target)) {
525
+ seenTargets.add(target);
526
+ opts.logger.note("route", `${match.prefix} \u2192 ${target}`);
527
+ }
528
+ const method = req.method ?? "GET";
529
+ const body = method === "POST" || method === "DELETE" ? await readBody(req) : void 0;
530
+ if (body && body.length) observe(body.toString("utf8"), "c2s", correlator);
531
+ const upstream = await fetch(target, {
532
+ method,
533
+ headers: forwardHeaders(req, match.forwardCredentials),
534
+ body: body && body.length ? body : void 0,
535
+ redirect: "manual"
536
+ });
537
+ const sid = upstream.headers.get("mcp-session-id");
538
+ if (sid && !seenSessions.has(sid)) {
539
+ seenSessions.add(sid);
540
+ opts.logger.note("session", `Mcp-Session-Id=${sid}`);
541
+ }
542
+ const headers = {};
543
+ upstream.headers.forEach((v, k) => {
544
+ if (["transfer-encoding", "content-encoding", "connection", "content-length"].includes(k)) return;
545
+ if (!match.forwardCredentials && ["set-cookie", "mcp-session-id"].includes(k)) return;
546
+ headers[k] = v;
547
+ });
548
+ const ct = upstream.headers.get("content-type") ?? "";
549
+ if (ct.includes("text/event-stream") && upstream.body) {
550
+ res.writeHead(upstream.status, headers);
551
+ const sse = new SseParser((data) => observe(data, "s2c", correlator));
552
+ const reader = upstream.body.getReader();
553
+ try {
554
+ for (; ; ) {
555
+ const { done, value } = await reader.read();
556
+ if (done) break;
557
+ const buf = Buffer.from(value);
558
+ res.write(buf);
559
+ try {
560
+ sse.push(buf);
561
+ } catch (e) {
562
+ opts.logger.note("observer-error", `sse ${e.message}`);
563
+ }
564
+ }
565
+ } catch (e) {
566
+ opts.logger.note("sse-upstream-error", e.message);
567
+ } finally {
568
+ await reader.cancel().catch(() => {
569
+ });
570
+ }
571
+ res.end();
572
+ } else {
573
+ const buf = Buffer.from(await upstream.arrayBuffer());
574
+ res.writeHead(upstream.status, headers);
575
+ res.end(buf);
576
+ if (buf.length) observe(buf.toString("utf8"), "s2c", correlator);
577
+ }
578
+ }
579
+ return new Promise((resolve) => {
580
+ server.listen(opts.port, opts.host, () => {
581
+ const addr = server.address();
582
+ const port = typeof addr === "object" && addr ? addr.port : opts.port;
583
+ resolve({
584
+ port,
585
+ close: () => new Promise((r) => {
586
+ opts.logger.close();
587
+ server.close(() => r());
588
+ })
589
+ });
590
+ });
591
+ });
592
+ }
593
+
594
+ // src/snapshot.ts
595
+ import { writeFileSync } from "fs";
596
+
597
+ // src/mcp-connection.ts
598
+ import { spawn as spawn2 } from "child_process";
599
+ var McpConnection = class _McpConnection {
600
+ child;
601
+ pending = /* @__PURE__ */ new Map();
602
+ framer;
603
+ nextId = 1;
604
+ stderrBuf = "";
605
+ closed = false;
606
+ constructor(command, args, env) {
607
+ this.child = spawn2(command, args, { stdio: ["pipe", "pipe", "pipe"], env: env ?? process.env });
608
+ this.child.stdin.on("error", () => {
609
+ });
610
+ this.child.on("error", (err) => {
611
+ this.closed = true;
612
+ for (const w of this.pending.values()) w.reject(err instanceof Error ? err : new Error(String(err)));
613
+ this.pending.clear();
614
+ });
615
+ this.child.stderr.on("data", (c) => {
616
+ this.stderrBuf += c.toString("utf8");
617
+ });
618
+ this.framer = new LineFramer((f) => {
619
+ if (!f.msg || f.msg.method !== void 0) return;
620
+ const id = f.msg.id;
621
+ if (typeof id !== "number" || !Number.isInteger(id)) return;
622
+ const w = this.pending.get(id);
623
+ if (!w) return;
624
+ this.pending.delete(id);
625
+ w.resolve({ result: f.msg.result, error: f.msg.error });
626
+ }, "s2c");
627
+ this.child.stdout.on("data", (c) => this.framer.push(c));
628
+ this.child.on("exit", () => {
629
+ this.closed = true;
630
+ for (const w of this.pending.values()) w.reject(new Error("server exited"));
631
+ this.pending.clear();
632
+ });
633
+ }
634
+ static spawn(command, args, env) {
635
+ return new _McpConnection(command, args, env);
636
+ }
637
+ get stderr() {
638
+ return this.stderrBuf;
639
+ }
640
+ request(method, params, timeoutMs = 15e3) {
641
+ if (this.closed) return Promise.reject(new Error("connection closed"));
642
+ const id = this.nextId++;
643
+ return new Promise((resolve, reject) => {
644
+ const timer = setTimeout(() => {
645
+ this.pending.delete(id);
646
+ const tail = this.stderrBuf.trim();
647
+ reject(
648
+ new Error(
649
+ `timed out after ${timeoutMs}ms on "${method}"` + (tail ? `
650
+ --- server stderr ---
651
+ ${tail}` : "")
652
+ )
653
+ );
654
+ }, timeoutMs);
655
+ this.pending.set(id, {
656
+ resolve: (v) => {
657
+ clearTimeout(timer);
658
+ resolve(v);
659
+ },
660
+ reject: (e) => {
661
+ clearTimeout(timer);
662
+ reject(e);
663
+ }
664
+ });
665
+ this.child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
666
+ });
667
+ }
668
+ notify(method, params) {
669
+ if (this.closed) return;
670
+ this.child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
671
+ }
672
+ close() {
673
+ this.closed = true;
674
+ this.child.kill();
675
+ }
676
+ };
677
+
678
+ // src/version.ts
679
+ var VERSION = "1.0.0";
680
+
681
+ // src/client.ts
682
+ var PROTOCOL_VERSION = "2025-11-25";
683
+ var PROBE_TIMEOUT_MS = 15e3;
684
+ async function probeServer(command, args, timeoutMs = PROBE_TIMEOUT_MS, env) {
685
+ const conn = McpConnection.spawn(command, args, env);
686
+ try {
687
+ const init = await conn.request(
688
+ "initialize",
689
+ {
690
+ protocolVersion: PROTOCOL_VERSION,
691
+ capabilities: {},
692
+ clientInfo: { name: "mcpgaze", version: VERSION }
693
+ },
694
+ timeoutMs
695
+ );
696
+ if (init.error) throw new Error(`initialize failed: ${init.error.message}`);
697
+ const initResult = init.result ?? {};
698
+ conn.notify("notifications/initialized");
699
+ const tools = [];
700
+ let cursor;
701
+ do {
702
+ const page = await conn.request("tools/list", cursor ? { cursor } : {}, timeoutMs);
703
+ if (page.error) throw new Error(`tools/list failed: ${page.error.message}`);
704
+ const r = page.result ?? {};
705
+ for (const t of r.tools ?? []) {
706
+ tools.push({ name: t.name, description: t.description, inputSchema: t.inputSchema });
707
+ }
708
+ cursor = r.nextCursor;
709
+ } while (cursor);
710
+ return {
711
+ protocolVersion: initResult.protocolVersion ?? PROTOCOL_VERSION,
712
+ server: initResult.serverInfo ?? {},
713
+ tools
714
+ };
715
+ } finally {
716
+ conn.close();
717
+ }
718
+ }
719
+
720
+ // src/snapshot.ts
721
+ async function buildBaseline(command, args) {
722
+ const probe = await probeServer(command, args);
723
+ const tools = {};
724
+ for (const t of probe.tools) {
725
+ tools[t.name] = { description: t.description ?? "", inputSchema: t.inputSchema ?? {} };
726
+ }
727
+ return {
728
+ mcpgazeVersion: VERSION,
729
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
730
+ protocolVersion: probe.protocolVersion,
731
+ server: probe.server,
732
+ tools
733
+ };
734
+ }
735
+ async function snapshot(command, args, outPath) {
736
+ const baseline = await buildBaseline(command, args);
737
+ writeFileSync(outPath, JSON.stringify(baseline, null, 2) + "\n");
738
+ return baseline;
739
+ }
740
+
741
+ // src/diff.ts
742
+ import { readFileSync } from "fs";
743
+
744
+ // src/schema-diff.ts
745
+ function fmtType(t) {
746
+ return t === void 0 ? "(none)" : JSON.stringify(t);
747
+ }
748
+ function diffInputSchema(toolName, oldSchemaRaw, newSchemaRaw) {
749
+ const changes = [];
750
+ const oldS = oldSchemaRaw ?? {};
751
+ const newS = newSchemaRaw ?? {};
752
+ const oldProps = oldS.properties ?? {};
753
+ const newProps = newS.properties ?? {};
754
+ const oldReq = new Set(oldS.required ?? []);
755
+ const newReq = new Set(newS.required ?? []);
756
+ for (const k of Object.keys(oldProps)) {
757
+ if (!(k in newProps)) {
758
+ changes.push({ severity: "breaking", path: `${toolName}.${k}`, message: "property removed" });
759
+ }
760
+ }
761
+ for (const k of Object.keys(newProps)) {
762
+ if (!(k in oldProps)) {
763
+ const req = newReq.has(k);
764
+ changes.push({
765
+ severity: req ? "breaking" : "info",
766
+ path: `${toolName}.${k}`,
767
+ message: req ? "new required property added" : "new optional property added"
768
+ });
769
+ }
770
+ }
771
+ for (const k of Object.keys(oldProps)) {
772
+ if (!(k in newProps)) continue;
773
+ const o = oldProps[k] ?? {};
774
+ const n = newProps[k] ?? {};
775
+ if (JSON.stringify(o.type) !== JSON.stringify(n.type)) {
776
+ changes.push({
777
+ severity: "breaking",
778
+ path: `${toolName}.${k}`,
779
+ message: `type changed from ${fmtType(o.type)} to ${fmtType(n.type)}`
780
+ });
781
+ }
782
+ if (o.enum || n.enum) {
783
+ const oset = new Set((o.enum ?? []).map((v) => JSON.stringify(v)));
784
+ const nset = new Set((n.enum ?? []).map((v) => JSON.stringify(v)));
785
+ for (const v of oset) {
786
+ if (!nset.has(v)) {
787
+ changes.push({ severity: "breaking", path: `${toolName}.${k}`, message: `enum value removed: ${v}` });
788
+ }
789
+ }
790
+ for (const v of nset) {
791
+ if (!oset.has(v)) {
792
+ changes.push({ severity: "info", path: `${toolName}.${k}`, message: `enum value added: ${v}` });
793
+ }
794
+ }
795
+ }
796
+ const wasReq = oldReq.has(k);
797
+ const isReq = newReq.has(k);
798
+ if (!wasReq && isReq) {
799
+ changes.push({ severity: "breaking", path: `${toolName}.${k}`, message: "became required" });
800
+ } else if (wasReq && !isReq) {
801
+ changes.push({ severity: "warning", path: `${toolName}.${k}`, message: "no longer required" });
802
+ }
803
+ }
804
+ return changes;
805
+ }
806
+ var RANK = { info: 0, warning: 1, breaking: 2 };
807
+ function worstSeverity(changes) {
808
+ let worst = null;
809
+ for (const c of changes) {
810
+ if (worst === null || RANK[c.severity] > RANK[worst]) worst = c.severity;
811
+ }
812
+ return worst;
813
+ }
814
+
815
+ // src/diff.ts
816
+ async function diff(command, args, baselinePath) {
817
+ const baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
818
+ const probe = await probeServer(command, args);
819
+ const current = {};
820
+ for (const t of probe.tools) {
821
+ current[t.name] = { description: t.description ?? "", inputSchema: t.inputSchema ?? {} };
822
+ }
823
+ const baseTools = baseline.tools ?? {};
824
+ const toolsRemoved = Object.keys(baseTools).filter((n) => !(n in current));
825
+ const toolsAdded = Object.keys(current).filter((n) => !(n in baseTools));
826
+ const changes = [];
827
+ for (const n of toolsRemoved) changes.push({ severity: "breaking", path: n, message: "tool removed" });
828
+ for (const n of toolsAdded) changes.push({ severity: "info", path: n, message: "tool added" });
829
+ for (const n of Object.keys(baseTools)) {
830
+ if (!(n in current)) continue;
831
+ changes.push(...diffInputSchema(n, baseTools[n].inputSchema, current[n].inputSchema));
832
+ if ((baseTools[n].description ?? "") !== (current[n].description ?? "")) {
833
+ changes.push({ severity: "info", path: n, message: "description changed" });
834
+ }
835
+ }
836
+ return { changes, toolsAdded, toolsRemoved };
837
+ }
838
+
839
+ // src/cassette.ts
840
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
841
+ var MAX_DEPTH2 = 200;
842
+ function stableStringify(value, depth = 0) {
843
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
844
+ if (depth >= MAX_DEPTH2) return '"__mcpgaze_max_depth__"';
845
+ if (Array.isArray(value)) return "[" + value.map((v) => stableStringify(v, depth + 1)).join(",") + "]";
846
+ const obj = value;
847
+ const keys = Object.keys(obj).sort();
848
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k], depth + 1)).join(",") + "}";
849
+ }
850
+ var CassetteRecorder = class {
851
+ /** When true, mask credential-shaped params/results before they are persisted. */
852
+ constructor(redact = false) {
853
+ this.redact = redact;
854
+ }
855
+ redact;
856
+ interactions = [];
857
+ seen = /* @__PURE__ */ new Set();
858
+ add(request, response) {
859
+ const params = this.redact ? redactValue(request.params) : request.params;
860
+ const interaction = {
861
+ request: { method: request.method, params },
862
+ response: response.error ? { error: this.redact ? redactValue(response.error) : response.error } : { result: this.redact ? redactValue(response.result) : response.result }
863
+ };
864
+ const key = stableStringify(interaction);
865
+ if (this.seen.has(key)) return;
866
+ this.seen.add(key);
867
+ this.interactions.push(interaction);
868
+ }
869
+ toCassette() {
870
+ return {
871
+ mcpgazeVersion: VERSION,
872
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
873
+ interactions: this.interactions
874
+ };
875
+ }
876
+ write(path) {
877
+ writeFileSync2(path, JSON.stringify(this.toCassette(), null, 2) + "\n", { mode: 384 });
878
+ return this.interactions.length;
879
+ }
880
+ };
881
+ function buildIndex(cassette) {
882
+ const byKey = /* @__PURE__ */ new Map();
883
+ const byMethod = /* @__PURE__ */ new Map();
884
+ for (const it of cassette.interactions) {
885
+ byKey.set(it.request.method + "|" + stableStringify(it.request.params ?? null), it);
886
+ const list = byMethod.get(it.request.method) ?? [];
887
+ list.push(it);
888
+ byMethod.set(it.request.method, list);
889
+ }
890
+ return { byKey, byMethod };
891
+ }
892
+ function matchRequest(index, method, params) {
893
+ const exact = index.byKey.get(method + "|" + stableStringify(params ?? null));
894
+ const chosen = exact ?? pickMethodOnly(index, method);
895
+ if (!chosen) {
896
+ return { kind: "error", error: { code: -32601, message: `no recorded interaction for "${method}"` } };
897
+ }
898
+ if (chosen.response.error) return { kind: "error", error: chosen.response.error };
899
+ return { kind: "result", result: chosen.response.result };
900
+ }
901
+ function pickMethodOnly(index, method) {
902
+ const list = index.byMethod.get(method);
903
+ return list && list.length === 1 ? list[0] : void 0;
904
+ }
905
+ function runReplayServer(cassettePath) {
906
+ return new Promise((resolve, reject) => {
907
+ let index;
908
+ try {
909
+ const cassette = parseCassette(readFileSync2(cassettePath, "utf8"));
910
+ index = buildIndex(cassette);
911
+ } catch (e) {
912
+ reject(new Error(`invalid cassette: ${e.message}`));
913
+ return;
914
+ }
915
+ const framer = new LineFramer((f) => {
916
+ if (!f.msg) return;
917
+ const isRequest = f.msg.method !== void 0 && f.msg.id !== void 0 && f.msg.id !== null;
918
+ if (!isRequest) return;
919
+ const outcome = matchRequest(index, f.msg.method, f.msg.params);
920
+ const envelope = outcome.kind === "result" ? { jsonrpc: "2.0", id: f.msg.id, result: outcome.result } : { jsonrpc: "2.0", id: f.msg.id, error: outcome.error };
921
+ process.stdout.write(JSON.stringify(envelope) + "\n");
922
+ }, "c2s");
923
+ process.stdin.on("data", (chunk) => {
924
+ try {
925
+ framer.push(chunk);
926
+ } catch {
927
+ }
928
+ });
929
+ process.stdin.on("end", () => resolve(0));
930
+ for (const sig of ["SIGINT", "SIGTERM"]) {
931
+ process.on(sig, () => resolve(0));
932
+ }
933
+ });
934
+ }
935
+ function parseCassette(text) {
936
+ const parsed = JSON.parse(text);
937
+ if (!parsed || typeof parsed !== "object") throw new Error("not a JSON object");
938
+ const c = parsed;
939
+ if (!Array.isArray(c.interactions)) throw new Error("missing 'interactions' array");
940
+ for (const it of c.interactions) {
941
+ if (!it || typeof it !== "object") throw new Error("interaction is not an object");
942
+ const req = it.request;
943
+ if (!req || typeof req !== "object" || typeof req.method !== "string") {
944
+ throw new Error("interaction.request.method must be a string");
945
+ }
946
+ }
947
+ return parsed;
948
+ }
949
+
950
+ // src/preflight.ts
951
+ import { readFileSync as readFileSync3 } from "fs";
952
+ var GUI_INHERITED_DEFAULT = process.platform === "win32" ? ["PATH", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP", "SystemRoot", "ProgramFiles", "ProgramData", "ComSpec", "NUMBER_OF_PROCESSORS", "OS"] : ["HOME", "PATH", "USER", "SHELL", "LANG", "LOGNAME", "TERM", "TMPDIR", "LC_ALL", "LC_CTYPE"];
953
+ var SUSPECT = /KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|_API|API_|AUTH|DSN|ENDPOINT|_URL|_URI|_HOST|_PORT|ACCOUNT|REGION|BUCKET|PROJECT|DATABASE|CONN/i;
954
+ function restrict(env, allow) {
955
+ const out = {};
956
+ for (const k of allow) if (env[k] !== void 0) out[k] = env[k];
957
+ return out;
958
+ }
959
+ async function preflight(command, args, options = {}) {
960
+ const env = options.baseEnv ?? process.env;
961
+ const allow = options.allowlist ?? GUI_INHERITED_DEFAULT;
962
+ const timeout = options.timeoutMs ?? PROBE_TIMEOUT_MS;
963
+ let fullEnvOk = true;
964
+ let fullError;
965
+ try {
966
+ await probeServer(command, args, timeout, env);
967
+ } catch (e) {
968
+ fullEnvOk = false;
969
+ fullError = e.message;
970
+ }
971
+ let restrictedEnvOk = true;
972
+ let restrictedError;
973
+ try {
974
+ await probeServer(command, args, timeout, restrict(env, allow));
975
+ } catch (e) {
976
+ restrictedEnvOk = false;
977
+ restrictedError = e.message;
978
+ }
979
+ const allowSet = new Set(allow);
980
+ const missingVars = Object.keys(env).filter((k) => !allowSet.has(k)).sort();
981
+ const suspectVars = missingVars.filter((k) => SUSPECT.test(k));
982
+ return { fullEnvOk, restrictedEnvOk, fullError, restrictedError, missingVars, suspectVars };
983
+ }
984
+ function checkConfigEnv(configPath, serverName) {
985
+ const raw = JSON.parse(readFileSync3(configPath, "utf8"));
986
+ const servers = raw.mcpServers ?? {};
987
+ const names = serverName ? [serverName] : Object.keys(servers);
988
+ const findings = [];
989
+ for (const name of names) {
990
+ const env = servers[name]?.env ?? {};
991
+ for (const [key, value] of Object.entries(env)) {
992
+ if (typeof value !== "string") continue;
993
+ if (/\$\{?[A-Z_]/i.test(value)) {
994
+ findings.push({ level: "error", key, message: `value contains "${value}" \u2014 GUI clients do NOT expand shell variables; pass the literal value` });
995
+ }
996
+ if (value.trim() === "") {
997
+ findings.push({ level: "warning", key, message: "empty value" });
998
+ }
999
+ }
1000
+ }
1001
+ return findings;
1002
+ }
1003
+
1004
+ // src/conform.ts
1005
+ var KNOWN_SPEC_VERSIONS = ["2025-06-18", "2025-11-25", "2026-07-28"];
1006
+ var ok = (detail) => ({ status: "pass", detail });
1007
+ var fail = (detail) => ({ status: "fail", detail });
1008
+ var warn = (detail) => ({ status: "warn", detail });
1009
+ var CHECKS = [
1010
+ {
1011
+ id: "init.result",
1012
+ title: "initialize returns a valid result",
1013
+ level: "required",
1014
+ run: async (c) => c.initError ? fail(c.initError) : c.initResult ? ok("initialize responded") : fail("no initialize result")
1015
+ },
1016
+ {
1017
+ id: "init.protocolVersion",
1018
+ title: "initialize result advertises a protocolVersion",
1019
+ level: "required",
1020
+ run: async (c) => c.initResult?.protocolVersion ? ok(`server reports ${c.initResult.protocolVersion}`) : fail("missing protocolVersion in initialize result")
1021
+ },
1022
+ {
1023
+ id: "init.serverInfo",
1024
+ title: "initialize result includes serverInfo.name",
1025
+ level: "required",
1026
+ run: async (c) => c.initResult?.serverInfo?.name ? ok(`serverInfo.name = ${c.initResult.serverInfo.name}`) : fail("missing serverInfo.name")
1027
+ },
1028
+ {
1029
+ id: "init.capabilities",
1030
+ title: "initialize result includes a capabilities object",
1031
+ level: "recommended",
1032
+ run: async (c) => c.initResult && typeof c.initResult.capabilities === "object" && c.initResult.capabilities !== null ? ok("capabilities present") : warn("no capabilities object advertised")
1033
+ },
1034
+ {
1035
+ id: "tools.list",
1036
+ title: "tools/list returns an array of tools",
1037
+ level: "required",
1038
+ run: async (c) => Array.isArray(c.tools) ? ok(`${c.tools.length} tool(s)`) : fail("tools/list did not return an array")
1039
+ },
1040
+ {
1041
+ id: "tools.names",
1042
+ title: "every tool has a non-empty name",
1043
+ level: "required",
1044
+ run: async (c) => {
1045
+ const list = Array.isArray(c.tools) ? c.tools : [];
1046
+ const bad = list.filter((t) => !t.name || typeof t.name !== "string");
1047
+ return bad.length === 0 ? ok("all tools named") : fail(`${bad.length} tool(s) missing a name`);
1048
+ }
1049
+ },
1050
+ {
1051
+ id: "tools.inputSchema",
1052
+ title: 'every tool inputSchema is an object schema (type: "object")',
1053
+ level: "recommended",
1054
+ run: async (c) => {
1055
+ const list = Array.isArray(c.tools) ? c.tools : [];
1056
+ const bad = list.filter((t) => {
1057
+ const s = t.inputSchema;
1058
+ return !s || typeof s !== "object" || s.type !== "object";
1059
+ });
1060
+ return bad.length === 0 ? ok("all input schemas are object schemas") : warn(`${bad.length} tool(s) without an object inputSchema`);
1061
+ }
1062
+ },
1063
+ {
1064
+ id: "tools.requiredRefs",
1065
+ title: "tool required[] only names declared properties",
1066
+ level: "recommended",
1067
+ run: async (c) => {
1068
+ const offenders = [];
1069
+ for (const t of Array.isArray(c.tools) ? c.tools : []) {
1070
+ const s = t.inputSchema;
1071
+ if (!Array.isArray(s?.required)) continue;
1072
+ const props = new Set(Object.keys(s.properties ?? {}));
1073
+ for (const r of s.required) if (!props.has(r)) offenders.push(`${t.name}.${r}`);
1074
+ }
1075
+ return offenders.length === 0 ? ok("required[] is consistent") : warn(`undeclared required fields: ${offenders.join(", ")}`);
1076
+ }
1077
+ },
1078
+ {
1079
+ id: "error.unknownMethod",
1080
+ title: "an unknown method returns a JSON-RPC error (not a hang/crash)",
1081
+ level: "required",
1082
+ run: async (c) => {
1083
+ try {
1084
+ const res = await c.conn.request("mcpgaze/definitely-not-a-method", {}, Math.min(c.timeoutMs, 4e3));
1085
+ if (res.error) {
1086
+ return res.error.code === -32601 ? ok("returns -32601 method not found") : warn(`returns an error, but code ${res.error.code} (expected -32601)`);
1087
+ }
1088
+ return fail("unknown method returned a result instead of an error");
1089
+ } catch (e) {
1090
+ return fail(`no error response (${e.message.split("\n")[0]})`);
1091
+ }
1092
+ }
1093
+ }
1094
+ ];
1095
+ async function conform(command, args, protocolVersion, timeoutMs = 8e3) {
1096
+ const conn = McpConnection.spawn(command, args);
1097
+ const ctx = { conn, initResult: null, tools: [], timeoutMs };
1098
+ try {
1099
+ const init = await conn.request(
1100
+ "initialize",
1101
+ { protocolVersion, capabilities: {}, clientInfo: { name: "mcpgaze", version: VERSION } },
1102
+ timeoutMs
1103
+ );
1104
+ if (init.error) ctx.initError = `initialize error: ${init.error.message}`;
1105
+ else ctx.initResult = init.result ?? {};
1106
+ conn.notify("notifications/initialized");
1107
+ if (ctx.initResult) {
1108
+ try {
1109
+ const list = await conn.request("tools/list", {}, timeoutMs);
1110
+ const r = list.result ?? {};
1111
+ ctx.tools = r.tools ?? [];
1112
+ } catch {
1113
+ ctx.tools = [];
1114
+ }
1115
+ }
1116
+ const results = [];
1117
+ for (const chk of CHECKS) {
1118
+ let r;
1119
+ try {
1120
+ r = await chk.run(ctx);
1121
+ } catch (e) {
1122
+ r = { status: "fail", detail: `check errored: ${e.message}` };
1123
+ }
1124
+ results.push({ id: chk.id, title: chk.title, level: chk.level, ...r });
1125
+ }
1126
+ const passed = !results.some((r) => r.level === "required" && r.status === "fail");
1127
+ return {
1128
+ protocolVersion,
1129
+ serverProtocolVersion: ctx.initResult?.protocolVersion ?? null,
1130
+ results,
1131
+ passed
1132
+ };
1133
+ } finally {
1134
+ conn.close();
1135
+ }
1136
+ }
1137
+
1138
+ // src/verify.ts
1139
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
1140
+
1141
+ // src/shape.ts
1142
+ var MAX_DEPTH3 = 500;
1143
+ function shapeOf(value, depth = 0) {
1144
+ if (value === null || value === void 0) return "null";
1145
+ if (depth >= MAX_DEPTH3) return "null";
1146
+ if (Array.isArray(value)) return { array: value.length ? shapeOf(value[0], depth + 1) : null };
1147
+ const t = typeof value;
1148
+ if (t === "string") return "string";
1149
+ if (t === "number") return "number";
1150
+ if (t === "boolean") return "boolean";
1151
+ if (t === "object") {
1152
+ const obj = value;
1153
+ const out = {};
1154
+ for (const k of Object.keys(obj).sort()) out[k] = shapeOf(obj[k], depth + 1);
1155
+ return { object: out };
1156
+ }
1157
+ return "null";
1158
+ }
1159
+ function kind(s) {
1160
+ if (typeof s === "object") return "object" in s ? "object" : "array";
1161
+ return "primitive";
1162
+ }
1163
+ function diffShape(path, oldS, newS, depth = 0) {
1164
+ const changes = [];
1165
+ if (depth >= MAX_DEPTH3) return changes;
1166
+ const ok2 = kind(oldS);
1167
+ const nk = kind(newS);
1168
+ if (ok2 !== nk) {
1169
+ changes.push({ severity: "breaking", path, message: `type changed from ${ok2} to ${nk}` });
1170
+ return changes;
1171
+ }
1172
+ if (ok2 === "primitive") {
1173
+ if (oldS !== newS) {
1174
+ changes.push({ severity: "breaking", path, message: `type changed from ${String(oldS)} to ${String(newS)}` });
1175
+ }
1176
+ return changes;
1177
+ }
1178
+ if (ok2 === "object") {
1179
+ const o = oldS.object;
1180
+ const n = newS.object;
1181
+ for (const k of Object.keys(o)) {
1182
+ if (!(k in n)) changes.push({ severity: "breaking", path: `${path}.${k}`, message: "field removed from response" });
1183
+ else changes.push(...diffShape(`${path}.${k}`, o[k], n[k], depth + 1));
1184
+ }
1185
+ for (const k of Object.keys(n)) {
1186
+ if (!(k in o)) changes.push({ severity: "info", path: `${path}.${k}`, message: "field added to response" });
1187
+ }
1188
+ return changes;
1189
+ }
1190
+ const oe = oldS.array;
1191
+ const ne = newS.array;
1192
+ if (oe !== null && ne === null) {
1193
+ changes.push({ severity: "warning", path: `${path}[]`, message: "array is now empty (was populated)" });
1194
+ } else if (oe === null && ne !== null) {
1195
+ changes.push({ severity: "info", path: `${path}[]`, message: "array is now populated (was empty)" });
1196
+ } else if (oe !== null && ne !== null) {
1197
+ changes.push(...diffShape(`${path}[]`, oe, ne, depth + 1));
1198
+ }
1199
+ return changes;
1200
+ }
1201
+
1202
+ // src/verify.ts
1203
+ var READ_ONLY_METHODS = /* @__PURE__ */ new Set([
1204
+ "tools/list",
1205
+ "resources/list",
1206
+ "resources/templates/list",
1207
+ "prompts/list",
1208
+ "ping",
1209
+ "completion/complete"
1210
+ ]);
1211
+ function isVerifiable(method) {
1212
+ return method !== "initialize" && !method.startsWith("notifications/");
1213
+ }
1214
+ function shouldReissue(method, allowToolCalls) {
1215
+ if (!isVerifiable(method)) return false;
1216
+ return allowToolCalls || READ_ONLY_METHODS.has(method);
1217
+ }
1218
+ async function verify(command, args, cassettePath, timeoutMs = 15e3, opts = {}) {
1219
+ const allowToolCalls = Boolean(opts.allowToolCalls);
1220
+ const cassette = parseCassette(readFileSync4(cassettePath, "utf8"));
1221
+ const conn = McpConnection.spawn(command, args);
1222
+ const result = { checked: 0, changes: [], errors: [], skipped: [] };
1223
+ try {
1224
+ const init = await conn.request(
1225
+ "initialize",
1226
+ { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "mcpgaze", version: VERSION } },
1227
+ timeoutMs
1228
+ );
1229
+ if (init.error) throw new Error(`initialize failed: ${init.error.message}`);
1230
+ conn.notify("notifications/initialized");
1231
+ for (const it of cassette.interactions) {
1232
+ const method = it.request.method;
1233
+ if (!isVerifiable(method)) continue;
1234
+ if (!shouldReissue(method, allowToolCalls)) {
1235
+ result.skipped.push(method);
1236
+ continue;
1237
+ }
1238
+ result.checked++;
1239
+ let live;
1240
+ try {
1241
+ live = await conn.request(method, it.request.params, timeoutMs);
1242
+ } catch (e) {
1243
+ result.errors.push({ method, message: e.message.split("\n")[0] });
1244
+ continue;
1245
+ }
1246
+ const recordedIsError = it.response.error !== void 0;
1247
+ const liveIsError = live.error !== void 0;
1248
+ if (recordedIsError !== liveIsError) {
1249
+ result.changes.push({
1250
+ method,
1251
+ severity: "breaking",
1252
+ path: method,
1253
+ message: recordedIsError ? "recorded an error but the live server now returns a result" : "recorded a result but the live server now returns an error"
1254
+ });
1255
+ continue;
1256
+ }
1257
+ if (recordedIsError) continue;
1258
+ const recordedShape = shapeOf(it.response.result);
1259
+ const liveShape = shapeOf(live.result);
1260
+ for (const c of diffShape(method, recordedShape, liveShape)) {
1261
+ result.changes.push({ method, ...c });
1262
+ }
1263
+ }
1264
+ return result;
1265
+ } finally {
1266
+ conn.close();
1267
+ }
1268
+ }
1269
+ async function updateCassette(command, args, cassettePath, timeoutMs = 15e3, opts = {}) {
1270
+ const allowToolCalls = Boolean(opts.allowToolCalls);
1271
+ const cassette = parseCassette(readFileSync4(cassettePath, "utf8"));
1272
+ const conn = McpConnection.spawn(command, args);
1273
+ let updated = 0;
1274
+ try {
1275
+ const init = await conn.request(
1276
+ "initialize",
1277
+ { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "mcpgaze", version: VERSION } },
1278
+ timeoutMs
1279
+ );
1280
+ if (init.error) throw new Error(`initialize failed: ${init.error.message}`);
1281
+ conn.notify("notifications/initialized");
1282
+ for (const it of cassette.interactions) {
1283
+ if (!shouldReissue(it.request.method, allowToolCalls)) continue;
1284
+ const live = await conn.request(it.request.method, it.request.params, timeoutMs);
1285
+ it.response = live.error ? { error: live.error } : { result: live.result };
1286
+ updated++;
1287
+ }
1288
+ cassette.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
1289
+ let serialized;
1290
+ try {
1291
+ serialized = JSON.stringify(cassette, null, 2) + "\n";
1292
+ } catch (e) {
1293
+ throw new Error(
1294
+ `failed to serialize updated cassette (a live response is too deeply nested): ${e.message}`
1295
+ );
1296
+ }
1297
+ writeFileSync3(cassettePath, serialized, { mode: 384 });
1298
+ return updated;
1299
+ } finally {
1300
+ conn.close();
1301
+ }
1302
+ }
1303
+
1304
+ // src/triage.ts
1305
+ import { readFileSync as readFileSync5 } from "fs";
1306
+ var STDERR_SIGNAL = /(error|fatal|exception|traceback|panic|unhandled|econnrefused|eaddrinuse)/i;
1307
+ var NOTE_FAILURES = /* @__PURE__ */ new Set(["orphan-request", "spawn-error", "proxy-error", "observer-error", "origin-rejected"]);
1308
+ function truncate(s, n = 400) {
1309
+ return s.length > n ? s.slice(0, n) + "\u2026" : s;
1310
+ }
1311
+ function extractFailures(events) {
1312
+ const failures = [];
1313
+ for (const e of events) {
1314
+ if (e.type === "message" && e.kind === "error") {
1315
+ failures.push({ kind: "rpc-error", summary: `error response to ${e.method ?? "request"} (id ${String(e.id)})`, detail: e.raw ? truncate(e.raw) : void 0 });
1316
+ } else if (e.type === "message" && e.parseError) {
1317
+ failures.push({ kind: "parse-error", summary: `malformed JSON-RPC on ${e.dir ?? "?"}`, detail: e.parseError });
1318
+ } else if (e.type === "note" && e.code && NOTE_FAILURES.has(e.code)) {
1319
+ failures.push({ kind: e.code, summary: e.code.replace(/-/g, " "), detail: e.detail });
1320
+ } else if (e.type === "server_stderr" && e.text && STDERR_SIGNAL.test(e.text)) {
1321
+ failures.push({ kind: "server-stderr", summary: "server logged an error", detail: truncate(e.text.trim()) });
1322
+ }
1323
+ }
1324
+ return failures;
1325
+ }
1326
+ function parseSessionLog(path) {
1327
+ const text = readFileSync5(path, "utf8");
1328
+ const out = [];
1329
+ for (const line of text.split("\n")) {
1330
+ const t = line.trim();
1331
+ if (!t) continue;
1332
+ try {
1333
+ out.push(JSON.parse(t));
1334
+ } catch {
1335
+ }
1336
+ }
1337
+ return out;
1338
+ }
1339
+ function buildTriagePrompt(failures) {
1340
+ const lines = failures.map(
1341
+ (f, i) => `${i + 1}. [${f.kind}] ${f.summary}${f.detail ? `
1342
+ ${redactText(f.detail)}` : ""}`
1343
+ );
1344
+ return [
1345
+ "You are debugging a Model Context Protocol (MCP) server. Below are failure",
1346
+ "signals captured by a transparent proxy during a live session. For each",
1347
+ "distinct problem, give the most likely root cause and a concrete fix.",
1348
+ "Be specific and concise (a few bullets). Common MCP gotchas: writing logs to",
1349
+ "stdout corrupts the JSON-RPC wire; GUI clients don't inherit shell env vars;",
1350
+ "tool schema/signature drift breaks agents silently; default ~30s client",
1351
+ "timeouts; transport mismatches (stdio vs Streamable HTTP).",
1352
+ "",
1353
+ "Failure signals:",
1354
+ ...lines
1355
+ ].join("\n");
1356
+ }
1357
+ var DEFAULT_TRIAGE_MODEL = "claude-sonnet-4-6";
1358
+ async function callClaude(prompt, apiKey, model = DEFAULT_TRIAGE_MODEL) {
1359
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
1360
+ method: "POST",
1361
+ headers: {
1362
+ "content-type": "application/json",
1363
+ "x-api-key": apiKey,
1364
+ "anthropic-version": "2023-06-01"
1365
+ },
1366
+ body: JSON.stringify({ model, max_tokens: 1024, messages: [{ role: "user", content: prompt }] })
1367
+ });
1368
+ if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${truncate(await res.text(), 200)}`);
1369
+ const data = await res.json();
1370
+ return (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text ?? "").join("").trim();
1371
+ }
1372
+ async function triage(logPath, opts = {}) {
1373
+ const failures = extractFailures(parseSessionLog(logPath));
1374
+ const report = { failures };
1375
+ if (failures.length === 0) return report;
1376
+ if (!opts.useAi) {
1377
+ report.aiSkippedReason = "AI triage not requested (pass --ai)";
1378
+ return report;
1379
+ }
1380
+ if (!opts.apiKey) {
1381
+ report.aiSkippedReason = "no ANTHROPIC_API_KEY set";
1382
+ return report;
1383
+ }
1384
+ const prompt = buildTriagePrompt(failures);
1385
+ const consented = opts.confirmEgress ? await opts.confirmEgress(prompt) : false;
1386
+ if (!consented) {
1387
+ report.aiSkippedReason = "egress to api.anthropic.com not confirmed (pass --yes to consent)";
1388
+ return report;
1389
+ }
1390
+ try {
1391
+ report.aiDiagnosis = await callClaude(prompt, opts.apiKey, opts.model);
1392
+ } catch (e) {
1393
+ report.aiSkippedReason = e.message;
1394
+ }
1395
+ return report;
1396
+ }
1397
+
1398
+ // src/health.ts
1399
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "fs";
1400
+ import { dirname as dirname2 } from "path";
1401
+ import { performance as performance2 } from "perf_hooks";
1402
+ async function healthCheckOnce(command, args, timeoutMs = 8e3) {
1403
+ const at = (/* @__PURE__ */ new Date()).toISOString();
1404
+ const conn = McpConnection.spawn(command, args);
1405
+ const t0 = performance2.now();
1406
+ try {
1407
+ const init = await conn.request(
1408
+ "initialize",
1409
+ { protocolVersion: PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: "mcpgaze", version: VERSION } },
1410
+ timeoutMs
1411
+ );
1412
+ if (init.error) return { at, ok: false, error: `initialize: ${init.error.message}` };
1413
+ conn.notify("notifications/initialized");
1414
+ const list = await conn.request("tools/list", {}, timeoutMs);
1415
+ if (list.error) return { at, ok: false, error: `tools/list: ${list.error.message}` };
1416
+ const tools = (list.result ?? {}).tools ?? [];
1417
+ return {
1418
+ at,
1419
+ ok: true,
1420
+ latencyMs: Math.round(performance2.now() - t0),
1421
+ toolCount: tools.length,
1422
+ toolsHash: stableStringify(tools.map((t) => ({ n: t.name, s: t.inputSchema })))
1423
+ };
1424
+ } catch (e) {
1425
+ return { at, ok: false, error: e.message.split("\n")[0] };
1426
+ } finally {
1427
+ conn.close();
1428
+ }
1429
+ }
1430
+ function summarize(history) {
1431
+ const checks = history.length;
1432
+ const upCount = history.filter((h) => h.ok).length;
1433
+ let consecutiveFailures = 0;
1434
+ for (let i = history.length - 1; i >= 0; i--) {
1435
+ if (history[i].ok) break;
1436
+ consecutiveFailures++;
1437
+ }
1438
+ const lats = history.filter((h) => h.ok && typeof h.latencyMs === "number").map((h) => h.latencyMs).sort((a, b) => a - b);
1439
+ const p50 = lats.length ? lats[Math.floor(lats.length / 2)] : null;
1440
+ return {
1441
+ checks,
1442
+ upCount,
1443
+ uptimePct: checks ? Math.round(upCount / checks * 1e3) / 10 : 0,
1444
+ consecutiveFailures,
1445
+ p50LatencyMs: p50
1446
+ };
1447
+ }
1448
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
1449
+ async function runHealthDaemon(command, args, opts = {}) {
1450
+ const intervalMs = opts.intervalMs ?? 6e4;
1451
+ const statusPath = opts.statusPath ?? ".mcpgaze/health.json";
1452
+ const maxHistory = opts.maxHistory ?? 500;
1453
+ const history = [];
1454
+ let prevOk = null;
1455
+ let prevHash;
1456
+ let last = { at: (/* @__PURE__ */ new Date()).toISOString(), ok: false };
1457
+ const persist = () => {
1458
+ try {
1459
+ mkdirSync2(dirname2(statusPath), { recursive: true });
1460
+ writeFileSync4(
1461
+ statusPath,
1462
+ JSON.stringify(
1463
+ { server: [command, ...args].join(" "), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), current: last, summary: summarize(history), history },
1464
+ null,
1465
+ 2
1466
+ )
1467
+ );
1468
+ } catch {
1469
+ }
1470
+ };
1471
+ for (; ; ) {
1472
+ const c = await healthCheckOnce(command, args);
1473
+ last = c;
1474
+ history.push(c);
1475
+ if (history.length > maxHistory) history.shift();
1476
+ opts.onCheck?.(c);
1477
+ if (prevOk !== null && prevOk !== c.ok) {
1478
+ opts.onTransition?.(c.ok ? "recovered: server is responding again" : `DOWN: ${c.error ?? "unresponsive"}`);
1479
+ }
1480
+ if (c.ok && prevHash !== void 0 && c.toolsHash !== prevHash) {
1481
+ opts.onTransition?.("schema drift: the server's tool surface changed");
1482
+ }
1483
+ prevOk = c.ok;
1484
+ if (c.ok && c.toolsHash) prevHash = c.toolsHash;
1485
+ persist();
1486
+ if (opts.once) return c;
1487
+ await sleep(intervalMs);
1488
+ }
1489
+ }
1490
+
1491
+ // src/tui.ts
1492
+ var TuiState = class {
1493
+ serverCmd;
1494
+ startedAt = Date.now();
1495
+ requests = 0;
1496
+ responses = 0;
1497
+ errors = 0;
1498
+ notifications = 0;
1499
+ orphans = 0;
1500
+ parseErrors = 0;
1501
+ latencies = [];
1502
+ recent = [];
1503
+ lastStderr = "";
1504
+ constructor(serverCmd) {
1505
+ this.serverCmd = serverCmd;
1506
+ }
1507
+ ingest(e) {
1508
+ if (e.type === "message") {
1509
+ if (e.parseError) this.parseErrors++;
1510
+ switch (e.kind) {
1511
+ case "request":
1512
+ this.requests++;
1513
+ break;
1514
+ case "response":
1515
+ this.responses++;
1516
+ break;
1517
+ case "error":
1518
+ this.errors++;
1519
+ break;
1520
+ case "notification":
1521
+ this.notifications++;
1522
+ break;
1523
+ }
1524
+ if (typeof e.latencyMs === "number") this.latencies.push(e.latencyMs);
1525
+ this.recent.push({
1526
+ dir: e.dir ?? "?",
1527
+ kind: e.kind ?? "?",
1528
+ method: e.method ?? "",
1529
+ id: e.id != null ? String(e.id) : "",
1530
+ latencyMs: typeof e.latencyMs === "number" ? e.latencyMs : null,
1531
+ error: e.kind === "error"
1532
+ });
1533
+ if (this.recent.length > 200) this.recent.shift();
1534
+ } else if (e.type === "server_stderr" && e.text) {
1535
+ this.lastStderr = e.text.trim().split("\n").pop() ?? "";
1536
+ } else if (e.type === "note" && e.code === "orphan-request") {
1537
+ this.orphans++;
1538
+ }
1539
+ }
1540
+ };
1541
+ function percentile(samples, p) {
1542
+ if (samples.length === 0) return null;
1543
+ const sorted = [...samples].sort((a, b) => a - b);
1544
+ const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
1545
+ return sorted[idx];
1546
+ }
1547
+ function fmtMs(n) {
1548
+ return n == null ? "\u2014" : `${n.toFixed(0)}ms`;
1549
+ }
1550
+ function fmtDuration(ms) {
1551
+ const s = Math.floor(ms / 1e3);
1552
+ if (s < 60) return `${s}s`;
1553
+ const m = Math.floor(s / 60);
1554
+ return `${m}m${String(s % 60).padStart(2, "0")}s`;
1555
+ }
1556
+ function renderFrame(state, cols, rows) {
1557
+ const width = Math.max(40, Math.min(1e3, Number.isFinite(cols) ? Math.floor(cols) : 80));
1558
+ const safeRows = Math.max(8, Math.min(1e3, Number.isFinite(rows) ? Math.floor(rows) : 24));
1559
+ const bar = "\u2500".repeat(width);
1560
+ const lines = [];
1561
+ lines.push(color.bold("mcpgaze") + color.dim(` \u27F3 ${fmtDuration(Date.now() - state.startedAt)} \xB7 ${state.serverCmd}`));
1562
+ lines.push(color.dim(bar));
1563
+ const listHeight = Math.max(3, safeRows - 8);
1564
+ const slice = state.recent.slice(-listHeight);
1565
+ for (const r of slice) {
1566
+ const arrow = r.dir === "c2s" ? color.cyan("\u2192") : color.green("\u2190");
1567
+ const tag = r.error ? color.red("ERR ") : r.kind === "request" ? color.bold("req ") : r.kind === "response" ? "res " : r.kind === "notification" ? color.gray("note") : "? ";
1568
+ const id = r.id ? color.dim(`#${r.id}`) : " ";
1569
+ const lat = r.latencyMs != null ? color.dim(` ${r.latencyMs.toFixed(1)}ms`) : "";
1570
+ lines.push(`${arrow} ${tag} ${id} ${r.method}${lat}`);
1571
+ }
1572
+ for (let i = slice.length; i < listHeight; i++) lines.push("");
1573
+ lines.push(color.dim(bar));
1574
+ if (state.lastStderr) lines.push(color.yellow("stderr ") + color.dim(state.lastStderr.slice(0, width - 8)));
1575
+ else lines.push(color.dim("stderr \u2014"));
1576
+ const p50 = fmtMs(percentile(state.latencies, 50));
1577
+ const p95 = fmtMs(percentile(state.latencies, 95));
1578
+ const errPart = state.errors > 0 ? color.red(`${state.errors} err`) : color.green("0 err");
1579
+ const orphanPart = state.orphans > 0 ? color.red(`${state.orphans} orphan`) : "0 orphan";
1580
+ lines.push(
1581
+ color.dim(
1582
+ `req ${state.requests} res ${state.responses} notif ${state.notifications} `
1583
+ ) + `${errPart} ${orphanPart} ` + color.dim(`p50 ${p50} p95 ${p95}`)
1584
+ );
1585
+ lines.push(color.dim("Ctrl-C to stop"));
1586
+ return lines.join("\n");
1587
+ }
1588
+ var ALT_ON = "\x1B[?1049h\x1B[?25l";
1589
+ var ALT_OFF = "\x1B[?25h\x1B[?1049l";
1590
+ var HOME = "\x1B[H";
1591
+ var CLEAR_DOWN = "\x1B[0J";
1592
+ var Tui = class {
1593
+ state;
1594
+ out;
1595
+ timer = null;
1596
+ active = false;
1597
+ constructor(serverCmd, out = process.stderr) {
1598
+ this.state = new TuiState(serverCmd);
1599
+ this.out = out;
1600
+ }
1601
+ static isSupported(out = process.stderr) {
1602
+ return Boolean(out.isTTY) && !process.env.NO_TUI;
1603
+ }
1604
+ start() {
1605
+ this.active = true;
1606
+ this.out.write(ALT_ON);
1607
+ this.paint();
1608
+ this.timer = setInterval(() => this.paint(), 100);
1609
+ }
1610
+ update(e) {
1611
+ this.state.ingest(e);
1612
+ }
1613
+ paint() {
1614
+ if (!this.active) return;
1615
+ const cols = this.out.columns ?? 80;
1616
+ const rows = this.out.rows ?? 24;
1617
+ this.out.write(HOME + renderFrame(this.state, cols, rows) + CLEAR_DOWN);
1618
+ }
1619
+ stop() {
1620
+ if (!this.active) return;
1621
+ this.active = false;
1622
+ if (this.timer) clearInterval(this.timer);
1623
+ this.out.write(ALT_OFF);
1624
+ }
1625
+ };
1626
+
1627
+ // src/index.ts
1628
+ function splitOnDoubleDash(args) {
1629
+ const i = args.indexOf("--");
1630
+ if (i === -1) return { opts: args, cmd: [] };
1631
+ return { opts: args.slice(0, i), cmd: args.slice(i + 1) };
1632
+ }
1633
+ function getOpt(opts, name) {
1634
+ const i = opts.indexOf(name);
1635
+ return i >= 0 && i + 1 < opts.length ? opts[i + 1] : void 0;
1636
+ }
1637
+ function getOpts(opts, name) {
1638
+ const out = [];
1639
+ for (let i = 0; i < opts.length - 1; i++) if (opts[i] === name) out.push(opts[i + 1]);
1640
+ return out;
1641
+ }
1642
+ function hasFlag(opts, name) {
1643
+ return opts.includes(name);
1644
+ }
1645
+ function parseTimeoutOpt(raw, name) {
1646
+ if (raw === void 0) return void 0;
1647
+ const n = Number(raw);
1648
+ if (!Number.isFinite(n) || n <= 0) die(`${name} must be a positive number of milliseconds (got "${raw}")`);
1649
+ return n;
1650
+ }
1651
+ function die(msg) {
1652
+ process.stderr.write(color.red(msg) + "\n");
1653
+ process.exit(2);
1654
+ }
1655
+ function timestamp() {
1656
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
1657
+ }
1658
+ function promptYesNo(question) {
1659
+ return new Promise((resolve) => {
1660
+ process.stderr.write(question);
1661
+ const onData = (chunk) => {
1662
+ process.stdin.pause();
1663
+ process.stdin.off("data", onData);
1664
+ resolve(/^y(es)?$/i.test(chunk.toString("utf8").trim()));
1665
+ };
1666
+ process.stdin.resume();
1667
+ process.stdin.once("data", onData);
1668
+ });
1669
+ }
1670
+ var HELP = `mcpgaze v${VERSION} \u2014 a transparent wiretap for MCP servers
1671
+
1672
+ USAGE
1673
+ mcpgaze wrap [--log <path>] [--print] [--redact] [--tui] [--native] -- <server command...>
1674
+ mcpgaze wrap-http (--upstream <url> | --route <prefix>=<url> ...) [--port <n>] [--host 127.0.0.1]
1675
+ [--forward-credentials | --creds-route <prefix> ... | --no-forward-credentials] [--redact]
1676
+ mcpgaze record [--cassette mcpgaze.cassette.json] [--log <path>] [--no-redact] -- <server command...>
1677
+ mcpgaze replay --cassette <file>
1678
+ mcpgaze snapshot [--out mcpgaze.baseline.json] -- <server command...>
1679
+ mcpgaze diff [--baseline <f>] [--fail-on <breaking|warning|any>] [--update] -- <server command...>
1680
+ mcpgaze conform [--spec <ver>|--all] [--json] -- <server command...>
1681
+ mcpgaze verify --cassette <file> [--fail-on <sev>] [--update] [--allow-tool-calls] -- <server command...>
1682
+ mcpgaze health [--interval <sec>] [--once] [--status <path>] -- <server command...>
1683
+ mcpgaze triage [--log <session.jsonl>] [--ai] [--yes] [--model <name>]
1684
+ mcpgaze preflight [--config <file> [--server <name>]] [--timeout <ms>] [-- <server command...>]
1685
+
1686
+ COMMANDS
1687
+ wrap Transparent stdio proxy; logs every JSON-RPC message to a side
1688
+ channel without touching the wire. --tui shows a live dashboard;
1689
+ --native uses the zero-overhead Rust single-binary proxy.
1690
+ wrap-http Same idea for Streamable HTTP: localhost-bound, Origin-checked.
1691
+ Routes by path prefix, so one proxy can front several upstreams
1692
+ (--route /a=URL --route /b=URL); --upstream is the single-route form.
1693
+ A single --upstream/--route forwards the client's Authorization/
1694
+ Cookie to that one upstream (one destination, nothing to misroute);
1695
+ add --no-forward-credentials to strip them. With MULTIPLE routes a
1696
+ token could reach the wrong upstream, so credentials are stripped
1697
+ unless a route opts in (--creds-route /a, or --forward-credentials).
1698
+ record Wrap a server and write a replayable cassette of req/res pairs.
1699
+ Secrets are redacted by default (it is a shareable artifact);
1700
+ pass --no-redact to capture params/results/stderr verbatim.
1701
+ replay Deterministic mock MCP server (stdio) from a cassette.
1702
+ snapshot Probe the server, write a tool-schema baseline you commit to git.
1703
+ diff Diff the live tool surface vs the baseline. --update accepts it.
1704
+ conform Spec-conformance suite across protocol versions.
1705
+ verify Re-issue recorded requests and diff RESPONSE SHAPES. --update
1706
+ re-baselines the cassette (accept intentional behavioral changes).
1707
+ Only read-only methods are re-issued unless --allow-tool-calls.
1708
+ health Continuously health-check a server (uptime, latency, drift), or
1709
+ --once as a cron/CI liveness probe (exit 0 up / 1 down).
1710
+ triage Read a session log, surface failures, and (with --ai) get a
1711
+ plain-English root-cause + fix from Claude.
1712
+ preflight Diagnose env vars a GUI client won't inherit; check config env.
1713
+
1714
+ EXAMPLES
1715
+ mcpgaze wrap --tui -- node server.js
1716
+ mcpgaze wrap --native -- node server.js
1717
+ mcpgaze health --interval 30 -- node server.js
1718
+ mcpgaze conform --all -- node server.js
1719
+ mcpgaze diff --update -- node server.js # accept the new tool surface
1720
+ mcpgaze verify --cassette s.json --update -- node server.js
1721
+ `;
1722
+ function findNativeProxy() {
1723
+ const fromEnv = process.env.MCPGAZE_PROXY_BIN;
1724
+ if (fromEnv && existsSync(fromEnv)) return fromEnv;
1725
+ const here = dirname3(fileURLToPath(import.meta.url));
1726
+ const candidates = [
1727
+ join(here, "..", "native", "mcpgaze-proxy", "target", "release", "mcpgaze-proxy"),
1728
+ join(here, "..", "..", "native", "mcpgaze-proxy", "target", "release", "mcpgaze-proxy")
1729
+ ];
1730
+ return candidates.find((p) => existsSync(p)) ?? null;
1731
+ }
1732
+ async function cmdWrap(args) {
1733
+ const { opts, cmd } = splitOnDoubleDash(args);
1734
+ if (cmd.length === 0) die("usage: mcpgaze wrap [--log <path>] [--print] [--tui] [--native] -- <server command...>");
1735
+ const logPath = getOpt(opts, "--log") ?? `.mcpgaze/session-${timestamp()}.jsonl`;
1736
+ if (hasFlag(opts, "--native")) {
1737
+ const bin = findNativeProxy();
1738
+ if (!bin) {
1739
+ process.stderr.write(color.yellow("[mcpgaze] native proxy not found; build it with `cargo build --release` in native/mcpgaze-proxy, or set MCPGAZE_PROXY_BIN. Falling back to the Node proxy.\n"));
1740
+ } else {
1741
+ const child = spawn3(bin, ["--log", logPath, "--", ...cmd], { stdio: "inherit" });
1742
+ child.on("exit", (code2) => process.exit(code2 ?? 0));
1743
+ return;
1744
+ }
1745
+ }
1746
+ const useTui = hasFlag(opts, "--tui");
1747
+ if (useTui && !Tui.isSupported()) {
1748
+ process.stderr.write(color.yellow("[mcpgaze] --tui needs a TTY; falling back to plain logging.\n"));
1749
+ }
1750
+ const tui = useTui && Tui.isSupported() ? new Tui(cmd.join(" ")) : null;
1751
+ const logger = new Logger({
1752
+ jsonlPath: logPath,
1753
+ pretty: !tui && hasFlag(opts, "--print"),
1754
+ onEvent: tui ? (ev) => tui.update(ev) : void 0,
1755
+ redact: hasFlag(opts, "--redact")
1756
+ });
1757
+ if (tui) {
1758
+ tui.start();
1759
+ const stop = () => {
1760
+ tui.stop();
1761
+ process.exit(0);
1762
+ };
1763
+ process.on("SIGINT", stop);
1764
+ process.on("SIGTERM", stop);
1765
+ } else {
1766
+ process.stderr.write(color.dim(`[mcpgaze] logging session to ${logPath}
1767
+ `));
1768
+ }
1769
+ const code = await runProxy({ command: cmd[0], args: cmd.slice(1), logger, mirrorStderr: !tui });
1770
+ if (tui) tui.stop();
1771
+ process.exit(code);
1772
+ }
1773
+ async function cmdWrapHttp(args) {
1774
+ const { opts } = splitOnDoubleDash(args);
1775
+ const upstream = getOpt(opts, "--upstream");
1776
+ const routeSpecs = getOpts(opts, "--route");
1777
+ if (!upstream && routeSpecs.length === 0) {
1778
+ die("usage: mcpgaze wrap-http (--upstream <url> | --route <prefix>=<url> ...) [--port <n>] [--host 127.0.0.1]");
1779
+ }
1780
+ const noForwardCredentials = hasFlag(opts, "--no-forward-credentials");
1781
+ if (noForwardCredentials && (hasFlag(opts, "--forward-credentials") || getOpts(opts, "--creds-route").length > 0)) {
1782
+ die("--no-forward-credentials cannot be combined with --forward-credentials or --creds-route");
1783
+ }
1784
+ let routes;
1785
+ try {
1786
+ routes = buildRoutes(upstream, routeSpecs, {
1787
+ forwardCredentials: hasFlag(opts, "--forward-credentials"),
1788
+ credsPrefixes: getOpts(opts, "--creds-route"),
1789
+ noForwardCredentials
1790
+ });
1791
+ } catch (e) {
1792
+ die(e.message);
1793
+ }
1794
+ const host = getOpt(opts, "--host") ?? "127.0.0.1";
1795
+ const port = Number(getOpt(opts, "--port") ?? "0");
1796
+ const logPath = getOpt(opts, "--log") ?? `.mcpgaze/session-${timestamp()}.jsonl`;
1797
+ const allow = getOpt(opts, "--allow-origin");
1798
+ const logger = new Logger({ jsonlPath: logPath, pretty: hasFlag(opts, "--print"), redact: hasFlag(opts, "--redact") });
1799
+ const handle = await runHttpProxy({
1800
+ routes,
1801
+ host,
1802
+ port,
1803
+ logger,
1804
+ allowedOrigins: allow ? allow.split(",") : void 0
1805
+ });
1806
+ process.stderr.write(color.green(`[mcpgaze] proxy listening on http://${host}:${handle.port}`) + "\n");
1807
+ for (const r of routes) {
1808
+ process.stderr.write(color.dim(` ${r.prefix.padEnd(12)} \u2192 ${r.upstream}
1809
+ `));
1810
+ }
1811
+ process.stderr.write(color.dim(`[mcpgaze] logging to ${logPath}
1812
+ `));
1813
+ if (host !== "127.0.0.1" && host !== "localhost") {
1814
+ process.stderr.write(color.yellow(`[mcpgaze] warning: binding to ${host} exposes the proxy beyond localhost
1815
+ `));
1816
+ }
1817
+ const shutdown = () => void handle.close().then(() => process.exit(0));
1818
+ process.on("SIGINT", shutdown);
1819
+ process.on("SIGTERM", shutdown);
1820
+ }
1821
+ async function cmdRecord(args) {
1822
+ const { opts, cmd } = splitOnDoubleDash(args);
1823
+ if (cmd.length === 0) die("usage: mcpgaze record [--cassette <path>] -- <server command...>");
1824
+ const cassettePath = getOpt(opts, "--cassette") ?? "mcpgaze.cassette.json";
1825
+ const logPath = getOpt(opts, "--log") ?? `.mcpgaze/session-${timestamp()}.jsonl`;
1826
+ const redact = !hasFlag(opts, "--no-redact");
1827
+ const logger = new Logger({ jsonlPath: logPath, pretty: hasFlag(opts, "--print"), redact });
1828
+ const recorder = new CassetteRecorder(redact);
1829
+ process.stderr.write(color.dim(`[mcpgaze] recording cassette to ${cassettePath}
1830
+ `));
1831
+ process.stderr.write(
1832
+ redact ? color.dim("[mcpgaze] secret redaction ON (cassette + log); pass --no-redact to capture verbatim\n") : color.yellow("[mcpgaze] --no-redact: secrets in params/results/stderr will be stored verbatim\n")
1833
+ );
1834
+ const code = await runProxy({
1835
+ command: cmd[0],
1836
+ args: cmd.slice(1),
1837
+ logger,
1838
+ onInteraction: ({ request, response }) => recorder.add(request, response)
1839
+ });
1840
+ const n = recorder.write(cassettePath);
1841
+ process.stderr.write(color.green(`[mcpgaze] wrote ${cassettePath} \u2014 ${n} interaction(s)
1842
+ `));
1843
+ process.exit(code);
1844
+ }
1845
+ async function cmdReplay(args) {
1846
+ const { opts } = splitOnDoubleDash(args);
1847
+ const cassettePath = getOpt(opts, "--cassette");
1848
+ if (!cassettePath) die("usage: mcpgaze replay --cassette <file>");
1849
+ const code = await runReplayServer(cassettePath);
1850
+ process.exit(code);
1851
+ }
1852
+ async function cmdPreflight(args) {
1853
+ const { opts, cmd } = splitOnDoubleDash(args);
1854
+ const configPath = getOpt(opts, "--config");
1855
+ if (configPath) {
1856
+ const findings = checkConfigEnv(configPath, getOpt(opts, "--server"));
1857
+ if (findings.length === 0) {
1858
+ process.stdout.write(color.green("\u2713 config env block looks clean\n"));
1859
+ } else {
1860
+ for (const f of findings) {
1861
+ const badge = f.level === "error" ? color.red("ERROR ") : color.yellow("WARNING");
1862
+ process.stdout.write(` ${badge} ${color.bold(f.key)} \u2014 ${f.message}
1863
+ `);
1864
+ }
1865
+ }
1866
+ if (cmd.length === 0) return;
1867
+ }
1868
+ if (cmd.length === 0) die("usage: mcpgaze preflight [--config <file>] [--timeout <ms>] [-- <server command...>]");
1869
+ const timeoutMs = parseTimeoutOpt(getOpt(opts, "--timeout") ?? process.env.MCPGAZE_PREFLIGHT_TIMEOUT, "--timeout");
1870
+ process.stderr.write(color.dim("[mcpgaze] probing with full env, then with the GUI-inherited subset\u2026\n"));
1871
+ const r = await preflight(cmd[0], cmd.slice(1), timeoutMs !== void 0 ? { timeoutMs } : {});
1872
+ if (!r.fullEnvOk) {
1873
+ process.stdout.write(color.red("\u2717 the server failed to start even with your full environment.\n"));
1874
+ if (r.fullError) process.stdout.write(color.dim(` ${r.fullError.split("\n")[0]}
1875
+ `));
1876
+ process.exit(1);
1877
+ }
1878
+ if (r.restrictedEnvOk) {
1879
+ process.stdout.write(color.green("\u2713 starts cleanly with only the env a GUI client inherits \u2014 no env surprises.\n"));
1880
+ return;
1881
+ }
1882
+ process.stdout.write(
1883
+ color.yellow("\u26A0 starts with your full shell env but FAILS with only what a GUI client inherits.\n") + " Your server likely depends on env vars Claude Desktop (and similar) won't pass.\n"
1884
+ );
1885
+ if (r.suspectVars.length) {
1886
+ process.stdout.write(color.bold("\n Likely culprits (set in your shell, not inherited by GUI clients):\n"));
1887
+ for (const v of r.suspectVars.slice(0, 15)) process.stdout.write(` \u2022 ${v}
1888
+ `);
1889
+ process.stdout.write(
1890
+ color.dim(`
1891
+ Fix: pass them explicitly in your client config's "env" block (literal values, not $VARS).
1892
+ `)
1893
+ );
1894
+ }
1895
+ process.exit(1);
1896
+ }
1897
+ async function cmdSnapshot(args) {
1898
+ const { opts, cmd } = splitOnDoubleDash(args);
1899
+ if (cmd.length === 0) die("usage: mcpgaze snapshot [--out <path>] -- <server command...>");
1900
+ const outPath = getOpt(opts, "--out") ?? "mcpgaze.baseline.json";
1901
+ const baseline = await snapshot(cmd[0], cmd.slice(1), outPath);
1902
+ const n = Object.keys(baseline.tools).length;
1903
+ process.stdout.write(
1904
+ color.green(`\u2713 wrote ${outPath}`) + ` \u2014 ${n} tool${n === 1 ? "" : "s"} from ${baseline.server.name ?? "server"} ` + color.dim(`(protocol ${baseline.protocolVersion})`) + "\n"
1905
+ );
1906
+ }
1907
+ function thresholdMet(worst, failOn) {
1908
+ if (!failOn) return false;
1909
+ if (worst === null) return false;
1910
+ const rank = { info: 0, warning: 1, breaking: 2 };
1911
+ const want = failOn === "any" ? 0 : failOn === "warning" ? 1 : 2;
1912
+ return rank[worst] >= want;
1913
+ }
1914
+ async function cmdDiff(args) {
1915
+ const { opts, cmd } = splitOnDoubleDash(args);
1916
+ if (cmd.length === 0) die("usage: mcpgaze diff [--baseline <path>] [--fail-on <sev>] [--update] -- <server command...>");
1917
+ const baselinePath = getOpt(opts, "--baseline") ?? "mcpgaze.baseline.json";
1918
+ const failOn = hasFlag(opts, "--fail-on-drift") ? "breaking" : getOpt(opts, "--fail-on");
1919
+ if (hasFlag(opts, "--update")) {
1920
+ const baseline = await snapshot(cmd[0], cmd.slice(1), baselinePath);
1921
+ const n = Object.keys(baseline.tools).length;
1922
+ process.stdout.write(color.green(`\u2713 baseline updated`) + ` \u2014 ${n} tool(s) accepted into ${baselinePath}
1923
+ `);
1924
+ return;
1925
+ }
1926
+ const result = await diff(cmd[0], cmd.slice(1), baselinePath);
1927
+ if (result.changes.length === 0) {
1928
+ process.stdout.write(color.green("\u2713 no drift \u2014 tool surface matches the baseline\n"));
1929
+ return;
1930
+ }
1931
+ const badge = {
1932
+ breaking: color.red("BREAKING"),
1933
+ warning: color.yellow("WARNING "),
1934
+ info: color.blue("INFO ")
1935
+ };
1936
+ process.stdout.write(color.bold(`Found ${result.changes.length} change(s):
1937
+ `));
1938
+ for (const c of result.changes) {
1939
+ process.stdout.write(` ${badge[c.severity]} ${color.bold(c.path)} \u2014 ${c.message}
1940
+ `);
1941
+ }
1942
+ const worst = worstSeverity(result.changes);
1943
+ if (thresholdMet(worst, failOn)) {
1944
+ process.stderr.write(color.red(`
1945
+ \u2717 drift at/above '${failOn}' \u2014 failing.
1946
+ `));
1947
+ process.exit(1);
1948
+ }
1949
+ }
1950
+ var SEV_BADGE = {
1951
+ breaking: color.red("BREAKING"),
1952
+ warning: color.yellow("WARNING "),
1953
+ info: color.blue("INFO ")
1954
+ };
1955
+ async function cmdConform(args) {
1956
+ const { opts, cmd } = splitOnDoubleDash(args);
1957
+ if (cmd.length === 0) die("usage: mcpgaze conform [--spec <ver>|--all] [--json] -- <server command...>");
1958
+ const specs = hasFlag(opts, "--all") ? [...KNOWN_SPEC_VERSIONS] : [getOpt(opts, "--spec") ?? KNOWN_SPEC_VERSIONS[0]];
1959
+ const reports = [];
1960
+ for (const spec of specs) reports.push(await conform(cmd[0], cmd.slice(1), spec));
1961
+ if (hasFlag(opts, "--json")) {
1962
+ process.stdout.write(JSON.stringify(reports, null, 2) + "\n");
1963
+ } else {
1964
+ const mark = {
1965
+ pass: color.green("\u2713"),
1966
+ fail: color.red("\u2717"),
1967
+ warn: color.yellow("\u26A0"),
1968
+ skip: color.dim("\u2218")
1969
+ };
1970
+ for (const r of reports) {
1971
+ process.stdout.write(
1972
+ color.bold(`
1973
+ Protocol ${r.protocolVersion}`) + color.dim(` (server reports ${r.serverProtocolVersion ?? "?"})
1974
+ `)
1975
+ );
1976
+ for (const c of r.results) {
1977
+ const lvl = c.level === "required" ? "" : color.dim(" (recommended)");
1978
+ process.stdout.write(` ${mark[c.status]} ${c.title}${lvl} \u2014 ${color.dim(c.detail)}
1979
+ `);
1980
+ }
1981
+ process.stdout.write(r.passed ? color.green(" PASS\n") : color.red(" FAIL\n"));
1982
+ }
1983
+ }
1984
+ if (reports.some((r) => !r.passed)) process.exit(1);
1985
+ }
1986
+ async function cmdVerify(args) {
1987
+ const { opts, cmd } = splitOnDoubleDash(args);
1988
+ const cassette = getOpt(opts, "--cassette");
1989
+ if (!cassette || cmd.length === 0) die("usage: mcpgaze verify --cassette <file> [--update] [--allow-tool-calls] -- <server command...>");
1990
+ const allowToolCalls = hasFlag(opts, "--allow-tool-calls");
1991
+ if (hasFlag(opts, "--update")) {
1992
+ const n = await updateCassette(cmd[0], cmd.slice(1), cassette, void 0, { allowToolCalls });
1993
+ process.stdout.write(color.green(`\u2713 cassette re-baselined`) + ` \u2014 ${n} response(s) accepted into ${cassette}
1994
+ `);
1995
+ return;
1996
+ }
1997
+ const failOn = getOpt(opts, "--fail-on");
1998
+ const r = await verify(cmd[0], cmd.slice(1), cassette, void 0, { allowToolCalls });
1999
+ process.stdout.write(color.dim(`re-issued ${r.checked} recorded request(s)
2000
+ `));
2001
+ if (r.skipped.length > 0) {
2002
+ const uniq = [...new Set(r.skipped)];
2003
+ process.stdout.write(
2004
+ color.dim(`skipped ${r.skipped.length} state-changing request(s) [${uniq.join(", ")}] \u2014 pass --allow-tool-calls to re-issue
2005
+ `)
2006
+ );
2007
+ }
2008
+ for (const e of r.errors) process.stdout.write(` ${color.red("ERROR ")} ${color.bold(e.method)} \u2014 ${e.message}
2009
+ `);
2010
+ if (r.changes.length === 0 && r.errors.length === 0) {
2011
+ process.stdout.write(color.green("\u2713 no behavioral drift \u2014 response shapes match the cassette\n"));
2012
+ return;
2013
+ }
2014
+ for (const c of r.changes) {
2015
+ process.stdout.write(` ${SEV_BADGE[c.severity]} ${color.bold(c.path)} \u2014 ${c.message}
2016
+ `);
2017
+ }
2018
+ const worst = worstSeverity(r.changes);
2019
+ if (thresholdMet(worst, failOn) || failOn && r.errors.length > 0) {
2020
+ process.stderr.write(color.red(`
2021
+ \u2717 behavioral drift at/above '${failOn}' \u2014 failing.
2022
+ `));
2023
+ process.exit(1);
2024
+ }
2025
+ }
2026
+ async function cmdHealth(args) {
2027
+ const { opts, cmd } = splitOnDoubleDash(args);
2028
+ if (cmd.length === 0) die("usage: mcpgaze health [--interval <sec>] [--once] [--status <path>] -- <server command...>");
2029
+ const once = hasFlag(opts, "--once");
2030
+ const intervalMs = Number(getOpt(opts, "--interval") ?? "60") * 1e3;
2031
+ const statusPath = getOpt(opts, "--status") ?? ".mcpgaze/health.json";
2032
+ if (!once) {
2033
+ process.stderr.write(
2034
+ color.dim(`[mcpgaze] health-checking every ${intervalMs / 1e3}s \xB7 status \u2192 ${statusPath} \xB7 Ctrl-C to stop
2035
+ `)
2036
+ );
2037
+ }
2038
+ const last = await runHealthDaemon(cmd[0], cmd.slice(1), {
2039
+ once,
2040
+ intervalMs,
2041
+ statusPath,
2042
+ onCheck: (c) => {
2043
+ if (!once) {
2044
+ const stamp = color.dim(new Date(c.at).toLocaleTimeString());
2045
+ process.stderr.write(
2046
+ c.ok ? `${stamp} ${color.green("UP")} ${c.toolCount} tools \xB7 ${c.latencyMs}ms
2047
+ ` : `${stamp} ${color.red("DOWN")} ${c.error ?? ""}
2048
+ `
2049
+ );
2050
+ }
2051
+ },
2052
+ onTransition: (msg) => process.stderr.write(color.bold(` \u27EB ${msg}
2053
+ `))
2054
+ });
2055
+ if (once) {
2056
+ process.stdout.write(
2057
+ last.ok ? color.green(`\u2713 UP`) + ` \u2014 ${last.toolCount} tools, ${last.latencyMs}ms
2058
+ ` : color.red(`\u2717 DOWN`) + ` \u2014 ${last.error ?? "unresponsive"}
2059
+ `
2060
+ );
2061
+ process.exit(last.ok ? 0 : 1);
2062
+ }
2063
+ }
2064
+ async function cmdTriage(args) {
2065
+ const { opts } = splitOnDoubleDash(args);
2066
+ const logPath = getOpt(opts, "--log");
2067
+ if (!logPath) die("usage: mcpgaze triage --log <session.jsonl> [--ai] [--yes] [--model <name>]");
2068
+ const preConsented = hasFlag(opts, "--yes");
2069
+ const report = await triage(logPath, {
2070
+ useAi: hasFlag(opts, "--ai"),
2071
+ apiKey: process.env.ANTHROPIC_API_KEY,
2072
+ model: getOpt(opts, "--model") ?? process.env.MCPGAZE_TRIAGE_MODEL,
2073
+ // --ai egresses (redacted) failure details to api.anthropic.com. Require an
2074
+ // explicit acknowledgement: --yes, or a "y" at the interactive preview.
2075
+ confirmEgress: async (prompt) => {
2076
+ if (preConsented) return true;
2077
+ if (!process.stdin.isTTY) return false;
2078
+ process.stderr.write(
2079
+ color.yellow("\n[mcpgaze] triage --ai will POST these (redacted) failure details to api.anthropic.com:\n") + color.dim(prompt.split("\n").map((l) => " " + l).join("\n") + "\n")
2080
+ );
2081
+ return await promptYesNo("Send to Anthropic? [y/N] ");
2082
+ }
2083
+ });
2084
+ if (report.failures.length === 0) {
2085
+ process.stdout.write(color.green("\u2713 no failure signals in this session\n"));
2086
+ return;
2087
+ }
2088
+ process.stdout.write(color.bold(`Found ${report.failures.length} failure signal(s):
2089
+ `));
2090
+ for (const f of report.failures) {
2091
+ process.stdout.write(` ${color.red("\u2022")} ${color.bold(f.kind)} \u2014 ${f.summary}
2092
+ `);
2093
+ if (f.detail) process.stdout.write(color.dim(` ${f.detail}
2094
+ `));
2095
+ }
2096
+ if (report.aiDiagnosis) {
2097
+ process.stdout.write(color.bold("\n\u2500\u2500 Claude's triage \u2500\u2500\n") + report.aiDiagnosis + "\n");
2098
+ } else if (report.aiSkippedReason) {
2099
+ process.stdout.write(color.dim(`
2100
+ (AI triage skipped: ${report.aiSkippedReason})
2101
+ `));
2102
+ }
2103
+ }
2104
+ async function main() {
2105
+ const [, , command, ...rest] = process.argv;
2106
+ switch (command) {
2107
+ case "wrap":
2108
+ return cmdWrap(rest);
2109
+ case "wrap-http":
2110
+ return cmdWrapHttp(rest);
2111
+ case "record":
2112
+ return cmdRecord(rest);
2113
+ case "replay":
2114
+ return cmdReplay(rest);
2115
+ case "snapshot":
2116
+ return cmdSnapshot(rest);
2117
+ case "diff":
2118
+ return cmdDiff(rest);
2119
+ case "conform":
2120
+ return cmdConform(rest);
2121
+ case "verify":
2122
+ return cmdVerify(rest);
2123
+ case "triage":
2124
+ return cmdTriage(rest);
2125
+ case "health":
2126
+ return cmdHealth(rest);
2127
+ case "preflight":
2128
+ return cmdPreflight(rest);
2129
+ case "-v":
2130
+ case "--version":
2131
+ process.stdout.write(VERSION + "\n");
2132
+ return;
2133
+ case void 0:
2134
+ case "-h":
2135
+ case "--help":
2136
+ case "help":
2137
+ process.stdout.write(HELP);
2138
+ return;
2139
+ default:
2140
+ die(`unknown command: ${command}
2141
+
2142
+ ${HELP}`);
2143
+ }
2144
+ }
2145
+ main().catch((e) => {
2146
+ process.stderr.write(color.red(`error: ${e.message}`) + "\n");
2147
+ process.exit(1);
2148
+ });