@modelcontextprotocol/inspector 2.14.0 → 2.15.0

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as withDefaultCatalogPath, A as resolveLoadOptions, B as assertSocketPathWithinLimit, C as clearAllOAuthClientState, D as loadSummaryWarnings, E as LOAD_FLAG_CONFLICT, F as generateDaemonToken, G as getDaemonSocketPath, H as ensureDaemonDir, I as getDaemonTokenFromEnv, J as loadServerEntries, K as readInspectorVersion, L as readDaemonTokenFile, M as sanitizeDeep, N as sanitizeText, O as positiveIntegerParser, P as encodeRequest, Q as resolveServerSource, R as DAEMON_DIR_ENV, S as NodeOAuthStorage, T as resetNodeOAuthStorageCache, U as getDaemonDir, V as createPrivateDaemonDir, W as getDaemonLogPath, X as parseHeaderPair, Y as selectServerEntry, Z as parseKeyValuePair, _ as isCliAutoOpenForced, _t as awaitableError, a as obtainPendingUrlForKey, at as readOAuthStore, b as DEFAULT_CONNECT_TIMEOUT_MS, c as runAuthHelper, ct as setSecretStorageWarningsQuiet, d as emaLogin, dt as CallbackNavigation, et as DEFAULT_MAX_FETCH_REQUESTS, f as emaLogout, ft as clearEmaIdpSession, g as runEmaIdpInteractiveFlow, gt as parseIdpOAuthStorageKey, h as requireIdp, ht as normalizeIdpIssuer, i as obtainPendingAuthUrl, it as handleError, j as isSafeLinkTarget, k as positiveNumberParser, l as writePendingAuthMarker, lt as warnAboutSecretStorage, m as loadEmaIdpConfig, mt as idpOAuthStorageKey, n as AUTH_HELPER_COMMAND, nt as CliExitCodeError, o as pendingAuthMarkerPath, ot as disallowMemorySecretStoreFallback, p as getEmaStatus, pt as getEmaIdpLoginState, q as readInspectorVersionSafe, r as PENDING_AUTH_TTL_MS, rt as EXIT_CODES, s as removeOwnPendingAuthMarker, st as getSecretStorageInfo, tt as DEFAULT_TASK_TTL_MS, u as authorizeInFrontend, ut as InMemorySecretStore, v as PLAIN, vt as awaitableLog, w as getStateFilePath, x as withConnectTimeout, y as styleFromOpts, z as DAEMON_TOKEN_ENV } from "./server-Dv339Ejx.js";
2
+ import { $ as getDaemonTokenFromEnv, A as resolveLoadOptions, At as CallbackNavigation, B as LOAD_SCENARIO_INVALID, C as clearAllOAuthClientState, Ct as EXIT_CODES, D as LOAD_FLAG_CONFLICT, Dt as setSecretStorageWarningsQuiet, E as MAX_REQUEST_LINE_BYTES, Et as getSecretStorageInfo, F as maxP99Parser, Ft as parseIdpOAuthStorageKey, G as loadArgsError, H as parseLoadScenario, I as minThroughputParser, It as decodeJwtPayload, J as sanitizeDeep, K as parseLoadArgsRows, L as resolveLoadThresholds, Lt as awaitableError, M as fractionParser, Mt as getEmaIdpLoginState, N as maxP50Parser, Nt as idpOAuthStorageKey, O as formatLoadProgress, Ot as warnAboutSecretStorage, P as maxP95Parser, Pt as normalizeIdpIssuer, Q as generateDaemonToken, R as positiveIntegerParser, Rt as awaitableLog, S as NodeOAuthStorage, St as CliExitCodeError, T as resetNodeOAuthStorageCache, Tt as disallowMemorySecretStoreFallback, U as LOAD_ARGS_INVALID, V as loadScenarioError, W as assertLoadArgsTemplate, X as encodeRequest, Y as sanitizeText, Z as generateDaemonNonce, _ as isCliAutoOpenForced, _t as resolveServerSource, a as obtainPendingUrlForKey, at as createPrivateDaemonDir, b as DEFAULT_CONNECT_TIMEOUT_MS, bt as DEFAULT_TASK_TTL_MS, c as runAuthHelper, ct as getDaemonLogPath, d as emaLogin, dt as readInspectorVersion, et as readDaemonTokenFile, f as emaLogout, ft as readInspectorVersionSafe, g as runEmaIdpInteractiveFlow, gt as parseKeyValuePair, h as requireIdp, ht as parseHeaderPair, i as obtainPendingAuthUrl, it as assertSocketPathWithinLimit, j as checkLoadThresholds, jt as clearEmaIdpSession, k as loadSummaryWarnings, kt as InMemorySecretStore, l as writePendingAuthMarker, lt as getDaemonSocketPath, m as loadEmaIdpConfig, mt as selectServerEntry, n as AUTH_HELPER_COMMAND, nt as DAEMON_DIR_ENV, o as pendingAuthMarkerPath, ot as ensureDaemonDir, p as getEmaStatus, pt as loadServerEntries, q as isSafeLinkTarget, r as PENDING_AUTH_TTL_MS, rt as DAEMON_TOKEN_ENV, s as removeOwnPendingAuthMarker, st as getDaemonDir, tt as verifyDaemonProof, u as authorizeInFrontend, ut as getDaemonTokenPath, v as PLAIN, vt as withDefaultCatalogPath, w as getStateFilePath, wt as readOAuthStore, x as withConnectTimeout, xt as handleError, y as styleFromOpts, yt as DEFAULT_MAX_FETCH_REQUESTS, z as positiveNumberParser } from "./server-B-CUVIBj.js";
3
3
  import { realpathSync } from "fs";
4
4
  import { resolve } from "path";
5
5
  import { fileURLToPath } from "url";
@@ -8,41 +8,95 @@ import path, * as path$1 from "node:path";
8
8
  import * as fs from "node:fs";
9
9
  import nodeFs, { existsSync as existsSync$1, readFileSync as readFileSync$1 } from "node:fs";
10
10
  import { randomBytes, randomUUID } from "node:crypto";
11
+ import * as net from "node:net";
11
12
  import { Command } from "commander";
12
13
  import { fileURLToPath as fileURLToPath$1 } from "node:url";
13
14
  import { getDefaultEnvironment } from "@modelcontextprotocol/client/stdio";
14
- import * as net from "node:net";
15
15
  import { spawn } from "node:child_process";
16
16
  import * as readline from "node:readline";
17
17
  import { createInterface as createInterface$1 } from "node:readline/promises";
18
- //#region src/daemon/client.ts
18
+ // ../../core/cli/utils/prompt-args.ts
19
+ function parsePromptArgPair(pair, previous = {}) {
20
+ const eq = pair.indexOf("=");
21
+ if (eq <= 0) throw new Error(`Invalid parameter format: ${pair}. Use key=value format.`);
22
+ return {
23
+ ...previous,
24
+ [pair.slice(0, eq)]: pair.slice(eq + 1)
25
+ };
26
+ }
27
+ // src/daemon/handshake.ts
28
+ function createDaemonHello() {
29
+ const id = randomUUID();
30
+ const nonce = generateDaemonNonce();
31
+ return {
32
+ id,
33
+ nonce,
34
+ line: encodeRequest({
35
+ id,
36
+ op: "hello",
37
+ params: { nonce }
38
+ })
39
+ };
40
+ }
41
+ function daemonUnverifiedError(socketPath) {
42
+ return new CliExitCodeError(EXIT_CODES.UNREACHABLE, `Could not verify the connection daemon at ${socketPath}: it did not prove it holds this user's daemon token, so nothing was sent to it. Either MCP_INSPECTOR_DAEMON_TOKEN does not match the running daemon's token, or a daemon from an older mcpdo release is still running (end the \`mcpdod\` process, then re-run the command), or another process is impersonating the daemon.`, { code: "daemon_unverified" });
43
+ }
44
+ function daemonTokenMissingError(tokenPath) {
45
+ return new CliExitCodeError(EXIT_CODES.UNREACHABLE, `No connection daemon token found (${tokenPath}), so the daemon cannot be verified and nothing was sent. Re-run the command to start a fresh daemon; if this persists, another process may be holding the daemon endpoint.`, { code: "daemon_unverified" });
46
+ }
47
+ function checkDaemonHelloLine(line, hello, token, socketPath) {
48
+ if (token === void 0) return daemonUnverifiedError(socketPath);
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(line);
52
+ } catch {
53
+ return daemonUnverifiedError(socketPath);
54
+ }
55
+ if (parsed === null || typeof parsed !== "object") return daemonUnverifiedError(socketPath);
56
+ const response = parsed;
57
+ if (response.id !== hello.id || response.ok !== true) return daemonUnverifiedError(socketPath);
58
+ const result = response.result;
59
+ if (!verifyDaemonProof(token, hello.nonce, result?.proof)) return daemonUnverifiedError(socketPath);
60
+ }
61
+ // src/daemon/client.ts
19
62
  function daemonTokenDir(options) {
20
63
  if (options.dir !== void 0) return options.dir;
21
64
  if (options.socketPath !== void 0 && !options.socketPath.startsWith("\\\\.\\pipe\\")) return path$1.dirname(options.socketPath);
22
65
  return getDaemonDir();
23
66
  }
67
+ function resolveDaemonToken(options) {
68
+ const dir = daemonTokenDir(options);
69
+ return {
70
+ token: options.token ?? getDaemonTokenFromEnv() ?? readDaemonTokenFile(dir),
71
+ tokenPath: getDaemonTokenPath(dir)
72
+ };
73
+ }
24
74
  function relayedEnvelope(error) {
25
75
  return {
26
76
  code: error.code,
27
77
  ...error.status !== void 0 && { status: error.status },
28
78
  ...error.method !== void 0 && { method: error.method },
29
79
  ...error.retryAfter !== void 0 && { retryAfter: sanitizeText(error.retryAfter) },
30
- ...error.retryAfterMs !== void 0 && { retryAfterMs: error.retryAfterMs }
80
+ ...error.retryAfterMs !== void 0 && { retryAfterMs: error.retryAfterMs },
81
+ ...error.oauthError !== void 0 && { oauthError: sanitizeText(error.oauthError) },
82
+ ...error.oauthErrorDescription !== void 0 && { oauthErrorDescription: sanitizeText(error.oauthErrorDescription) }
31
83
  };
32
84
  }
33
85
  async function callDaemon(op, params, options = {}) {
34
86
  const socketPath = options.socketPath ?? getDaemonSocketPath();
35
87
  const timeoutMs = options.timeoutMs ?? 6e4;
36
88
  const id = randomUUID();
37
- const token = options.token ?? getDaemonTokenFromEnv() ?? readDaemonTokenFile(daemonTokenDir(options));
89
+ const { token, tokenPath } = resolveDaemonToken(options);
38
90
  const request = {
39
91
  id,
40
92
  op,
41
- params
93
+ params,
94
+ token
42
95
  };
43
- if (token !== void 0) request.token = token;
96
+ const hello = createDaemonHello();
44
97
  return new Promise((resolve, reject) => {
45
98
  let settled = false;
99
+ let verified = false;
46
100
  let buffer = "";
47
101
  let queue = Promise.resolve();
48
102
  let timer;
@@ -76,6 +130,15 @@ async function callDaemon(op, params, options = {}) {
76
130
  function handleLine(line) {
77
131
  const trimmed = line.trim();
78
132
  if (!trimmed) return Promise.resolve();
133
+ if (!verified) {
134
+ const error = checkDaemonHelloLine(trimmed, hello, token, socketPath);
135
+ if (error) fail(error);
136
+ else {
137
+ verified = true;
138
+ socket.write(encodeRequest(request));
139
+ }
140
+ return Promise.resolve();
141
+ }
79
142
  let parsed;
80
143
  try {
81
144
  parsed = JSON.parse(trimmed);
@@ -84,6 +147,11 @@ async function callDaemon(op, params, options = {}) {
84
147
  return Promise.resolve();
85
148
  }
86
149
  if (parsed !== null && typeof parsed === "object" && "kind" in parsed && parsed.kind === "elicitation-request") return handleElicitationRequest(parsed);
150
+ if (parsed !== null && typeof parsed === "object" && "kind" in parsed && parsed.kind === "load-progress") {
151
+ const frame = parsed;
152
+ if (frame.id === id) options.onLoadProgress?.(frame.progress);
153
+ return Promise.resolve();
154
+ }
87
155
  handleResponse(parsed);
88
156
  return Promise.resolve();
89
157
  }
@@ -122,7 +190,11 @@ async function callDaemon(op, params, options = {}) {
122
190
  if (options.signal?.aborted) onAbort();
123
191
  else options.signal?.addEventListener("abort", onAbort, { once: true });
124
192
  socket.once("connect", () => {
125
- socket.write(encodeRequest(request));
193
+ if (token === void 0) {
194
+ fail(daemonTokenMissingError(tokenPath));
195
+ return;
196
+ }
197
+ socket.write(hello.line);
126
198
  });
127
199
  socket.on("data", (chunk) => {
128
200
  buffer += String(chunk);
@@ -136,21 +208,22 @@ async function callDaemon(op, params, options = {}) {
136
208
  if (!settled) socket.connect(socketPath);
137
209
  });
138
210
  }
139
- //#endregion
140
- //#region src/daemon/stream-client.ts
211
+ // src/daemon/stream-client.ts
141
212
  async function streamDaemon(params, options) {
142
213
  const socketPath = options.socketPath ?? getDaemonSocketPath();
143
214
  const timeoutMs = options.timeoutMs ?? 6e4;
144
215
  const id = randomUUID();
145
- const token = options.token ?? getDaemonTokenFromEnv() ?? readDaemonTokenFile(daemonTokenDir(options));
216
+ const { token, tokenPath } = resolveDaemonToken(options);
146
217
  const request = {
147
218
  id,
148
219
  op: "stream",
149
- params
220
+ params,
221
+ token
150
222
  };
151
- if (token !== void 0) request.token = token;
223
+ const hello = createDaemonHello();
152
224
  return new Promise((resolve, reject) => {
153
225
  let settled = false;
226
+ let verified = false;
154
227
  let buffer = "";
155
228
  let streaming = false;
156
229
  let pendingCallbacks = 0;
@@ -184,6 +257,16 @@ async function streamDaemon(params, options) {
184
257
  function handleLine(line) {
185
258
  const trimmed = line.trim();
186
259
  if (!trimmed) return;
260
+ if (!verified) {
261
+ const error = checkDaemonHelloLine(trimmed, hello, token, socketPath);
262
+ if (error) {
263
+ fail(error);
264
+ return;
265
+ }
266
+ verified = true;
267
+ socket.write(encodeRequest(request));
268
+ return;
269
+ }
187
270
  if (!streaming) {
188
271
  let response;
189
272
  try {
@@ -247,7 +330,11 @@ async function streamDaemon(params, options) {
247
330
  if (options.signal?.aborted) onAbort();
248
331
  else options.signal?.addEventListener("abort", onAbort, { once: true });
249
332
  socket.once("connect", () => {
250
- socket.write(encodeRequest(request));
333
+ if (token === void 0) {
334
+ fail(daemonTokenMissingError(tokenPath));
335
+ return;
336
+ }
337
+ socket.write(hello.line);
251
338
  });
252
339
  socket.on("data", (chunk) => {
253
340
  buffer += String(chunk);
@@ -261,8 +348,7 @@ async function streamDaemon(params, options) {
261
348
  if (!settled) socket.connect(socketPath);
262
349
  });
263
350
  }
264
- //#endregion
265
- //#region src/daemon/ensure.ts
351
+ // src/daemon/ensure.ts
266
352
  const READY_TIMEOUT_MS = 1e4;
267
353
  const READY_POLL_MS = 50;
268
354
  function resolveDaemonScriptPath() {
@@ -363,6 +449,7 @@ async function ensureDaemon(options) {
363
449
  token ??= readDaemonTokenFile(dir);
364
450
  const pong = await callDaemon("ping", {}, {
365
451
  socketPath,
452
+ dir,
366
453
  timeoutMs: 2e3,
367
454
  token
368
455
  });
@@ -409,8 +496,7 @@ async function ensureDaemon(options) {
409
496
  spawned: true
410
497
  };
411
498
  }
412
- //#endregion
413
- //#region ../../core/cli/handlers/servers-list.ts
499
+ // ../../core/cli/handlers/servers-list.ts
414
500
  function resolveServerListSource(serverOptions = {}) {
415
501
  const source = resolveServerSource(withDefaultCatalogPath(serverOptions));
416
502
  if (!source) return null;
@@ -524,8 +610,7 @@ function isSensitiveHeader(key) {
524
610
  const k = key.toLowerCase();
525
611
  return k.includes("auth") || k.includes("cookie") || k.includes("secret") || k.includes("token") || k.includes("password") || k.includes("api-key") || k.includes("apikey");
526
612
  }
527
- //#endregion
528
- //#region ../../core/cli/handlers/method-types.ts
613
+ // ../../core/cli/handlers/method-types.ts
529
614
  const CONNECTION_RPC_METHODS = [
530
615
  "tools/list",
531
616
  "tools/call",
@@ -550,58 +635,369 @@ const CONNECTION_RPC_METHODS = [
550
635
  "skills/list",
551
636
  "skills/get"
552
637
  ];
553
- //#endregion
554
- //#region ../../core/cli/handlers/load-thresholds.ts
555
- const LOAD_ERROR_RATE_EXCEEDED = "load_error_rate_exceeded";
556
- const LOAD_P99_EXCEEDED = "load_p99_exceeded";
557
- function fractionParser(flag) {
558
- return (value) => {
559
- const n = value.trim() === "" ? NaN : Number(value);
560
- if (!Number.isFinite(n) || n < 0 || n > 1) throw new Error(`${flag} must be a number from 0 to 1.`);
561
- return n;
638
+ // ../../core/cli/handlers/load-baseline.ts
639
+ const LOAD_BASELINE_INVALID = "load_baseline_invalid";
640
+ const LOAD_BASELINE_MISMATCH = "load_baseline_mismatch";
641
+ const LOAD_REGRESSION = "load_regression";
642
+ function resolveLoadBaseline(flags) {
643
+ if (flags.baseline === void 0) {
644
+ if (flags.maxRegressionPct !== void 0) throw new CliExitCodeError(EXIT_CODES.USAGE, "--load-max-regression requires --load-baseline.", { code: LOAD_FLAG_CONFLICT });
645
+ return;
646
+ }
647
+ return {
648
+ baseline: readLoadBaselineFile(flags.baseline),
649
+ ...flags.maxRegressionPct !== void 0 && { maxRegressionPct: flags.maxRegressionPct }
562
650
  };
563
651
  }
564
- const maxP99Parser = positiveNumberParser("--load-max-p99");
565
- function resolveLoadThresholds(flags) {
566
- const { maxErrorRate, maxP99Ms } = flags;
567
- if (maxErrorRate === void 0 && maxP99Ms === void 0) return void 0;
652
+ function maxRegressionParser(value) {
653
+ const trimmed = value.trim().replace(/%$/, "").trim();
654
+ const n = trimmed === "" ? NaN : Number(trimmed);
655
+ if (!Number.isFinite(n) || n < 0) throw new Error("--load-max-regression must be a percentage of 0 or more, e.g. 10 or 10%.");
656
+ return n;
657
+ }
658
+ function invalid(source, problem) {
659
+ return new CliExitCodeError(EXIT_CODES.USAGE, `--load-baseline ${source}: ${problem}`, { code: LOAD_BASELINE_INVALID });
660
+ }
661
+ function isRecord$1(value) {
662
+ return typeof value === "object" && value !== null && !Array.isArray(value);
663
+ }
664
+ const NUMERIC_FIELDS = [
665
+ "concurrency",
666
+ "requests",
667
+ "ok",
668
+ "toolErrors",
669
+ "errors",
670
+ "elapsedMs",
671
+ "errorRate",
672
+ "okThroughput"
673
+ ];
674
+ function summaryProblem(value) {
675
+ if (!isRecord$1(value)) return "is not a JSON object";
676
+ for (const field of NUMERIC_FIELDS) if (typeof value[field] !== "number") return `is not a --load summary (no numeric ${field}); save one with --format json`;
677
+ const latency = value.latencyMs;
678
+ if (latency !== null && !(isRecord$1(latency) && typeof latency.p50 === "number" && typeof latency.p99 === "number")) return "is not a --load summary (latencyMs is neither null nor a set of percentiles)";
679
+ if (value.stoppedEarly !== void 0 && !isRecord$1(value.stoppedEarly)) return "has a stoppedEarly field that is not an object";
680
+ if (value.steps !== void 0) {
681
+ if (!Array.isArray(value.steps)) return "has a steps field that is not an array";
682
+ for (const step of value.steps) if (!isRecord$1(step) || typeof step.name !== "string" || typeof step.method !== "string" || typeof step.target !== "string" || typeof step.weight !== "number") return "has a scenario step without a name, method, target and weight";
683
+ }
684
+ if (value.stages !== void 0) {
685
+ if (!Array.isArray(value.stages)) return "has a stages field that is not an array";
686
+ for (const stage of value.stages) {
687
+ const problem = summaryProblem(stage);
688
+ if (problem !== void 0) return `has a stage that ${problem}`;
689
+ }
690
+ }
691
+ }
692
+ function parseLoadBaseline(text, source) {
693
+ let parsed;
694
+ try {
695
+ parsed = JSON.parse(text);
696
+ } catch (err) {
697
+ throw invalid(source, `is not valid JSON (${err instanceof Error ? err.message : String(err)}); save the baseline with --format json`);
698
+ }
699
+ const candidate = isRecord$1(parsed) && isRecord$1(parsed.result) && !("requests" in parsed) ? parsed.result : parsed;
700
+ const problem = summaryProblem(candidate);
701
+ if (problem !== void 0) throw invalid(source, problem);
702
+ const summary = candidate;
703
+ if (summary.stoppedEarly !== void 0) throw invalid(source, `the baseline run stopped early (${String(summary.stoppedEarly.reason)}), so it is not a baseline`);
568
704
  return {
569
- ...maxErrorRate !== void 0 && { maxErrorRate },
570
- ...maxP99Ms !== void 0 && { maxP99Ms }
705
+ source,
706
+ summary
571
707
  };
572
708
  }
573
- function exactErrorRate(summary) {
574
- return summary.requests === 0 ? 0 : (summary.toolErrors + summary.errors) / summary.requests;
709
+ function readLoadBaselineFile(path) {
710
+ let text;
711
+ try {
712
+ text = readFileSync$1(path, "utf8");
713
+ } catch (err) {
714
+ throw invalid(path, `cannot be read: ${err instanceof Error ? err.message : String(err)}`);
715
+ }
716
+ return parseLoadBaseline(text, path);
717
+ }
718
+ function shown(value) {
719
+ return value === void 0 ? "unset" : JSON.stringify(value);
575
720
  }
576
- function errorRateBreach(summary, max) {
577
- if (exactErrorRate(summary) <= max) return void 0;
578
- const failed = summary.toolErrors + summary.errors;
721
+ function stepsShape(summary) {
722
+ return summary.steps?.map(({ name, method, target, weight }) => ({
723
+ name,
724
+ method,
725
+ target,
726
+ weight
727
+ }));
728
+ }
729
+ function shapeDifferences(run, base) {
730
+ return [
731
+ [
732
+ "tool",
733
+ run.tool,
734
+ base.tool
735
+ ],
736
+ [
737
+ "scenario steps",
738
+ stepsShape(run),
739
+ stepsShape(base)
740
+ ],
741
+ [
742
+ "concurrency",
743
+ run.concurrency,
744
+ base.concurrency
745
+ ],
746
+ [
747
+ "durationMs",
748
+ run.durationMs,
749
+ base.durationMs
750
+ ],
751
+ [
752
+ "requestLimit",
753
+ run.requestLimit,
754
+ base.requestLimit
755
+ ],
756
+ [
757
+ "rate",
758
+ run.rate,
759
+ base.rate
760
+ ],
761
+ [
762
+ "openLoop",
763
+ run.openLoop === true,
764
+ base.openLoop === true
765
+ ],
766
+ [
767
+ "connections",
768
+ run.connections?.requested ?? 1,
769
+ base.connections?.requested ?? 1
770
+ ],
771
+ [
772
+ "fan-out",
773
+ run.fanout && {
774
+ uri: run.fanout.uri,
775
+ subscribers: run.fanout.requested,
776
+ mode: run.fanout.mode
777
+ },
778
+ base.fanout && {
779
+ uri: base.fanout.uri,
780
+ subscribers: base.fanout.requested,
781
+ mode: base.fanout.mode
782
+ }
783
+ ],
784
+ [
785
+ "churn calls per session",
786
+ run.churn?.callsPerSession,
787
+ base.churn?.callsPerSession
788
+ ]
789
+ ].filter(([, a, b]) => JSON.stringify(a) !== JSON.stringify(b)).map(([label, a, b]) => `${label} is ${shown(a)} here but ${shown(b)} in the baseline`);
790
+ }
791
+ function mismatches(run, base) {
792
+ const found = shapeDifferences(run, base);
793
+ const runStages = run.stages ?? [];
794
+ const baseStages = base.stages ?? [];
795
+ if (run.stages === void 0 !== (base.stages === void 0)) {
796
+ found.push(run.stages === void 0 ? "the baseline is a stepped run (--load-steps / --load-ramp) and this one is not" : "this is a stepped run (--load-steps / --load-ramp) and the baseline is not");
797
+ return found;
798
+ }
799
+ const shared = Math.min(runStages.length, baseStages.length);
800
+ for (let i = 0; i < shared; i++) {
801
+ const r = runStages[i];
802
+ const b = baseStages[i];
803
+ if (r.stepBy !== b.stepBy || r.level !== b.level || r.durationMs !== b.durationMs) found.push(`stage ${r.stage} is ${r.stepBy} ${r.level} for ${shown(r.durationMs)}ms here but ${b.stepBy} ${b.level} for ${shown(b.durationMs)}ms in the baseline`);
804
+ if (r.rate !== b.rate) found.push(`stage ${r.stage} rate is ${shown(r.rate)} here but ${shown(b.rate)} in the baseline`);
805
+ }
806
+ if (runStages.length !== baseStages.length && run.stoppedEarly === void 0) found.push(`this run has ${runStages.length} stages but the baseline has ${baseStages.length}`);
807
+ return found;
808
+ }
809
+ function round2(n) {
810
+ return Math.round(n * 100) / 100;
811
+ }
812
+ function shownNumber(n) {
813
+ return n !== 0 && Math.abs(n) < .01 ? Number(n.toPrecision(2)) : round2(n);
814
+ }
815
+ function relativeChange(baseline, value, worseWhen) {
816
+ return (worseWhen === "higher" ? value - baseline : baseline - value) / baseline * 100;
817
+ }
818
+ function exactErrorRate(s) {
819
+ return s.requests === 0 ? 0 : (s.toolErrors + s.errors) / s.requests;
820
+ }
821
+ function exactOkThroughput(s) {
822
+ return s.elapsedMs > 0 ? s.ok * 1e3 / s.elapsedMs : 0;
823
+ }
824
+ function latencyFigure(figure, run, base) {
825
+ const baseline = base.latencyMs?.[figure] ?? null;
826
+ const value = run.latencyMs?.[figure] ?? null;
579
827
  return {
580
- code: LOAD_ERROR_RATE_EXCEEDED,
581
- message: `error rate ${summary.errorRate} (${failed} of ${summary.requests} calls failed) is above --load-max-error-rate ${max}`
828
+ figure,
829
+ baseline,
830
+ value,
831
+ change: baseline === null || baseline === 0 ? null : value === null ? Infinity : relativeChange(baseline, value, "higher")
582
832
  };
583
833
  }
584
- function p99Breach(summary, max) {
585
- const p99 = summary.latencyMs?.p99;
586
- if (p99 === void 0) return {
587
- code: LOAD_P99_EXCEEDED,
588
- message: `no call succeeded, so there is no p99 latency to check against --load-max-p99 ${max}`
834
+ function compareFigures(run, base) {
835
+ const baseRate = exactErrorRate(base);
836
+ const runRate = exactErrorRate(run);
837
+ const baseThroughput = exactOkThroughput(base);
838
+ const runThroughput = exactOkThroughput(run);
839
+ return [
840
+ latencyFigure("p50", run, base),
841
+ latencyFigure("p99", run, base),
842
+ {
843
+ figure: "errorRate",
844
+ baseline: baseRate,
845
+ value: runRate,
846
+ change: (runRate - baseRate) * 100
847
+ },
848
+ {
849
+ figure: "okThroughput",
850
+ baseline: base.okThroughput,
851
+ value: run.okThroughput,
852
+ change: baseThroughput > 0 ? relativeChange(baseThroughput, runThroughput, "lower") : null
853
+ }
854
+ ];
855
+ }
856
+ function compareLoadToBaseline(summary, baseline) {
857
+ const base = baseline.summary;
858
+ const found = mismatches(summary, base);
859
+ if (found.length > 0) throw new CliExitCodeError(EXIT_CODES.USAGE, `--load-baseline ${baseline.source} is not comparable with this run: ${found.join("; ")}.`, { code: LOAD_BASELINE_MISMATCH });
860
+ if (summary.stages !== void 0) {
861
+ const baseStages = base.stages;
862
+ return {
863
+ source: baseline.source,
864
+ rows: summary.stages.map((stage, i) => ({
865
+ where: ` at stage ${stage.stage} (${stage.stepBy} ${stage.level})`,
866
+ figures: compareFigures(stage, baseStages[i])
867
+ }))
868
+ };
869
+ }
870
+ return {
871
+ source: baseline.source,
872
+ rows: [{
873
+ where: "",
874
+ figures: compareFigures(summary, base)
875
+ }]
589
876
  };
590
- if (p99 <= max) return void 0;
877
+ }
878
+ const FIGURE_LABEL = {
879
+ p50: "p50",
880
+ p99: "p99",
881
+ errorRate: "error rate",
882
+ okThroughput: "ok throughput"
883
+ };
884
+ function figureValue(f, n) {
885
+ if (n === null) return "none";
886
+ switch (f.figure) {
887
+ case "p50":
888
+ case "p99": return `${n}ms`;
889
+ case "errorRate": return `${shownNumber(n * 100)}%`;
890
+ case "okThroughput": return `${n}/s`;
891
+ }
892
+ }
893
+ function changeText(f) {
894
+ if (f.change === null) return "not compared";
895
+ if (f.change === Infinity) return "no call succeeded";
896
+ const unit = f.figure === "errorRate" ? " pts" : "%";
897
+ return `${f.change > 0 ? "+" : ""}${shownNumber(f.change)}${unit} worse`;
898
+ }
899
+ function figureLine(f) {
900
+ return `${FIGURE_LABEL[f.figure]} ${figureValue(f, f.baseline)} -> ${figureValue(f, f.value)} (${changeText(f)})`;
901
+ }
902
+ function formatLoadBaselineComparison(comparison) {
903
+ return comparison.rows.map((row) => `Baseline ${comparison.source}${row.where}: ${row.figures.map(figureLine).join(", ")}`);
904
+ }
905
+ const BUDGET_EPSILON = 1e-9;
906
+ function checkLoadRegression(comparison, maxRegressionPct) {
907
+ if (maxRegressionPct === void 0) return;
908
+ for (const row of comparison.rows) {
909
+ const regressed = row.figures.filter((f) => f.change !== null && f.change > maxRegressionPct + BUDGET_EPSILON);
910
+ if (regressed.length === 0) continue;
911
+ throw new CliExitCodeError(EXIT_CODES.LOAD_THRESHOLD_EXCEEDED, `Load regression${row.where} against --load-baseline ${comparison.source}: ${regressed.map(figureLine).join("; ")}; over --load-max-regression ${maxRegressionPct}%.`, { code: LOAD_REGRESSION });
912
+ }
913
+ }
914
+ // ../../core/cli/handlers/load-files.ts
915
+ function readLoadScenarioFile(path) {
916
+ let text;
917
+ try {
918
+ text = readFileSync$1(path, "utf8");
919
+ } catch (err) {
920
+ throw loadScenarioError(path, `cannot be read: ${err.message}`);
921
+ }
922
+ return parseLoadScenario(text, path);
923
+ }
924
+ function readLoadArgsFile(path) {
925
+ let text;
926
+ try {
927
+ text = readFileSync$1(path, "utf8");
928
+ } catch (err) {
929
+ throw loadArgsError(`--load-args-file ${path}: cannot be read: ${err.message}`);
930
+ }
931
+ try {
932
+ return parseLoadArgsRows(text);
933
+ } catch (err) {
934
+ throw loadArgsError(`--load-args-file ${path}: ${err.message}`);
935
+ }
936
+ }
937
+ function loadStepsParser(value) {
938
+ const levels = value.split(",").map((p) => p.trim()).map((p) => p === "" ? NaN : Number(p));
939
+ if (levels.length === 0 || levels.some((n) => !Number.isFinite(n) || n <= 0)) throw new Error("--load-steps must be a comma-separated list of positive numbers, e.g. 10,25,50.");
940
+ return levels;
941
+ }
942
+ function loadRampParser(value) {
943
+ const parts = value.split(":").map((p) => p.trim());
944
+ const [from, to, durationSec] = parts.map((p) => p === "" ? NaN : Number(p));
945
+ if (parts.length !== 3 || ![
946
+ from,
947
+ to,
948
+ durationSec
949
+ ].every((n) => Number.isFinite(n) && n > 0)) throw new Error("--load-ramp must be <from>:<to>:<seconds>, three positive numbers, e.g. 10:100:60.");
591
950
  return {
592
- code: LOAD_P99_EXCEEDED,
593
- message: `p99 latency ${p99}ms is above --load-max-p99 ${max}ms`
951
+ from,
952
+ to,
953
+ durationSec
594
954
  };
595
955
  }
596
- function checkLoadThresholds(summary, thresholds) {
597
- if (!thresholds) return;
598
- const breaches = [thresholds.maxErrorRate !== void 0 ? errorRateBreach(summary, thresholds.maxErrorRate) : void 0, thresholds.maxP99Ms !== void 0 ? p99Breach(summary, thresholds.maxP99Ms) : void 0].filter((b) => b !== void 0);
599
- const [first] = breaches;
600
- if (!first) return;
601
- throw new CliExitCodeError(EXIT_CODES.LOAD_THRESHOLD_EXCEEDED, `Load threshold exceeded: ${breaches.map((b) => b.message).join("; ")}.`, { code: first.code });
956
+ function loadStepByParser(value) {
957
+ if (value !== "concurrency" && value !== "rate") throw new Error("--load-step-by must be concurrency or rate.");
958
+ return value;
959
+ }
960
+ function rampLevels(ramp, count, by) {
961
+ const levels = Array.from({ length: count }, (_, i) => count === 1 ? ramp.to : ramp.from + (ramp.to - ramp.from) * i / (count - 1));
962
+ return by === "concurrency" ? levels.map((l) => Math.max(1, Math.round(l))) : levels;
963
+ }
964
+ function resolveLoadStages(flags) {
965
+ const { steps, ramp } = flags;
966
+ if (steps === void 0 && ramp === void 0) {
967
+ for (const [flag, value] of [
968
+ ["--load-step-by", flags.stepBy],
969
+ ["--load-step-duration", flags.stepDurationSec],
970
+ ["--load-stop-on-breach", flags.stopOnBreach]
971
+ ]) if (value !== void 0) throw new Error(`${flag} requires --load-steps or --load-ramp.`);
972
+ return;
973
+ }
974
+ if (steps !== void 0 && ramp !== void 0) throw new Error("--load-steps cannot be combined with --load-ramp.");
975
+ const shape = steps !== void 0 ? "--load-steps" : "--load-ramp";
976
+ if (flags.durationSec !== void 0 || flags.requests !== void 0) throw new Error(`${shape} cannot be combined with --load-duration or --load-requests; each stage runs for --load-step-duration.`);
977
+ const by = flags.stepBy ?? "concurrency";
978
+ if (by === "concurrency" && flags.concurrency !== void 0) throw new Error(`${shape} sets the concurrency of each stage, so it cannot be combined with --load-concurrency; use --load-step-by rate to step the rate over a fixed pool.`);
979
+ if (by === "rate" && flags.rate !== void 0) throw new Error(`${shape} with --load-step-by rate sets the rate of each stage, so it cannot be combined with --load-rate.`);
980
+ if (by === "concurrency" && steps !== void 0 && steps.some((n) => !Number.isInteger(n))) throw new Error("--load-steps must be whole numbers when stepping concurrency.");
981
+ if (flags.stopOnBreach === true && flags.thresholds === void 0) throw new Error("--load-stop-on-breach requires a threshold (--load-max-error-rate, --load-max-p50, --load-max-p95, --load-max-p99 or --load-min-throughput).");
982
+ const stepSec = flags.stepDurationSec ?? 10;
983
+ let levels;
984
+ let stageMs;
985
+ if (ramp !== void 0) {
986
+ const count = Math.max(1, Math.round(ramp.durationSec / stepSec));
987
+ levels = rampLevels(ramp, count, by);
988
+ stageMs = ramp.durationSec * 1e3 / count;
989
+ } else {
990
+ levels = steps;
991
+ stageMs = stepSec * 1e3;
992
+ }
993
+ return {
994
+ by,
995
+ levels,
996
+ stageMs,
997
+ ...flags.stopOnBreach === true && { stopOnBreach: flags.thresholds }
998
+ };
602
999
  }
603
- //#endregion
604
- //#region src/connection/load-flags.ts
1000
+ // src/connection/load-flags.ts
605
1001
  function resolveLoadFlags(flags) {
606
1002
  const bounds = [
607
1003
  ["--load-concurrency", flags.loadConcurrency],
@@ -609,41 +1005,118 @@ function resolveLoadFlags(flags) {
609
1005
  ["--load-duration", flags.loadDuration],
610
1006
  ["--load-requests", flags.loadRequests],
611
1007
  ["--load-rate", flags.loadRate],
1008
+ ["--load-scenario", flags.loadScenario],
1009
+ ["--load-args-file", flags.loadArgsFile],
1010
+ ["--load-args-random", flags.loadArgsRandom],
1011
+ ["--load-steps", flags.loadSteps],
1012
+ ["--load-ramp", flags.loadRamp],
1013
+ ["--load-step-by", flags.loadStepBy],
1014
+ ["--load-step-duration", flags.loadStepDuration],
1015
+ ["--load-stop-on-breach", flags.loadStopOnBreach],
1016
+ ["--load-interval", flags.loadInterval],
1017
+ ["--load-warmup", flags.loadWarmup],
1018
+ ["--load-progress", flags.loadProgress],
1019
+ ["--load-open-loop", flags.loadOpenLoop],
1020
+ ["--load-churn", flags.loadChurn],
1021
+ ["--load-churn-calls", flags.loadChurnCalls],
1022
+ ["--load-subscribe", flags.loadSubscribe],
1023
+ ["--load-subscribers", flags.loadSubscribers],
1024
+ ["--load-subscribe-wait", flags.loadSubscribeWait],
612
1025
  ["--load-max-error-rate", flags.loadMaxErrorRate],
613
- ["--load-max-p99", flags.loadMaxP99]
1026
+ ["--load-max-p99", flags.loadMaxP99],
1027
+ ["--load-max-p50", flags.loadMaxP50],
1028
+ ["--load-max-p95", flags.loadMaxP95],
1029
+ ["--load-min-throughput", flags.loadMinThroughput],
1030
+ ["--load-baseline", flags.loadBaseline],
1031
+ ["--load-max-regression", flags.loadMaxRegression]
614
1032
  ];
615
1033
  if (flags.load !== true) {
616
1034
  for (const [flag, value] of bounds) if (value !== void 0) throw usage(`${flag} requires --load.`);
617
1035
  return;
618
1036
  }
619
1037
  if (flags.appInfo === true) throw usage("--load cannot be combined with --app-info.");
620
- if (!flags.toolName) throw usage("--load requires a tool name.", "missing_tool_name");
1038
+ if (flags.loadScenario !== void 0) {
1039
+ const named = [
1040
+ ["a tool name", flags.toolName !== void 0],
1041
+ ["tool arguments", Object.keys(flags.toolArg ?? {}).length > 0],
1042
+ ["--tool-metadata", Object.keys(flags.toolMetadata ?? {}).length > 0]
1043
+ ];
1044
+ for (const [what, given] of named) if (given) throw usage(`--load-scenario cannot be combined with ${what}; each scenario step names its own tool and arguments.`);
1045
+ if (flags.loadArgsFile !== void 0) throw usage("--load-args-file cannot be combined with --load-scenario; put placeholders in each step's args instead.");
1046
+ } else if (!flags.toolName && flags.loadSubscribe === void 0) throw usage("--load requires a tool name.", "missing_tool_name");
1047
+ if (flags.loadArgsRandom === true && flags.loadArgsFile === void 0) throw usage("--load-args-random requires --load-args-file.");
1048
+ assertLoadArgsTemplate(flags.toolArg, "tool arguments");
1049
+ const scenario = flags.loadScenario !== void 0 ? readLoadScenarioFile(flags.loadScenario) : void 0;
1050
+ const argRows = flags.loadArgsFile !== void 0 ? {
1051
+ rows: readLoadArgsFile(flags.loadArgsFile),
1052
+ order: flags.loadArgsRandom === true ? "random" : "sequential"
1053
+ } : void 0;
1054
+ const thresholds = resolveLoadThresholds({
1055
+ maxErrorRate: flags.loadMaxErrorRate,
1056
+ maxP50Ms: flags.loadMaxP50,
1057
+ maxP95Ms: flags.loadMaxP95,
1058
+ maxP99Ms: flags.loadMaxP99,
1059
+ minThroughput: flags.loadMinThroughput
1060
+ });
621
1061
  let options;
622
1062
  try {
1063
+ const stages = resolveLoadStages({
1064
+ steps: flags.loadSteps,
1065
+ ramp: flags.loadRamp,
1066
+ stepBy: flags.loadStepBy,
1067
+ stepDurationSec: flags.loadStepDuration,
1068
+ stopOnBreach: flags.loadStopOnBreach,
1069
+ thresholds,
1070
+ concurrency: flags.loadConcurrency,
1071
+ rate: flags.loadRate,
1072
+ durationSec: flags.loadDuration,
1073
+ requests: flags.loadRequests
1074
+ });
623
1075
  options = resolveLoadOptions({
624
1076
  concurrency: flags.loadConcurrency,
625
1077
  connections: flags.loadConnections,
626
1078
  durationSec: flags.loadDuration,
627
1079
  requests: flags.loadRequests,
628
- rate: flags.loadRate
1080
+ rate: flags.loadRate,
1081
+ scenario,
1082
+ argRows,
1083
+ stages,
1084
+ intervalSec: flags.loadInterval,
1085
+ warmupSec: flags.loadWarmup,
1086
+ openLoop: flags.loadOpenLoop,
1087
+ churn: flags.loadChurn,
1088
+ churnCalls: flags.loadChurnCalls,
1089
+ subscribe: flags.loadSubscribe,
1090
+ subscribers: flags.loadSubscribers,
1091
+ subscribeWaitSec: flags.loadSubscribeWait,
1092
+ progressSec: flags.loadProgress ?? 5
629
1093
  });
630
1094
  } catch (err) {
631
1095
  throw usage(err.message);
632
1096
  }
633
- const thresholds = resolveLoadThresholds({
634
- maxErrorRate: flags.loadMaxErrorRate,
635
- maxP99Ms: flags.loadMaxP99
1097
+ assertFitsOneRequest(options);
1098
+ const baseline = resolveLoadBaseline({
1099
+ baseline: flags.loadBaseline,
1100
+ maxRegressionPct: flags.loadMaxRegression
636
1101
  });
637
1102
  return {
638
1103
  options,
639
- ...thresholds && { thresholds }
1104
+ ...thresholds && { thresholds },
1105
+ ...baseline && { baseline }
640
1106
  };
641
1107
  }
1108
+ const LOAD_REQUEST_HEADROOM_BYTES = 65536;
1109
+ function assertFitsOneRequest(options) {
1110
+ const budget = MAX_REQUEST_LINE_BYTES - LOAD_REQUEST_HEADROOM_BYTES;
1111
+ const bytes = Buffer.byteLength(JSON.stringify(options));
1112
+ if (bytes <= budget) return;
1113
+ const [flag, code] = options.argRows !== void 0 ? ["--load-args-file", LOAD_ARGS_INVALID] : ["--load-scenario", LOAD_SCENARIO_INVALID];
1114
+ throw usage(`${flag} is too large for mcpdo: the run's options encode to ${bytes} bytes, over the ${budget}-byte limit of one daemon request. Use fewer or smaller rows, or the one-shot CLI (mcp-inspector --cli), which has no such limit.`, code);
1115
+ }
642
1116
  function usage(message, code = LOAD_FLAG_CONFLICT) {
643
1117
  return new CliExitCodeError(EXIT_CODES.USAGE, message, { code });
644
1118
  }
645
- //#endregion
646
- //#region src/connection/ema-login-helper.ts
1119
+ // src/connection/ema-login-helper.ts
647
1120
  const EMA_LOGIN_HELPER_COMMAND = "auth/complete-ema-login";
648
1121
  function emaLoginMarkerKey(issuer) {
649
1122
  return `ema-idp:${issuer}`;
@@ -704,8 +1177,7 @@ async function runEmaLoginHelper() {
704
1177
  if (markerPath !== void 0) removeOwnPendingAuthMarker(markerPath);
705
1178
  }
706
1179
  }
707
- //#endregion
708
- //#region src/connection/parse-tool-args.ts
1180
+ // src/connection/parse-tool-args.ts
709
1181
  function assertJsonRoundTrips(value, context) {
710
1182
  if (typeof value === "number" && !Number.isFinite(value)) throw new Error(`Invalid ${context}: ${value} has no JSON representation (it would silently reach the tool as null).`);
711
1183
  if (Array.isArray(value)) for (const item of value) assertJsonRoundTrips(item, context);
@@ -787,8 +1259,7 @@ function parseJsonObject(raw, flag) {
787
1259
  assertJsonRoundTrips(parsed, flag);
788
1260
  return parsed;
789
1261
  }
790
- //#endregion
791
- //#region src/connection/resolve-command.ts
1262
+ // src/connection/resolve-command.ts
792
1263
  function resolveCommandPath(command, env = process.env) {
793
1264
  if (!command || command.includes("/") || command.includes(path.sep)) return command;
794
1265
  const pathVar = env.PATH ?? "";
@@ -804,8 +1275,7 @@ function resolveCommandPath(command, env = process.env) {
804
1275
  }
805
1276
  return command;
806
1277
  }
807
- //#endregion
808
- //#region src/connection/form-schema.ts
1278
+ // src/connection/form-schema.ts
809
1279
  function isRecord(value) {
810
1280
  return typeof value === "object" && value !== null && !Array.isArray(value);
811
1281
  }
@@ -951,8 +1421,245 @@ function parseFormSchema(schema) {
951
1421
  }
952
1422
  return fields;
953
1423
  }
954
- //#endregion
955
- //#region src/connection/format-human.ts
1424
+ // ../../core/cli/handlers/load-format.ts
1425
+ function code$1(style, name) {
1426
+ return `\`${style.bold(name)}\``;
1427
+ }
1428
+ function heading$1(style, text) {
1429
+ return style.bold(text);
1430
+ }
1431
+ function formatLoadSummaryHuman(summary, style = PLAIN) {
1432
+ const tool = summary.tool !== void 0 ? ` ${code$1(style, summary.tool)}` : summary.steps !== void 0 ? ` scenario (${summary.steps.length} ${summary.steps.length === 1 ? "step" : "steps"})` : "";
1433
+ const bounds = [
1434
+ `concurrency ${summary.concurrency}`,
1435
+ ...summary.durationMs !== void 0 ? [`duration ${summary.durationMs / 1e3}s`] : [],
1436
+ ...summary.requestLimit !== void 0 ? [`limit ${summary.requestLimit}`] : [],
1437
+ ...summary.rate !== void 0 ? [`rate ${summary.rate}/s`] : [],
1438
+ ...summary.openLoop === true ? ["open loop"] : [],
1439
+ ...summary.churn !== void 0 ? ["churn"] : [],
1440
+ ...summary.workers !== void 0 ? [`${summary.workers} worker processes`] : []
1441
+ ];
1442
+ const failed = summary.toolErrors + summary.errors;
1443
+ const rate = `${(summary.errorRate * 100).toFixed(2)}%`;
1444
+ const lines = [
1445
+ `${heading$1(style, "Load test")}${tool} ${style.dim(`(${bounds.join(", ")})`)}`,
1446
+ ` Requests: ${summary.requests} in ${(summary.elapsedMs / 1e3).toFixed(2)}s (${summary.throughput}/s, ${summary.okThroughput}/s ok)`,
1447
+ ` Outcomes: ${style.green(`${summary.ok} ok`)}, ${summary.toolErrors} tool errors, ${summary.errors} errors`,
1448
+ ` Error rate: ${failed > 0 ? style.red(rate) : rate}`
1449
+ ];
1450
+ if (summary.plannedStarts !== void 0) {
1451
+ const dropped = `${summary.droppedSlots} dropped`;
1452
+ lines.push(` Rate: ${summary.achievedRate}/s achieved; ${summary.plannedStarts} planned, ${summary.droppedSlots > 0 ? style.red(dropped) : dropped}`);
1453
+ }
1454
+ const l = summary.latencyMs;
1455
+ lines.push(l === null ? ` Latency: ${style.dim("no calls succeeded")}` : ` Latency: ${latencyLine(l)}`);
1456
+ const e = summary.errorLatencyMs;
1457
+ if (e !== null) lines.push(` Failed in: ${latencyLine(e)}`);
1458
+ if (summary.errorMessages.length > 0) {
1459
+ lines.push(` ${style.bold("Top errors:")}`);
1460
+ for (const { message, count } of summary.errorMessages) lines.push(` ${count}× ${message}`);
1461
+ }
1462
+ if (summary.toolErrorMessages.length > 0) {
1463
+ lines.push(` ${style.bold("Top tool errors:")}`);
1464
+ for (const { message, count } of summary.toolErrorMessages) lines.push(` ${count}× ${message}`);
1465
+ }
1466
+ if (summary.stoppedEarly !== void 0) lines.push(` ${style.red("Stopped early")} (${summary.stoppedEarly.reason}): ${summary.stoppedEarly.detail}`);
1467
+ const w = summary.warmup;
1468
+ if (w !== void 0) {
1469
+ const stopped = w.stoppedEarly !== void 0 ? ` ${style.red(`(stopped: ${w.stoppedEarly.reason})`)}` : "";
1470
+ lines.push(` Warm-up: ${w.durationMs / 1e3}s, ${w.requests} calls excluded (${w.ok} ok, ${w.failed} failed)${stopped}`);
1471
+ }
1472
+ if (summary.intervals !== void 0) {
1473
+ lines.push(` ${style.bold("Intervals:")}`);
1474
+ for (const row of summary.intervals) lines.push(formatLoadInterval(row));
1475
+ }
1476
+ if (summary.stages !== void 0) {
1477
+ lines.push(` ${style.bold("Stages:")}`);
1478
+ for (const stage of summary.stages) {
1479
+ lines.push(formatLoadStage(stage, style));
1480
+ for (const row of stage.intervals ?? []) lines.push(` ${formatLoadInterval(row)}`);
1481
+ }
1482
+ }
1483
+ if (summary.steps !== void 0) {
1484
+ lines.push(` ${style.bold("Steps:")}`);
1485
+ for (const step of summary.steps) lines.push(...formatLoadStep(step, style));
1486
+ }
1487
+ const c = summary.connections;
1488
+ if (c !== void 0) {
1489
+ const failed = `${c.failed} failed`;
1490
+ const connect = c.connectMs === null ? "" : ` (connect p50 ${c.connectMs.p50} / max ${c.connectMs.max} ms)`;
1491
+ lines.push(` Sessions: ${c.opened} of ${c.requested} opened, ${c.failed > 0 ? style.red(failed) : failed}${connect}`);
1492
+ for (const { message, count } of c.errorMessages) lines.push(` ${count}× ${message}`);
1493
+ }
1494
+ const f = summary.fanout;
1495
+ if (f !== void 0) lines.push(...formatLoadFanout(f, style));
1496
+ const churn = summary.churn;
1497
+ if (churn !== void 0) {
1498
+ const failed = `${churn.failed} failed to open`;
1499
+ const connect = churn.connectMs === null ? "" : ` (connect p50 ${churn.connectMs.p50} / p99 ${churn.connectMs.p99} / max ${churn.connectMs.max} ms)`;
1500
+ const closeFailed = churn.closeFailed > 0 ? `, ${churn.closeFailed} failed to close` : "";
1501
+ lines.push(` Churn: ${churn.opened} sessions of ${churn.callsPerSession} ${churn.callsPerSession === 1 ? "call" : "calls"}, ${churn.failed > 0 ? style.red(failed) : failed}${closeFailed}${connect}`);
1502
+ for (const { message, count } of churn.errorMessages) lines.push(` ${count}× ${message}`);
1503
+ }
1504
+ return lines.join("\n");
1505
+ }
1506
+ function formatLoadFanout(f, style) {
1507
+ const failed = `${f.failed} failed`;
1508
+ const subscribe = f.subscribeMs === null ? "" : ` (subscribe p50 ${f.subscribeMs.p50} / max ${f.subscribeMs.max} ms)`;
1509
+ const missed = `${f.missed} missed`;
1510
+ const lines = [` Fan-out: ${code$1(style, f.uri)}, ${f.mode}; ${f.subscribed} of ${f.requested} sessions subscribed, ${f.failed > 0 ? style.red(failed) : failed}${subscribe}`, ` Delivered: ${f.delivered} of ${f.updates * f.subscribed} notifications (${f.updates} updates × ${f.subscribed}), ${f.missed > 0 ? style.red(missed) : missed} across ${f.sessionsMissed} sessions, ${f.unexpected} unexpected`];
1511
+ if (f.deliveryMs !== null) lines.push(` Delivery: ${latencyLine(f.deliveryMs)}`);
1512
+ for (const { message, count } of f.errorMessages) lines.push(` ${count}× ${message}`);
1513
+ return lines;
1514
+ }
1515
+ function latencyLine(l) {
1516
+ return `min ${l.min} / mean ${l.mean} / p50 ${l.p50} / p90 ${l.p90} / p95 ${l.p95} / p99 ${l.p99} / p99.9 ${l.p999} / max ${l.max} ms`;
1517
+ }
1518
+ function formatLoadInterval(row) {
1519
+ const failed = row.toolErrors + row.errors;
1520
+ const l = row.latencyMs;
1521
+ const latency = l === null ? "no successes" : `p50 ${l.p50} / p99 ${l.p99} ms`;
1522
+ return ` ${row.startMs / 1e3}–${row.endMs / 1e3}s: ${row.requests} calls (${row.throughput}/s), ${failed} failed; ${latency}`;
1523
+ }
1524
+ function formatLoadStage(stage, style) {
1525
+ const failed = stage.toolErrors + stage.errors;
1526
+ const rate = `${(stage.errorRate * 100).toFixed(2)}%`;
1527
+ const l = stage.latencyMs;
1528
+ const latency = l === null ? style.dim("no calls succeeded") : `p50 ${l.p50} / p99 ${l.p99} ms`;
1529
+ const level = stage.stepBy === "rate" ? `rate ${stage.level}/s` : `concurrency ${stage.level}`;
1530
+ const achieved = stage.achievedRate !== void 0 ? `, ${stage.achievedRate}/s started` : "";
1531
+ const stopped = stage.stoppedEarly !== void 0 ? ` ${style.red(`(stopped: ${stage.stoppedEarly.reason})`)}` : "";
1532
+ return ` ${stage.stage}. ${level}: ${stage.requests} requests (${stage.okThroughput}/s ok${achieved}), ${failed > 0 ? style.red(rate) : rate} errors; ${latency}${stopped}`;
1533
+ }
1534
+ function formatLoadStep(step, style) {
1535
+ const failed = step.toolErrors + step.errors;
1536
+ const rate = `${(step.errorRate * 100).toFixed(2)}%`;
1537
+ const l = step.latencyMs;
1538
+ const latency = l === null ? style.dim("no calls succeeded") : `p50 ${l.p50} / p99 ${l.p99} ms`;
1539
+ const lines = [` ${code$1(style, step.name)} ${style.dim(`(${step.method} ${step.target}, weight ${step.weight})`)}`, ` ${step.requests} requests (${step.throughput}/s): ${style.green(`${step.ok} ok`)}, ${step.toolErrors} tool errors, ${step.errors} errors (${failed > 0 ? style.red(rate) : rate}); ${latency}`];
1540
+ for (const { message, count } of [...step.errorMessages, ...step.toolErrorMessages]) lines.push(` ${count}× ${message}`);
1541
+ return lines;
1542
+ }
1543
+ // ../../core/auth/accessTokenClaims.ts
1544
+ const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/g;
1545
+ function clean(value) {
1546
+ return value.replace(CONTROL_CHARS, "");
1547
+ }
1548
+ function stringClaim(value) {
1549
+ if (typeof value !== "string") return void 0;
1550
+ const cleaned = clean(value).trim();
1551
+ return cleaned.length > 0 ? cleaned : void 0;
1552
+ }
1553
+ function listClaim(value) {
1554
+ const strings = (Array.isArray(value) ? value : [value]).map(stringClaim).filter((s) => s !== void 0);
1555
+ return strings.length > 0 ? strings : void 0;
1556
+ }
1557
+ function scopeString(value) {
1558
+ const s = stringClaim(value);
1559
+ if (s === void 0) return void 0;
1560
+ return s.split(/\s+/).filter(Boolean);
1561
+ }
1562
+ function numericClaim(value) {
1563
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
1564
+ }
1565
+ function summarizeAccessToken(tokens) {
1566
+ const claims = decodeJwtPayload(tokens.access_token)?.payload;
1567
+ if (claims && typeof claims === "object" && !Array.isArray(claims)) return {
1568
+ format: "jwt",
1569
+ issuer: stringClaim(claims.iss),
1570
+ audience: listClaim(claims.aud),
1571
+ subject: stringClaim(claims.sub),
1572
+ clientId: stringClaim(claims.client_id) ?? stringClaim(claims.azp),
1573
+ scopes: scopeString(claims.scope) ?? (typeof claims.scp === "string" ? scopeString(claims.scp) : listClaim(claims.scp)),
1574
+ issuedAt: numericClaim(claims.iat),
1575
+ expiresAt: numericClaim(claims.exp)
1576
+ };
1577
+ return {
1578
+ format: "opaque",
1579
+ scopes: scopeString(tokens.scope),
1580
+ expiresIn: numericClaim(tokens.expires_in)
1581
+ };
1582
+ }
1583
+ function isAccessTokenSummaryExpired(summary, nowMs) {
1584
+ return summary.expiresAt !== void 0 && summary.expiresAt * 1e3 <= nowMs;
1585
+ }
1586
+ function formatDuration(seconds) {
1587
+ const s = Math.floor(Math.abs(seconds));
1588
+ if (s < 60) return `${s}s`;
1589
+ const m = Math.floor(s / 60);
1590
+ if (m < 60) return `${m}m`;
1591
+ const h = Math.floor(m / 60);
1592
+ if (h < 24) return m % 60 === 0 ? `${h}h` : `${h}h ${m % 60}m`;
1593
+ const d = Math.floor(h / 24);
1594
+ return h % 24 === 0 ? `${d}d` : `${d}d ${h % 24}h`;
1595
+ }
1596
+ function formatEpoch(epochSeconds) {
1597
+ const date = /* @__PURE__ */ new Date(epochSeconds * 1e3);
1598
+ return Number.isNaN(date.getTime()) ? String(epochSeconds) : date.toISOString();
1599
+ }
1600
+ function relativeTo(epochSeconds, nowMs) {
1601
+ const delta = epochSeconds - nowMs / 1e3;
1602
+ return delta >= 0 ? `in ${formatDuration(delta)}` : `${formatDuration(delta)} ago`;
1603
+ }
1604
+ function accessTokenDetailLines(summary, nowMs) {
1605
+ const lines = [];
1606
+ if (summary.format === "jwt") {
1607
+ lines.push({
1608
+ label: "Format",
1609
+ value: "JWT (signature not verified)"
1610
+ });
1611
+ if (summary.issuer) lines.push({
1612
+ label: "Issuer",
1613
+ value: summary.issuer
1614
+ });
1615
+ if (summary.audience) lines.push({
1616
+ label: "Audience",
1617
+ value: summary.audience.join(", ")
1618
+ });
1619
+ if (summary.subject) lines.push({
1620
+ label: "Subject",
1621
+ value: summary.subject
1622
+ });
1623
+ if (summary.clientId) lines.push({
1624
+ label: "Client",
1625
+ value: summary.clientId
1626
+ });
1627
+ if (summary.scopes) lines.push({
1628
+ label: "Scopes",
1629
+ value: summary.scopes.join(", ")
1630
+ });
1631
+ if (summary.issuedAt !== void 0) lines.push({
1632
+ label: "Issued",
1633
+ value: `${formatEpoch(summary.issuedAt)} (${relativeTo(summary.issuedAt, nowMs)})`
1634
+ });
1635
+ if (summary.expiresAt !== void 0) {
1636
+ const expired = isAccessTokenSummaryExpired(summary, nowMs);
1637
+ const rel = relativeTo(summary.expiresAt, nowMs);
1638
+ lines.push({
1639
+ label: "Expires",
1640
+ value: `${formatEpoch(summary.expiresAt)} (${expired ? `expired ${rel}` : rel})`
1641
+ });
1642
+ } else lines.push({
1643
+ label: "Expires",
1644
+ value: "no exp claim"
1645
+ });
1646
+ return lines;
1647
+ }
1648
+ lines.push({
1649
+ label: "Format",
1650
+ value: "Opaque (not a JWT)"
1651
+ });
1652
+ if (summary.scopes) lines.push({
1653
+ label: "Scopes",
1654
+ value: summary.scopes.join(", ")
1655
+ });
1656
+ lines.push({
1657
+ label: "Expires in",
1658
+ value: summary.expiresIn !== void 0 ? `${formatDuration(summary.expiresIn)} from issuance (token response expires_in)` : "not stated by the token response"
1659
+ });
1660
+ return lines;
1661
+ }
1662
+ // src/connection/format-human.ts
956
1663
  function asArray(value) {
957
1664
  return Array.isArray(value) ? value : [];
958
1665
  }
@@ -1285,6 +1992,17 @@ function formatAuthListHuman(list, style = PLAIN) {
1285
1992
  if (servers.length === 0) lines.push(style.dim("(none)"));
1286
1993
  return lines.join("\n");
1287
1994
  }
1995
+ function formatAuthShowHuman(result, nowMs, style = PLAIN) {
1996
+ const title = `${heading(style, "Stored auth")} ${code(style, result.url)}${result.shownByName ? style.dim(` (${result.shownByName})`) : ""}`;
1997
+ if (!result.hasTokens || !result.accessToken) return [title, style.dim("No stored access token.")].join("\n");
1998
+ const rows = accessTokenDetailLines(result.accessToken, nowMs);
1999
+ const width = Math.max(...rows.map((r) => r.label.length));
2000
+ return [
2001
+ title,
2002
+ ...rows.map((r) => ` ${style.dim(`${r.label}:`.padEnd(width + 1))} ${r.value}`),
2003
+ ` ${style.dim("Refresh:".padEnd(width + 1))} ${result.hasRefreshToken ? "stored" : "none"}`
2004
+ ].join("\n");
2005
+ }
1288
2006
  function formatEmaStatusHuman(status, style = PLAIN) {
1289
2007
  const lines = [heading(style, "EMA (enterprise-managed auth):")];
1290
2008
  lines.push(style.dim(String(status.clientConfigPath ?? "")));
@@ -1484,48 +2202,6 @@ function formatStreamEventHuman(data, style = PLAIN) {
1484
2202
  }
1485
2203
  return JSON.stringify(ev, null, 2);
1486
2204
  }
1487
- function formatLoadSummaryHuman(summary, style = PLAIN) {
1488
- const tool = summary.tool !== void 0 ? ` ${code(style, summary.tool)}` : "";
1489
- const bounds = [
1490
- `concurrency ${summary.concurrency}`,
1491
- ...summary.durationMs !== void 0 ? [`duration ${summary.durationMs / 1e3}s`] : [],
1492
- ...summary.requestLimit !== void 0 ? [`limit ${summary.requestLimit}`] : [],
1493
- ...summary.rate !== void 0 ? [`rate ${summary.rate}/s`] : []
1494
- ];
1495
- const failed = summary.toolErrors + summary.errors;
1496
- const rate = `${(summary.errorRate * 100).toFixed(2)}%`;
1497
- const lines = [
1498
- `${heading(style, "Load test")}${tool} ${style.dim(`(${bounds.join(", ")})`)}`,
1499
- ` Requests: ${summary.requests} in ${(summary.elapsedMs / 1e3).toFixed(2)}s (${summary.throughput}/s, ${summary.okThroughput}/s ok)`,
1500
- ` Outcomes: ${style.green(`${summary.ok} ok`)}, ${summary.toolErrors} tool errors, ${summary.errors} errors`,
1501
- ` Error rate: ${failed > 0 ? style.red(rate) : rate}`
1502
- ];
1503
- if (summary.plannedStarts !== void 0) {
1504
- const dropped = `${summary.droppedSlots} dropped`;
1505
- lines.push(` Rate: ${summary.achievedRate}/s achieved; ${summary.plannedStarts} planned, ${summary.droppedSlots > 0 ? style.red(dropped) : dropped}`);
1506
- }
1507
- const l = summary.latencyMs;
1508
- lines.push(l === null ? ` Latency: ${style.dim("no calls succeeded")}` : ` Latency: min ${l.min} / mean ${l.mean} / p50 ${l.p50} / p90 ${l.p90} / p99 ${l.p99} / max ${l.max} ms`);
1509
- const e = summary.errorLatencyMs;
1510
- if (e !== null) lines.push(` Failed in: min ${e.min} / mean ${e.mean} / p50 ${e.p50} / p90 ${e.p90} / p99 ${e.p99} / max ${e.max} ms`);
1511
- if (summary.errorMessages.length > 0) {
1512
- lines.push(` ${style.bold("Top errors:")}`);
1513
- for (const { message, count } of summary.errorMessages) lines.push(` ${count}× ${message}`);
1514
- }
1515
- if (summary.toolErrorMessages.length > 0) {
1516
- lines.push(` ${style.bold("Top tool errors:")}`);
1517
- for (const { message, count } of summary.toolErrorMessages) lines.push(` ${count}× ${message}`);
1518
- }
1519
- if (summary.stoppedEarly !== void 0) lines.push(` ${style.red("Stopped early")} (${summary.stoppedEarly.reason}): ${summary.stoppedEarly.detail}`);
1520
- const c = summary.connections;
1521
- if (c !== void 0) {
1522
- const failed = `${c.failed} failed`;
1523
- const connect = c.connectMs === null ? "" : ` (connect p50 ${c.connectMs.p50} / max ${c.connectMs.max} ms)`;
1524
- lines.push(` Sessions: ${c.opened} of ${c.requested} opened, ${c.failed > 0 ? style.red(failed) : failed}${connect}`);
1525
- for (const { message, count } of c.errorMessages) lines.push(` ${count}× ${message}`);
1526
- }
1527
- return lines.join("\n");
1528
- }
1529
2205
  function formatRpcResultHuman(method, result, style = PLAIN) {
1530
2206
  switch (method) {
1531
2207
  case "tools/list": return formatToolsHuman(asArray(result.tools), style);
@@ -1550,8 +2226,7 @@ function formatRpcResultHuman(method, result, style = PLAIN) {
1550
2226
  default: return null;
1551
2227
  }
1552
2228
  }
1553
- //#endregion
1554
- //#region src/connection/format-connection.ts
2229
+ // src/connection/format-connection.ts
1555
2230
  function formatConnectionJson(data) {
1556
2231
  return JSON.stringify(data, null, 2).replace(/[\u0080-\u009F]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`) + "\n";
1557
2232
  }
@@ -1600,6 +2275,7 @@ function jsonPayload(payload) {
1600
2275
  case "daemon/stop": return payload.result;
1601
2276
  case "auth/list": return payload.list;
1602
2277
  case "auth/clear": return payload.result;
2278
+ case "auth/show": return payload.result;
1603
2279
  case "auth/ema-status": return payload.status;
1604
2280
  case "auth/ema-login": return payload.result;
1605
2281
  case "auth/ema-logout": return payload.result;
@@ -1673,6 +2349,7 @@ function humanPayload(payload, style, interactive) {
1673
2349
  case "auth/clear":
1674
2350
  if (payload.result.all === true) return style.green(`Cleared ${String(payload.result.cleared ?? 0)} stored auth entr${payload.result.cleared === 1 ? "y" : "ies"}.`);
1675
2351
  return `${style.green("Cleared")} \`${style.bold(String(payload.result.url ?? ""))}\`${payload.result.clearedByName ? style.dim(` (${String(payload.result.clearedByName)})`) : ""}`;
2352
+ case "auth/show": return formatAuthShowHuman(payload.result, payload.nowMs, style);
1676
2353
  case "auth/ema-status": return formatEmaStatusHuman(payload.status, style);
1677
2354
  case "auth/ema-login":
1678
2355
  if (payload.result.alreadyLoggedIn) return `${style.green("Already signed in")} to \`${style.bold(payload.result.issuer)}\` ${style.dim("(use auth/ema-login --relogin for a fresh connection)")}`;
@@ -1714,8 +2391,7 @@ function applyExitCodes(payload) {
1714
2391
  if (payload.result.isError === true) throw new CliExitCodeError(EXIT_CODES.TOOL_ERROR, `Tool '${sanitizeText(payload.toolName ?? "tool")}' returned isError:true.`, { code: "tool_is_error" });
1715
2392
  }
1716
2393
  }
1717
- //#endregion
1718
- //#region src/connection/form-prompt.ts
2394
+ // src/connection/form-prompt.ts
1719
2395
  function watchForClose(rl) {
1720
2396
  return new Promise((_, reject) => {
1721
2397
  rl.once("close", () => reject(/* @__PURE__ */ new Error("stdin closed before an answer was given")));
@@ -1857,8 +2533,7 @@ async function promptForm(rl, message, fields, style) {
1857
2533
  values.set(field.name, await promptField(rl, closed, field, style));
1858
2534
  }
1859
2535
  }
1860
- //#endregion
1861
- //#region src/connection/prompt-reader.ts
2536
+ // src/connection/prompt-reader.ts
1862
2537
  function exhaustedError() {
1863
2538
  return /* @__PURE__ */ new Error("stdin closed before an answer was given");
1864
2539
  }
@@ -1947,8 +2622,7 @@ function getSharedPromptReader() {
1947
2622
  shared ??= new PromptReader();
1948
2623
  return shared;
1949
2624
  }
1950
- //#endregion
1951
- //#region src/connection/elicitation-prompt.ts
2625
+ // src/connection/elicitation-prompt.ts
1952
2626
  function cancelResponse(frame) {
1953
2627
  return {
1954
2628
  id: frame.id,
@@ -2013,10 +2687,15 @@ async function promptElicitation(frame, opts) {
2013
2687
  return cancelResponse(frame);
2014
2688
  }
2015
2689
  }
2016
- //#endregion
2017
- //#region src/connection/dispatch.ts
2690
+ // src/connection/dispatch.ts
2018
2691
  const STREAM_METHODS = /* @__PURE__ */ new Set(["logging/tail", "resources/subscribe"]);
2019
2692
  const NDJSON_VARIANTS = /* @__PURE__ */ new Set(["skills/list", "skills/get"]);
2693
+ function loadOptionsFor(load, format) {
2694
+ if (format === "text") return load.options;
2695
+ const rest = { ...load.options };
2696
+ delete rest.progressMs;
2697
+ return rest;
2698
+ }
2020
2699
  async function dispatchConnectionRpc(method, methodArgs, opts) {
2021
2700
  const format = opts.format ?? "text";
2022
2701
  const style = styleFromOpts({
@@ -2028,7 +2707,7 @@ async function dispatchConnectionRpc(method, methodArgs, opts) {
2028
2707
  method,
2029
2708
  name: stripAt(opts.connection),
2030
2709
  requireExplicit: opts.requireExplicit,
2031
- ...opts.load !== void 0 && { load: opts.load.options }
2710
+ ...opts.load !== void 0 && { load: loadOptionsFor(opts.load, format) }
2032
2711
  };
2033
2712
  const { socketPath } = await ensureDaemon();
2034
2713
  if (STREAM_METHODS.has(method)) {
@@ -2072,6 +2751,10 @@ async function dispatchConnectionRpc(method, methodArgs, opts) {
2072
2751
  socketPath,
2073
2752
  timeoutMs: 0,
2074
2753
  signal: ac.signal,
2754
+ onLoadProgress: (progress) => {
2755
+ if (process.stderr.writableNeedDrain) return;
2756
+ process.stderr.write(`${formatLoadProgress(progress)}\n`);
2757
+ },
2075
2758
  onElicitation: (frame) => promptElicitation(frame, {
2076
2759
  style,
2077
2760
  interactive
@@ -2087,7 +2770,11 @@ async function dispatchConnectionRpc(method, methodArgs, opts) {
2087
2770
  }, method, methodArgs.toolName, outcome);
2088
2771
  if (outcome.kind === "load") {
2089
2772
  for (const warning of loadSummaryWarnings(outcome.summary)) await awaitableError(`${warning}\n`);
2773
+ const baseline = opts.load?.baseline;
2774
+ const comparison = baseline !== void 0 ? compareLoadToBaseline(outcome.summary, baseline.baseline) : void 0;
2775
+ for (const line of comparison !== void 0 ? formatLoadBaselineComparison(comparison) : []) await awaitableError(`${line}\n`);
2090
2776
  checkLoadThresholds(outcome.summary, opts.load?.thresholds);
2777
+ if (comparison !== void 0) checkLoadRegression(comparison, baseline.maxRegressionPct);
2091
2778
  }
2092
2779
  }
2093
2780
  async function writeRpcOutcome(out, method, toolName, outcome) {
@@ -2167,8 +2854,7 @@ function hoistAtConnection(argv) {
2167
2854
  }
2168
2855
  return { argv };
2169
2856
  }
2170
- //#endregion
2171
- //#region src/connection/private-env.ts
2857
+ // src/connection/private-env.ts
2172
2858
  function createPrivateBinding() {
2173
2859
  return {
2174
2860
  dir: createPrivateDaemonDir(),
@@ -2185,8 +2871,7 @@ function formatPrivateEnvExports(binding) {
2185
2871
  function shellSingleQuote(value) {
2186
2872
  return `'${value.replace(/'/g, `'\\''`)}'`;
2187
2873
  }
2188
- //#endregion
2189
- //#region src/connection/stored-auth.ts
2874
+ // src/connection/stored-auth.ts
2190
2875
  function normalizeServerUrl(serverUrl) {
2191
2876
  try {
2192
2877
  return new URL(serverUrl).href;
@@ -2238,6 +2923,21 @@ async function resolveStoredAuthKey(key) {
2238
2923
  if (normalized !== trimmed || /^https?:\/\//i.test(trimmed)) return normalized;
2239
2924
  throw new CliExitCodeError(EXIT_CODES.USAGE, `No stored auth entry for '${trimmed}'. Use auth/list to see keys.`, { code: "usage" });
2240
2925
  }
2926
+ async function showStoredAuth(key) {
2927
+ const url = await resolveStoredAuthKey(key);
2928
+ const tokens = await new NodeOAuthStorage().getTokens(url);
2929
+ if (!tokens?.access_token) return {
2930
+ url,
2931
+ hasTokens: false,
2932
+ hasRefreshToken: false
2933
+ };
2934
+ return {
2935
+ url,
2936
+ hasTokens: true,
2937
+ hasRefreshToken: Boolean(tokens.refresh_token),
2938
+ accessToken: summarizeAccessToken(tokens)
2939
+ };
2940
+ }
2241
2941
  async function clearStoredAuth(key) {
2242
2942
  const url = await resolveStoredAuthKey(key);
2243
2943
  await new NodeOAuthStorage().clear(url);
@@ -2256,8 +2956,7 @@ async function clearStoredAuthForRelogin(serverUrl) {
2256
2956
  await new NodeOAuthStorage().clear(url);
2257
2957
  resetNodeOAuthStorageCache();
2258
2958
  }
2259
- //#endregion
2260
- //#region src/connection/auth-names.ts
2959
+ // src/connection/auth-names.ts
2261
2960
  function urlIfHttp(value) {
2262
2961
  if (value && /^https?:\/\//i.test(value)) return normalizeServerUrl(value);
2263
2962
  }
@@ -2322,8 +3021,7 @@ function resolveFriendlyName(name, index) {
2322
3021
  name
2323
3022
  };
2324
3023
  }
2325
- //#endregion
2326
- //#region src/connection/mcp.ts
3024
+ // src/connection/mcp.ts
2327
3025
  function isDaemonUnreachable(error) {
2328
3026
  return error instanceof CliExitCodeError && error.envelope?.code === "daemon_unreachable";
2329
3027
  }
@@ -2486,7 +3184,7 @@ function registerConnect(program) {
2486
3184
  "both"
2487
3185
  ].includes(value)) throw new Error(`Invalid --elicit: ${value}. Use off, url, form, or both.`);
2488
3186
  return value;
2489
- }).option("--ema", "Treat the server as enterprise-managed (EMA): mint tokens from the signed-in enterprise IdP session instead of standard OAuth. Overrides the catalog/config entry's oauth.enterpriseManaged. Requires install-level IdP config (see auth/ema-status) and per-server OAuth client id/secret from the catalog entry, so it cannot be used with an ad-hoc target.").action(async (target, cmdOpts) => {
3187
+ }).option("--ema", "Treat the server as enterprise-managed (EMA): mint tokens from the signed-in enterprise IdP session instead of standard OAuth. Overrides the catalog/config entry's oauth.enterpriseManaged. Requires install-level IdP config (see auth/ema-status) and per-server OAuth client id/secret from the catalog entry, so it cannot be used with an ad-hoc target.").option("--oauth-debug", "Print each OAuth step to stderr: protected-resource and authorization-server metadata URLs and statuses, the client registration request and response, the authorization URL, and the token request and response, with tokens, codes and secrets redacted. Runs the sign-in in this process before the daemon connects, so the whole flow is traced here (an HTTP/SSE server with a valid stored token makes no OAuth requests; add --relogin to trace a fresh login).").action(async (target, cmdOpts) => {
2490
3188
  const opts = program.opts();
2491
3189
  const { name: positionalConnection, rest } = splitConnectionTarget(target);
2492
3190
  const connectionName = stripAt(opts.connection) ?? positionalConnection ?? cmdOpts.server?.trim() ?? rest[0];
@@ -2515,6 +3213,10 @@ function registerConnect(program) {
2515
3213
  const { detail } = summarizeServerConfig(serverConfig);
2516
3214
  const name = stripAt(connectionName);
2517
3215
  if (relogin && "url" in serverConfig && serverConfig.url) await clearStoredAuthForRelogin(serverConfig.url);
3216
+ if (cmdOpts.oauthDebug === true) await authorizeInFrontend(serverConfig, serverSettings, {
3217
+ storedAuthOnly: opts.storedAuthOnly === true,
3218
+ oauthDebug: true
3219
+ });
2518
3220
  const { socketPath } = await ensureDaemon();
2519
3221
  const connectParams = {
2520
3222
  name,
@@ -2531,7 +3233,7 @@ function registerConnect(program) {
2531
3233
  } catch (error) {
2532
3234
  if (!(error instanceof CliExitCodeError) || error.envelope?.code !== "auth_required") throw error;
2533
3235
  if (opts.storedAuthOnly) throw error;
2534
- if (!(process.stdin.isTTY === true || process.stderr.isTTY === true) && !isCliAutoOpenForced()) {
3236
+ if (!(process.stdin.isTTY === true || process.stderr.isTTY === true) && !isCliAutoOpenForced() && cmdOpts.oauthDebug !== true) {
2535
3237
  const authUrl = await obtainPendingAuthUrl(serverConfig, serverSettings);
2536
3238
  const { socketPath: pendingSocketPath } = await ensureDaemon();
2537
3239
  const pending = await callDaemon("connect", {
@@ -2548,7 +3250,10 @@ function registerConnect(program) {
2548
3250
  });
2549
3251
  return;
2550
3252
  }
2551
- await authorizeInFrontend(serverConfig, serverSettings, { storedAuthOnly: false });
3253
+ await authorizeInFrontend(serverConfig, serverSettings, {
3254
+ storedAuthOnly: false,
3255
+ oauthDebug: cmdOpts.oauthDebug === true
3256
+ });
2552
3257
  const { socketPath: freshSocketPath } = await ensureDaemon();
2553
3258
  result = await callDaemon("connect", connectParams, {
2554
3259
  socketPath: freshSocketPath,
@@ -2575,6 +3280,24 @@ async function collectAuthNameIndex(opts) {
2575
3280
  }
2576
3281
  return buildAuthNameIndex(entries, connections);
2577
3282
  }
3283
+ async function resolveAuthKeyArgument(trimmed, opts, action) {
3284
+ const storedKeys = (await listStoredAuth()).servers.map((s) => s.url);
3285
+ if (storedKeys.includes(trimmed)) return { key: trimmed };
3286
+ if (/^https?:\/\//i.test(trimmed)) {
3287
+ const idpKey = idpOAuthStorageKey(trimmed);
3288
+ return { key: storedKeys.includes(idpKey) && !storedKeys.includes(normalizeServerUrl(trimmed)) ? idpKey : trimmed };
3289
+ }
3290
+ const resolution = resolveFriendlyName(trimmed, await collectAuthNameIndex(opts));
3291
+ switch (resolution.kind) {
3292
+ case "no-url": throw new CliExitCodeError(EXIT_CODES.USAGE, `'${trimmed}' is a stdio server — it has no stored OAuth entry to ${action}.`, { code: "usage" });
3293
+ case "ambiguous": throw new CliExitCodeError(EXIT_CODES.USAGE, `'${trimmed}' maps to multiple server URLs (${resolution.urls.join(", ")}). Pass the explicit URL from auth/list.`, { code: "usage" });
3294
+ case "unknown": throw new CliExitCodeError(EXIT_CODES.USAGE, `No server named '${trimmed}'. Use auth/list or servers/list to see names, or pass a URL.`, { code: "usage" });
3295
+ case "url": return {
3296
+ key: resolution.url,
3297
+ byName: trimmed
3298
+ };
3299
+ }
3300
+ }
2578
3301
  function registerAuthCommands(program) {
2579
3302
  program.command(AUTH_HELPER_COMMAND, { hidden: true }).description("Internal: complete an OAuth sign-in (params JSON on stdin)").action(async () => {
2580
3303
  await runAuthHelper();
@@ -2638,42 +3361,30 @@ function registerAuthCommands(program) {
2638
3361
  });
2639
3362
  return;
2640
3363
  }
2641
- const trimmed = key.trim();
2642
- const storedKeys = (await listStoredAuth()).servers.map((s) => s.url);
2643
- if (storedKeys.includes(trimmed)) {
2644
- const result = await clearStoredAuth(trimmed);
2645
- await writeConnectionOutput(outOpts(opts), {
2646
- kind: "auth/clear",
2647
- result: { url: result.url }
2648
- });
2649
- return;
2650
- }
2651
- if (/^https?:\/\//i.test(trimmed)) {
2652
- const idpKey = idpOAuthStorageKey(trimmed);
2653
- const result = await clearStoredAuth(storedKeys.includes(idpKey) && !storedKeys.includes(normalizeServerUrl(trimmed)) ? idpKey : trimmed);
2654
- await writeConnectionOutput(outOpts(opts), {
2655
- kind: "auth/clear",
2656
- result: { url: result.url }
2657
- });
2658
- return;
2659
- }
2660
- const resolution = resolveFriendlyName(trimmed, await collectAuthNameIndex(opts));
2661
- switch (resolution.kind) {
2662
- case "no-url": throw new CliExitCodeError(EXIT_CODES.USAGE, `'${trimmed}' is a stdio server — it has no stored OAuth entry to clear.`, { code: "usage" });
2663
- case "ambiguous": throw new CliExitCodeError(EXIT_CODES.USAGE, `'${trimmed}' maps to multiple server URLs (${resolution.urls.join(", ")}). Pass the explicit URL from auth/list.`, { code: "usage" });
2664
- case "unknown": throw new CliExitCodeError(EXIT_CODES.USAGE, `No server named '${trimmed}'. Use auth/list or servers/list to see names, or pass a URL.`, { code: "usage" });
2665
- case "url": {
2666
- const result = await clearStoredAuth(resolution.url);
2667
- await writeConnectionOutput(outOpts(opts), {
2668
- kind: "auth/clear",
2669
- result: {
2670
- url: result.url,
2671
- clearedByName: trimmed
2672
- }
2673
- });
2674
- return;
3364
+ const target = await resolveAuthKeyArgument(key.trim(), opts, "clear");
3365
+ const result = await clearStoredAuth(target.key);
3366
+ await writeConnectionOutput(outOpts(opts), {
3367
+ kind: "auth/clear",
3368
+ result: {
3369
+ url: result.url,
3370
+ ...target.byName !== void 0 && { clearedByName: target.byName }
2675
3371
  }
2676
- }
3372
+ });
3373
+ });
3374
+ program.command("auth/show").description("Show the stored access token's decoded claims (issuer, audience, scopes, expiry) for one server — never the token itself").argument("<key>", "Server URL or catalog/connection name from auth/list").action(async (key) => {
3375
+ const opts = program.opts();
3376
+ const trimmed = key.trim();
3377
+ if (!trimmed) throw new CliExitCodeError(EXIT_CODES.USAGE, "auth/show requires a server URL key (from auth/list)", { code: "usage" });
3378
+ const target = await resolveAuthKeyArgument(trimmed, opts, "show");
3379
+ const result = await showStoredAuth(target.key);
3380
+ await writeConnectionOutput(outOpts(opts), {
3381
+ kind: "auth/show",
3382
+ result: {
3383
+ ...result,
3384
+ ...target.byName !== void 0 && { shownByName: target.byName }
3385
+ },
3386
+ nowMs: Date.now()
3387
+ });
2677
3388
  });
2678
3389
  program.command("auth/ema-status").description("Show enterprise-managed auth (EMA) configuration and IdP login state").action(async () => {
2679
3390
  const opts = program.opts();
@@ -2863,7 +3574,7 @@ function registerRpcCommands(program) {
2863
3574
  });
2864
3575
  break;
2865
3576
  case "tools/call":
2866
- cmd.argument("[toolName]", "Tool name").argument("[toolArgs...]", "Arguments as key:=value pairs or a JSON object").option("--tool-name <name>", "Tool name").option("--tool-arg <pairs...>", "Tool argument as key=value pair (alternative to key:=value positionals)", parseKeyValue, {}).option("--tool-args-json <json>", "Tool arguments as a JSON object (alternative to inline JSON positional)").option("--tool-metadata <pairs...>", "Tool-specific metadata", parseKeyValue, {}).option("--task", "Request a task-augmented tool call (legacy servers; on modern the server decides)").option("--app-info", "Probe MCP App metadata only").option("--load", "Load-test the tool: call it repeatedly and concurrently, then print a latency and error-rate summary instead of a result. Bounded by --load-duration (default 10s) and/or --load-requests. Later commands on this connection wait until the run ends.").option("--load-concurrency <n>", "With --load: most calls in flight at once (positive integer; default 10).", positiveIntegerParser("--load-concurrency")).option("--load-connections <n>", "With --load: spread the calls over this many independent sessions to the connection's HTTP or SSE server, opened before timing starts with stored credentials only (positive integer, at most --load-concurrency; default 1).", positiveIntegerParser("--load-connections")).option("--load-duration <sec>", "With --load: stop starting calls after this many seconds; in-flight calls still finish (default 10 when --load-requests is not set).", positiveNumberParser("--load-duration")).option("--load-requests <n>", "With --load: stop after starting this many calls (positive integer).", positiveIntegerParser("--load-requests")).option("--load-rate <n>", "With --load: start at most this many calls per second, spread evenly (positive number; default unlimited).", positiveNumberParser("--load-rate")).option("--load-max-error-rate <fraction>", "With --load: exit 10 (load_error_rate_exceeded) when the share of failed calls is above this value, after printing the summary (0 to 1; 0 fails on any failed call).", fractionParser("--load-max-error-rate")).option("--load-max-p99 <ms>", "With --load: exit 10 (load_p99_exceeded) when the p99 latency of the successful calls is above this many milliseconds, or when no call succeeded, after printing the summary (positive number).", maxP99Parser);
3577
+ cmd.argument("[toolName]", "Tool name").argument("[toolArgs...]", "Arguments as key:=value pairs or a JSON object").option("--tool-name <name>", "Tool name").option("--tool-arg <pairs...>", "Tool argument as key=value pair (alternative to key:=value positionals)", parseKeyValue, {}).option("--tool-args-json <json>", "Tool arguments as a JSON object (alternative to inline JSON positional)").option("--tool-metadata <pairs...>", "Tool-specific metadata", parseKeyValue, {}).option("--task", "Request a task-augmented tool call (legacy servers; on modern the server decides)").option("--app-info", "Probe MCP App metadata only").option("--load", "Load-test the tool: call it repeatedly and concurrently, then print a latency and error-rate summary instead of a result. Bounded by --load-duration (default 10s) and/or --load-requests. Later commands on this connection wait until the run ends.").option("--load-scenario <file>", "With --load: run the weighted mix of tools/call, resources/read and prompts/get steps in this JSON file instead of one tool, and break the summary down per step. Replaces the tool name, its arguments and --tool-metadata.").option("--load-args-file <file>", "With --load: NDJSON file of argument objects, one per line; each call merges the next row over the tool arguments (row wins), cycling. Placeholders {{seq}}, {{worker}}, {{uuid}} and {{rand:<min>-<max>}} work in rows and in argument values.").option("--load-args-random", "With --load-args-file: pick each call's row at random instead of in turn.").option("--load-steps <list>", "With --load: run consecutive stages at these levels (comma-separated, e.g. 10,25,50,100), each for --load-step-duration, with one summary row per stage. Levels are concurrency unless --load-step-by rate.", loadStepsParser).option("--load-ramp <from:to:sec>", "With --load: a staircase of stages from <from> to <to> over <sec> seconds, one stage per --load-step-duration, levels spaced evenly (concurrency unless --load-step-by rate).", loadRampParser).option("--load-step-by <what>", "With --load-steps or --load-ramp: what each level sets, concurrency (default) or rate (calls per second over --load-concurrency workers).", loadStepByParser).option("--load-step-duration <sec>", "With --load-steps or --load-ramp: how long each stage runs (default 10).", positiveNumberParser("--load-step-duration")).option("--load-interval <sec>", "With --load: also report the run as a time series of buckets this many seconds long (completions, errors, latency per bucket) in an intervals array.", positiveNumberParser("--load-interval")).option("--load-warmup <sec>", "With --load: run this many seconds first at the same concurrency and rate, and leave those calls out of every statistic.", positiveNumberParser("--load-warmup")).option("--load-progress <sec>", "With --load: print a progress line on stderr this often (default 5). Shown with --format text only; --format json prints none.", positiveNumberParser("--load-progress")).option("--load-open-loop", "With --load-rate: open-loop mode. Keep every slot of the rate grid instead of dropping the ones no worker is free for, and time each call from its scheduled start, so queueing shows in the latency.").option("--load-stop-on-breach", "With --load-steps or --load-ramp and a --load-max-* threshold: stop after the first stage that breaches it (the run still exits 10).").option("--load-subscribe <uri>", "With --load: notification fan-out. Subscribe --load-subscribers sessions to this resource and report how notifications/resources/updated reaches them: per-notification delivery latency, misses per session, and subscribe cost. With a tool name (or --load-scenario) each successful call is a trigger and latency runs from its start; without one the run listens for --load-duration to the server's own updates. Triggers run one at a time.").option("--load-subscribers <n>", "With --load-subscribe: sessions to subscribe, this connection included (positive integer; default 1; above 1 needs an HTTP or SSE connection, and opens the rest with stored credentials).", positiveIntegerParser("--load-subscribers")).option("--load-subscribe-wait <sec>", "With --load-subscribe: how long subscribers get to catch up after the run before a notification counts as missed (default 2).", positiveNumberParser("--load-subscribe-wait")).option("--load-churn", "With --load: session churn. Each worker opens a session to the connection's HTTP or SSE server with stored credentials, makes --load-churn-calls calls, closes it, and repeats; connect times and failures are reported apart from call latency. Not with --load-connections.").option("--load-churn-calls <n>", "With --load-churn: calls per session before it is closed (positive integer; default 1).", positiveIntegerParser("--load-churn-calls")).option("--load-concurrency <n>", "With --load: most calls in flight at once (positive integer; default 10).", positiveIntegerParser("--load-concurrency")).option("--load-connections <n>", "With --load: spread the calls over this many independent sessions to the connection's HTTP or SSE server, opened before timing starts with stored credentials only (positive integer, at most --load-concurrency; default 1).", positiveIntegerParser("--load-connections")).option("--load-duration <sec>", "With --load: stop starting calls after this many seconds; in-flight calls still finish (default 10 when --load-requests is not set).", positiveNumberParser("--load-duration")).option("--load-requests <n>", "With --load: stop after starting this many calls (positive integer).", positiveIntegerParser("--load-requests")).option("--load-rate <n>", "With --load: start at most this many calls per second, spread evenly (positive number; default unlimited).", positiveNumberParser("--load-rate")).option("--load-max-error-rate <fraction>", "With --load: exit 10 (load_error_rate_exceeded) when the share of failed calls is above this value, after printing the summary (0 to 1; 0 fails on any failed call).", fractionParser("--load-max-error-rate")).option("--load-max-p99 <ms>", "With --load: exit 10 (load_p99_exceeded) when the p99 latency of the successful calls is above this many milliseconds, or when no call succeeded, after printing the summary (positive number).", maxP99Parser).option("--load-max-p50 <ms>", "With --load: exit 10 (load_p50_exceeded) when the p50 latency of the successful calls is above this many milliseconds, or when no call succeeded, after printing the summary (positive number).", maxP50Parser).option("--load-max-p95 <ms>", "With --load: exit 10 (load_p95_exceeded) when the p95 latency of the successful calls is above this many milliseconds, or when no call succeeded, after printing the summary (positive number).", maxP95Parser).option("--load-min-throughput <n>", "With --load: exit 10 (load_throughput_below) when fewer than this many calls per second succeeded (okThroughput), after printing the summary (positive number).", minThroughputParser).option("--load-baseline <file>", "With --load: compare the run's p50, p99, error rate and ok throughput against a summary saved from an earlier run with --format json, printing the comparison on stderr. The runs must be shaped alike (same tool or scenario, concurrency, bounds, rate, connections and stages), or the run exits 1 (load_baseline_mismatch).").option("--load-max-regression <pct>", "With --load-baseline: exit 10 (load_regression) when p50 or p99 is higher, or ok throughput lower, than the baseline by more than this percentage, or the error rate higher by more than this many percentage points (e.g. 10 or 10%).", maxRegressionParser);
2867
3578
  cmd.action(async (toolNamePos, toolArgsPos, o) => {
2868
3579
  const { toolName, toolArg } = resolveToolCallArgs({
2869
3580
  toolNameFlag: o.toolName,
@@ -2874,7 +3585,9 @@ function registerRpcCommands(program) {
2874
3585
  });
2875
3586
  const load = resolveLoadFlags({
2876
3587
  ...o,
2877
- toolName
3588
+ toolName,
3589
+ toolArg,
3590
+ toolMetadata: o.toolMetadata
2878
3591
  });
2879
3592
  await runRpc(program, method, {
2880
3593
  toolName,
@@ -2927,7 +3640,7 @@ function registerRpcCommands(program) {
2927
3640
  });
2928
3641
  break;
2929
3642
  case "prompts/get":
2930
- cmd.argument("[promptName]", "Prompt name").option("--prompt-name <name>", "Prompt name").option("--prompt-args <pairs...>", "Prompt arguments", parseKeyValue, {});
3643
+ cmd.argument("[promptName]", "Prompt name").option("--prompt-name <name>", "Prompt name").option("--prompt-args <pairs...>", "Prompt arguments as key=value pairs (values are sent verbatim as strings)", parsePromptArgPair, {});
2931
3644
  cmd.action(async (promptPos, o) => {
2932
3645
  await runRpc(program, method, {
2933
3646
  promptName: o.promptName ?? promptPos,
@@ -3133,8 +3846,7 @@ function splitConnectionTarget(target) {
3133
3846
  rest: target
3134
3847
  };
3135
3848
  }
3136
- //#endregion
3137
- //#region src/mcp-bin.ts
3849
+ // src/mcp-bin.ts
3138
3850
  disallowMemorySecretStoreFallback();
3139
3851
  setSecretStorageWarningsQuiet(true);
3140
3852
  const __filename = fileURLToPath(import.meta.url);
@@ -3148,5 +3860,4 @@ function isMainModule() {
3148
3860
  }
3149
3861
  }
3150
3862
  if (isMainModule()) runMcp(process.argv).then(() => process.exit(0)).catch(handleError);
3151
- //#endregion
3152
3863
  export { runMcp };