@modelcontextprotocol/inspector 2.13.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.
Files changed (34) hide show
  1. package/README.md +1 -1
  2. package/clients/cli/build/index.js +5481 -1847
  3. package/clients/mcpdo/build/mcp-bin.js +925 -186
  4. package/clients/mcpdo/build/mcpdod.js +2 -3
  5. package/clients/mcpdo/build/{server-BHuKGkMj.js → server-B-CUVIBj.js} +3544 -995
  6. package/clients/tui/build/index.js +2126 -570
  7. package/clients/web/build/index.js +459 -280
  8. package/clients/web/dist/assets/fredoka-normal-hebrew-CjvktpUN.woff2 +0 -0
  9. package/clients/web/dist/assets/fredoka-normal-latin-DOQG_Jwn.woff2 +0 -0
  10. package/clients/web/dist/assets/fredoka-normal-latin-ext-tYNUQHO6.woff2 +0 -0
  11. package/clients/web/dist/assets/index-BZ94R6fh.js +1599 -0
  12. package/clients/web/dist/assets/{index-dqp8Sd8W.css → index-tBehH5iF.css} +1 -1
  13. package/clients/web/dist/assets/{prism-light-BIhJtJkv.js → prism-light-Crps--E8.js} +1 -1
  14. package/clients/web/dist/assets/roboto-mono-italic-cyrillic-C1E63de8.woff2 +0 -0
  15. package/clients/web/dist/assets/roboto-mono-italic-cyrillic-ext-DR9ovEqT.woff2 +0 -0
  16. package/clients/web/dist/assets/roboto-mono-italic-greek-Cs2u2nVh.woff2 +0 -0
  17. package/clients/web/dist/assets/roboto-mono-italic-latin-D_P-kX1t.woff2 +0 -0
  18. package/clients/web/dist/assets/roboto-mono-italic-latin-ext-CErTcBzw.woff2 +0 -0
  19. package/clients/web/dist/assets/roboto-mono-italic-vietnamese-D4UvzJMM.woff2 +0 -0
  20. package/clients/web/dist/assets/roboto-mono-normal-cyrillic-DfzWIUPd.woff2 +0 -0
  21. package/clients/web/dist/assets/roboto-mono-normal-cyrillic-ext-D8QBEkUe.woff2 +0 -0
  22. package/clients/web/dist/assets/roboto-mono-normal-greek-DlC4X1lQ.woff2 +0 -0
  23. package/clients/web/dist/assets/roboto-mono-normal-latin-BAKpfWyj.woff2 +0 -0
  24. package/clients/web/dist/assets/roboto-mono-normal-latin-ext-CSSVNFqV.woff2 +0 -0
  25. package/clients/web/dist/assets/roboto-mono-normal-vietnamese-DFkHeOHB.woff2 +0 -0
  26. package/clients/web/dist/assets/worker-json-DEGsizKa.js +7859 -0
  27. package/clients/web/dist/fonts/Fredoka-OFL.txt +93 -0
  28. package/clients/web/dist/fonts/RobotoMono-OFL.txt +93 -0
  29. package/clients/web/dist/index.html +2 -5
  30. package/clients/web/static/sandbox_proxy.html +177 -6
  31. package/package.json +8 -4
  32. package/skills/mcpdo/SKILL.md +63 -2
  33. package/clients/web/dist/assets/index-BqEbhCDX.js +0 -1587
  34. package/clients/web/dist/assets/worker-json-8CDXFfik.js +0 -1631
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as CliExitCodeError, A as sanitizeDeep, B as ensureDaemonDir, C as clearAllOAuthClientState, D as positiveNumberParser, E as positiveIntegerParser, F as readDaemonTokenFile, G as loadServerEntries, H as getDaemonLogPath, I as DAEMON_DIR_ENV, J as parseKeyValuePair, K as selectServerEntry, L as DAEMON_TOKEN_ENV, M as encodeRequest, N as generateDaemonToken, O as resolveLoadOptions, P as getDaemonTokenFromEnv, Q as DEFAULT_TASK_TTL_MS, R as assertSocketPathWithinLimit, S as NodeOAuthStorage, T as resetNodeOAuthStorageCache, U as getDaemonSocketPath, V as getDaemonDir, W as readInspectorVersion, X as withDefaultCatalogPath, Y as resolveServerSource, Z as DEFAULT_MAX_FETCH_REQUESTS, _ as isCliAutoOpenForced, a as obtainPendingUrlForKey, at as setSecretStorageWarningsQuiet, b as DEFAULT_CONNECT_TIMEOUT_MS, c as runAuthHelper, ct as CallbackNavigation, d as emaLogin, dt as idpOAuthStorageKey, et as EXIT_CODES, f as emaLogout, ft as normalizeIdpIssuer, g as runEmaIdpInteractiveFlow, h as requireIdp, ht as awaitableLog, i as obtainPendingAuthUrl, it as getSecretStorageInfo, j as sanitizeText, k as isSafeLinkTarget, l as writePendingAuthMarker, lt as clearEmaIdpSession, m as loadEmaIdpConfig, mt as awaitableError, n as AUTH_HELPER_COMMAND, nt as readOAuthStore, o as pendingAuthMarkerPath, ot as warnAboutSecretStorage, p as getEmaStatus, pt as parseIdpOAuthStorageKey, q as parseHeaderPair, r as PENDING_AUTH_TTL_MS, rt as disallowMemorySecretStoreFallback, s as removeOwnPendingAuthMarker, st as InMemorySecretStore, tt as handleError, u as authorizeInFrontend, ut as getEmaIdpLoginState, v as PLAIN, w as getStateFilePath, x as withConnectTimeout, y as styleFromOpts, z as createPrivateDaemonDir } from "./server-BHuKGkMj.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() {
@@ -304,14 +390,18 @@ async function waitForDaemon(socketPath, token, logPath, opts) {
304
390
  while (Date.now() < deadline) {
305
391
  if (await isDaemonReachable(socketPath)) {
306
392
  const effectiveToken = opts?.rereadTokenDir ? readDaemonTokenFile(opts.rereadTokenDir) ?? token : token;
393
+ let pong;
307
394
  try {
308
- await callDaemon("ping", {}, {
395
+ pong = await callDaemon("ping", {}, {
309
396
  socketPath,
310
397
  timeoutMs: 2e3,
311
398
  token: effectiveToken
312
399
  });
313
- return;
314
400
  } catch {}
401
+ if (pong !== void 0) {
402
+ assertDaemonVersion(pong.version, CLIENT_VERSION);
403
+ return;
404
+ }
315
405
  }
316
406
  await new Promise((r) => setTimeout(r, READY_POLL_MS));
317
407
  }
@@ -343,6 +433,12 @@ async function waitForDaemonExit(pid, socketPath, timeoutMs = STOPPING_EXIT_TIME
343
433
  await new Promise((r) => setTimeout(r, pollMs));
344
434
  }
345
435
  }
436
+ const CLIENT_VERSION = readInspectorVersionSafe(import.meta.url);
437
+ function assertDaemonVersion(daemonVersion, clientVersion) {
438
+ if (clientVersion === void 0 || daemonVersion === clientVersion) return;
439
+ const running = daemonVersion === void 0 ? "an older mcpdo release" : `mcpdo ${daemonVersion}`;
440
+ throw new CliExitCodeError(EXIT_CODES.UNREACHABLE, `The running connection daemon is from ${running}, but this is mcpdo ${clientVersion}. Run \`mcpdo daemon stop\` (its open connections will close), then re-run the command.`, { code: "daemon_version_mismatch" });
441
+ }
346
442
  async function ensureDaemon(options) {
347
443
  const dir = options?.dir ?? getDaemonDir();
348
444
  let token = options?.token ?? getDaemonTokenFromEnv();
@@ -353,13 +449,17 @@ async function ensureDaemon(options) {
353
449
  token ??= readDaemonTokenFile(dir);
354
450
  const pong = await callDaemon("ping", {}, {
355
451
  socketPath,
452
+ dir,
356
453
  timeoutMs: 2e3,
357
454
  token
358
455
  });
359
- if (pong?.stopping !== true) return {
360
- socketPath,
361
- spawned: false
362
- };
456
+ if (pong?.stopping !== true) {
457
+ assertDaemonVersion(pong?.version, CLIENT_VERSION);
458
+ return {
459
+ socketPath,
460
+ spawned: false
461
+ };
462
+ }
363
463
  await waitForDaemonExit(pong.pid, socketPath);
364
464
  }
365
465
  const tokenWasGenerated = token === void 0;
@@ -396,8 +496,7 @@ async function ensureDaemon(options) {
396
496
  spawned: true
397
497
  };
398
498
  }
399
- //#endregion
400
- //#region ../../core/cli/handlers/servers-list.ts
499
+ // ../../core/cli/handlers/servers-list.ts
401
500
  function resolveServerListSource(serverOptions = {}) {
402
501
  const source = resolveServerSource(withDefaultCatalogPath(serverOptions));
403
502
  if (!source) return null;
@@ -511,8 +610,7 @@ function isSensitiveHeader(key) {
511
610
  const k = key.toLowerCase();
512
611
  return k.includes("auth") || k.includes("cookie") || k.includes("secret") || k.includes("token") || k.includes("password") || k.includes("api-key") || k.includes("apikey");
513
612
  }
514
- //#endregion
515
- //#region ../../core/cli/handlers/method-types.ts
613
+ // ../../core/cli/handlers/method-types.ts
516
614
  const CONNECTION_RPC_METHODS = [
517
615
  "tools/list",
518
616
  "tools/call",
@@ -537,58 +635,369 @@ const CONNECTION_RPC_METHODS = [
537
635
  "skills/list",
538
636
  "skills/get"
539
637
  ];
540
- //#endregion
541
- //#region ../../core/cli/handlers/load-thresholds.ts
542
- const LOAD_ERROR_RATE_EXCEEDED = "load_error_rate_exceeded";
543
- const LOAD_P99_EXCEEDED = "load_p99_exceeded";
544
- function fractionParser(flag) {
545
- return (value) => {
546
- const n = value.trim() === "" ? NaN : Number(value);
547
- if (!Number.isFinite(n) || n < 0 || n > 1) throw new Error(`${flag} must be a number from 0 to 1.`);
548
- 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 }
549
650
  };
550
651
  }
551
- const maxP99Parser = positiveNumberParser("--load-max-p99");
552
- function resolveLoadThresholds(flags) {
553
- const { maxErrorRate, maxP99Ms } = flags;
554
- 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`);
555
704
  return {
556
- ...maxErrorRate !== void 0 && { maxErrorRate },
557
- ...maxP99Ms !== void 0 && { maxP99Ms }
705
+ source,
706
+ summary
558
707
  };
559
708
  }
560
- function exactErrorRate(summary) {
561
- 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);
562
720
  }
563
- function errorRateBreach(summary, max) {
564
- if (exactErrorRate(summary) <= max) return void 0;
565
- 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;
566
827
  return {
567
- code: LOAD_ERROR_RATE_EXCEEDED,
568
- 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")
569
832
  };
570
833
  }
571
- function p99Breach(summary, max) {
572
- const p99 = summary.latencyMs?.p99;
573
- if (p99 === void 0) return {
574
- code: LOAD_P99_EXCEEDED,
575
- message: `no call completed, 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
+ }]
576
876
  };
577
- 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.");
578
950
  return {
579
- code: LOAD_P99_EXCEEDED,
580
- message: `p99 latency ${p99}ms is above --load-max-p99 ${max}ms`
951
+ from,
952
+ to,
953
+ durationSec
581
954
  };
582
955
  }
583
- function checkLoadThresholds(summary, thresholds) {
584
- if (!thresholds) return;
585
- 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);
586
- const [first] = breaches;
587
- if (!first) return;
588
- 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
+ };
589
999
  }
590
- //#endregion
591
- //#region src/connection/load-flags.ts
1000
+ // src/connection/load-flags.ts
592
1001
  function resolveLoadFlags(flags) {
593
1002
  const bounds = [
594
1003
  ["--load-concurrency", flags.loadConcurrency],
@@ -596,41 +1005,118 @@ function resolveLoadFlags(flags) {
596
1005
  ["--load-duration", flags.loadDuration],
597
1006
  ["--load-requests", flags.loadRequests],
598
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],
599
1025
  ["--load-max-error-rate", flags.loadMaxErrorRate],
600
- ["--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]
601
1032
  ];
602
1033
  if (flags.load !== true) {
603
1034
  for (const [flag, value] of bounds) if (value !== void 0) throw usage(`${flag} requires --load.`);
604
1035
  return;
605
1036
  }
606
1037
  if (flags.appInfo === true) throw usage("--load cannot be combined with --app-info.");
607
- 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
+ });
608
1061
  let options;
609
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
+ });
610
1075
  options = resolveLoadOptions({
611
1076
  concurrency: flags.loadConcurrency,
612
1077
  connections: flags.loadConnections,
613
1078
  durationSec: flags.loadDuration,
614
1079
  requests: flags.loadRequests,
615
- 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
616
1093
  });
617
1094
  } catch (err) {
618
1095
  throw usage(err.message);
619
1096
  }
620
- const thresholds = resolveLoadThresholds({
621
- maxErrorRate: flags.loadMaxErrorRate,
622
- maxP99Ms: flags.loadMaxP99
1097
+ assertFitsOneRequest(options);
1098
+ const baseline = resolveLoadBaseline({
1099
+ baseline: flags.loadBaseline,
1100
+ maxRegressionPct: flags.loadMaxRegression
623
1101
  });
624
1102
  return {
625
1103
  options,
626
- ...thresholds && { thresholds }
1104
+ ...thresholds && { thresholds },
1105
+ ...baseline && { baseline }
627
1106
  };
628
1107
  }
629
- function usage(message, code = "load_flag_conflict") {
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
+ }
1116
+ function usage(message, code = LOAD_FLAG_CONFLICT) {
630
1117
  return new CliExitCodeError(EXIT_CODES.USAGE, message, { code });
631
1118
  }
632
- //#endregion
633
- //#region src/connection/ema-login-helper.ts
1119
+ // src/connection/ema-login-helper.ts
634
1120
  const EMA_LOGIN_HELPER_COMMAND = "auth/complete-ema-login";
635
1121
  function emaLoginMarkerKey(issuer) {
636
1122
  return `ema-idp:${issuer}`;
@@ -691,8 +1177,7 @@ async function runEmaLoginHelper() {
691
1177
  if (markerPath !== void 0) removeOwnPendingAuthMarker(markerPath);
692
1178
  }
693
1179
  }
694
- //#endregion
695
- //#region src/connection/parse-tool-args.ts
1180
+ // src/connection/parse-tool-args.ts
696
1181
  function assertJsonRoundTrips(value, context) {
697
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).`);
698
1183
  if (Array.isArray(value)) for (const item of value) assertJsonRoundTrips(item, context);
@@ -774,8 +1259,7 @@ function parseJsonObject(raw, flag) {
774
1259
  assertJsonRoundTrips(parsed, flag);
775
1260
  return parsed;
776
1261
  }
777
- //#endregion
778
- //#region src/connection/resolve-command.ts
1262
+ // src/connection/resolve-command.ts
779
1263
  function resolveCommandPath(command, env = process.env) {
780
1264
  if (!command || command.includes("/") || command.includes(path.sep)) return command;
781
1265
  const pathVar = env.PATH ?? "";
@@ -791,8 +1275,7 @@ function resolveCommandPath(command, env = process.env) {
791
1275
  }
792
1276
  return command;
793
1277
  }
794
- //#endregion
795
- //#region src/connection/form-schema.ts
1278
+ // src/connection/form-schema.ts
796
1279
  function isRecord(value) {
797
1280
  return typeof value === "object" && value !== null && !Array.isArray(value);
798
1281
  }
@@ -938,8 +1421,245 @@ function parseFormSchema(schema) {
938
1421
  }
939
1422
  return fields;
940
1423
  }
941
- //#endregion
942
- //#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
943
1663
  function asArray(value) {
944
1664
  return Array.isArray(value) ? value : [];
945
1665
  }
@@ -1272,6 +1992,17 @@ function formatAuthListHuman(list, style = PLAIN) {
1272
1992
  if (servers.length === 0) lines.push(style.dim("(none)"));
1273
1993
  return lines.join("\n");
1274
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
+ }
1275
2006
  function formatEmaStatusHuman(status, style = PLAIN) {
1276
2007
  const lines = [heading(style, "EMA (enterprise-managed auth):")];
1277
2008
  lines.push(style.dim(String(status.clientConfigPath ?? "")));
@@ -1471,37 +2202,6 @@ function formatStreamEventHuman(data, style = PLAIN) {
1471
2202
  }
1472
2203
  return JSON.stringify(ev, null, 2);
1473
2204
  }
1474
- function formatLoadSummaryHuman(summary, style = PLAIN) {
1475
- const tool = summary.tool !== void 0 ? ` ${code(style, summary.tool)}` : "";
1476
- const bounds = [
1477
- `concurrency ${summary.concurrency}`,
1478
- ...summary.durationMs !== void 0 ? [`duration ${summary.durationMs / 1e3}s`] : [],
1479
- ...summary.requestLimit !== void 0 ? [`limit ${summary.requestLimit}`] : [],
1480
- ...summary.rate !== void 0 ? [`rate ${summary.rate}/s`] : []
1481
- ];
1482
- const failed = summary.toolErrors + summary.errors;
1483
- const rate = `${(summary.errorRate * 100).toFixed(2)}%`;
1484
- const lines = [
1485
- `${heading(style, "Load test")}${tool} ${style.dim(`(${bounds.join(", ")})`)}`,
1486
- ` Requests: ${summary.requests} in ${(summary.elapsedMs / 1e3).toFixed(2)}s (${summary.throughput}/s)`,
1487
- ` Outcomes: ${style.green(`${summary.ok} ok`)}, ${summary.toolErrors} tool errors, ${summary.errors} errors`,
1488
- ` Error rate: ${failed > 0 ? style.red(rate) : rate}`
1489
- ];
1490
- const l = summary.latencyMs;
1491
- lines.push(l === null ? ` Latency: ${style.dim("no calls completed")}` : ` Latency: min ${l.min} / mean ${l.mean} / p50 ${l.p50} / p90 ${l.p90} / p99 ${l.p99} / max ${l.max} ms`);
1492
- if (summary.errorMessages.length > 0) {
1493
- lines.push(` ${style.bold("Top errors:")}`);
1494
- for (const { message, count } of summary.errorMessages) lines.push(` ${count}× ${message}`);
1495
- }
1496
- const c = summary.connections;
1497
- if (c !== void 0) {
1498
- const failed = `${c.failed} failed`;
1499
- const connect = c.connectMs === null ? "" : ` (connect p50 ${c.connectMs.p50} / max ${c.connectMs.max} ms)`;
1500
- lines.push(` Sessions: ${c.opened} of ${c.requested} opened, ${c.failed > 0 ? style.red(failed) : failed}${connect}`);
1501
- for (const { message, count } of c.errorMessages) lines.push(` ${count}× ${message}`);
1502
- }
1503
- return lines.join("\n");
1504
- }
1505
2205
  function formatRpcResultHuman(method, result, style = PLAIN) {
1506
2206
  switch (method) {
1507
2207
  case "tools/list": return formatToolsHuman(asArray(result.tools), style);
@@ -1526,8 +2226,7 @@ function formatRpcResultHuman(method, result, style = PLAIN) {
1526
2226
  default: return null;
1527
2227
  }
1528
2228
  }
1529
- //#endregion
1530
- //#region src/connection/format-connection.ts
2229
+ // src/connection/format-connection.ts
1531
2230
  function formatConnectionJson(data) {
1532
2231
  return JSON.stringify(data, null, 2).replace(/[\u0080-\u009F]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`) + "\n";
1533
2232
  }
@@ -1576,6 +2275,7 @@ function jsonPayload(payload) {
1576
2275
  case "daemon/stop": return payload.result;
1577
2276
  case "auth/list": return payload.list;
1578
2277
  case "auth/clear": return payload.result;
2278
+ case "auth/show": return payload.result;
1579
2279
  case "auth/ema-status": return payload.status;
1580
2280
  case "auth/ema-login": return payload.result;
1581
2281
  case "auth/ema-logout": return payload.result;
@@ -1649,6 +2349,7 @@ function humanPayload(payload, style, interactive) {
1649
2349
  case "auth/clear":
1650
2350
  if (payload.result.all === true) return style.green(`Cleared ${String(payload.result.cleared ?? 0)} stored auth entr${payload.result.cleared === 1 ? "y" : "ies"}.`);
1651
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);
1652
2353
  case "auth/ema-status": return formatEmaStatusHuman(payload.status, style);
1653
2354
  case "auth/ema-login":
1654
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)")}`;
@@ -1690,8 +2391,7 @@ function applyExitCodes(payload) {
1690
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" });
1691
2392
  }
1692
2393
  }
1693
- //#endregion
1694
- //#region src/connection/form-prompt.ts
2394
+ // src/connection/form-prompt.ts
1695
2395
  function watchForClose(rl) {
1696
2396
  return new Promise((_, reject) => {
1697
2397
  rl.once("close", () => reject(/* @__PURE__ */ new Error("stdin closed before an answer was given")));
@@ -1833,8 +2533,7 @@ async function promptForm(rl, message, fields, style) {
1833
2533
  values.set(field.name, await promptField(rl, closed, field, style));
1834
2534
  }
1835
2535
  }
1836
- //#endregion
1837
- //#region src/connection/prompt-reader.ts
2536
+ // src/connection/prompt-reader.ts
1838
2537
  function exhaustedError() {
1839
2538
  return /* @__PURE__ */ new Error("stdin closed before an answer was given");
1840
2539
  }
@@ -1923,8 +2622,7 @@ function getSharedPromptReader() {
1923
2622
  shared ??= new PromptReader();
1924
2623
  return shared;
1925
2624
  }
1926
- //#endregion
1927
- //#region src/connection/elicitation-prompt.ts
2625
+ // src/connection/elicitation-prompt.ts
1928
2626
  function cancelResponse(frame) {
1929
2627
  return {
1930
2628
  id: frame.id,
@@ -1989,10 +2687,15 @@ async function promptElicitation(frame, opts) {
1989
2687
  return cancelResponse(frame);
1990
2688
  }
1991
2689
  }
1992
- //#endregion
1993
- //#region src/connection/dispatch.ts
2690
+ // src/connection/dispatch.ts
1994
2691
  const STREAM_METHODS = /* @__PURE__ */ new Set(["logging/tail", "resources/subscribe"]);
1995
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
+ }
1996
2699
  async function dispatchConnectionRpc(method, methodArgs, opts) {
1997
2700
  const format = opts.format ?? "text";
1998
2701
  const style = styleFromOpts({
@@ -2004,7 +2707,7 @@ async function dispatchConnectionRpc(method, methodArgs, opts) {
2004
2707
  method,
2005
2708
  name: stripAt(opts.connection),
2006
2709
  requireExplicit: opts.requireExplicit,
2007
- ...opts.load !== void 0 && { load: opts.load.options }
2710
+ ...opts.load !== void 0 && { load: loadOptionsFor(opts.load, format) }
2008
2711
  };
2009
2712
  const { socketPath } = await ensureDaemon();
2010
2713
  if (STREAM_METHODS.has(method)) {
@@ -2048,6 +2751,10 @@ async function dispatchConnectionRpc(method, methodArgs, opts) {
2048
2751
  socketPath,
2049
2752
  timeoutMs: 0,
2050
2753
  signal: ac.signal,
2754
+ onLoadProgress: (progress) => {
2755
+ if (process.stderr.writableNeedDrain) return;
2756
+ process.stderr.write(`${formatLoadProgress(progress)}\n`);
2757
+ },
2051
2758
  onElicitation: (frame) => promptElicitation(frame, {
2052
2759
  style,
2053
2760
  interactive
@@ -2061,7 +2768,14 @@ async function dispatchConnectionRpc(method, methodArgs, opts) {
2061
2768
  format,
2062
2769
  style
2063
2770
  }, method, methodArgs.toolName, outcome);
2064
- if (outcome.kind === "load") checkLoadThresholds(outcome.summary, opts.load?.thresholds);
2771
+ if (outcome.kind === "load") {
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`);
2776
+ checkLoadThresholds(outcome.summary, opts.load?.thresholds);
2777
+ if (comparison !== void 0) checkLoadRegression(comparison, baseline.maxRegressionPct);
2778
+ }
2065
2779
  }
2066
2780
  async function writeRpcOutcome(out, method, toolName, outcome) {
2067
2781
  const { format, style } = out;
@@ -2140,8 +2854,7 @@ function hoistAtConnection(argv) {
2140
2854
  }
2141
2855
  return { argv };
2142
2856
  }
2143
- //#endregion
2144
- //#region src/connection/private-env.ts
2857
+ // src/connection/private-env.ts
2145
2858
  function createPrivateBinding() {
2146
2859
  return {
2147
2860
  dir: createPrivateDaemonDir(),
@@ -2158,8 +2871,7 @@ function formatPrivateEnvExports(binding) {
2158
2871
  function shellSingleQuote(value) {
2159
2872
  return `'${value.replace(/'/g, `'\\''`)}'`;
2160
2873
  }
2161
- //#endregion
2162
- //#region src/connection/stored-auth.ts
2874
+ // src/connection/stored-auth.ts
2163
2875
  function normalizeServerUrl(serverUrl) {
2164
2876
  try {
2165
2877
  return new URL(serverUrl).href;
@@ -2211,6 +2923,21 @@ async function resolveStoredAuthKey(key) {
2211
2923
  if (normalized !== trimmed || /^https?:\/\//i.test(trimmed)) return normalized;
2212
2924
  throw new CliExitCodeError(EXIT_CODES.USAGE, `No stored auth entry for '${trimmed}'. Use auth/list to see keys.`, { code: "usage" });
2213
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
+ }
2214
2941
  async function clearStoredAuth(key) {
2215
2942
  const url = await resolveStoredAuthKey(key);
2216
2943
  await new NodeOAuthStorage().clear(url);
@@ -2229,8 +2956,7 @@ async function clearStoredAuthForRelogin(serverUrl) {
2229
2956
  await new NodeOAuthStorage().clear(url);
2230
2957
  resetNodeOAuthStorageCache();
2231
2958
  }
2232
- //#endregion
2233
- //#region src/connection/auth-names.ts
2959
+ // src/connection/auth-names.ts
2234
2960
  function urlIfHttp(value) {
2235
2961
  if (value && /^https?:\/\//i.test(value)) return normalizeServerUrl(value);
2236
2962
  }
@@ -2295,18 +3021,21 @@ function resolveFriendlyName(name, index) {
2295
3021
  name
2296
3022
  };
2297
3023
  }
2298
- //#endregion
2299
- //#region src/connection/mcp.ts
3024
+ // src/connection/mcp.ts
2300
3025
  function isDaemonUnreachable(error) {
2301
3026
  return error instanceof CliExitCodeError && error.envelope?.code === "daemon_unreachable";
2302
3027
  }
3028
+ function assertNoDaemonMeansNothingConnected(error) {
3029
+ if (!isDaemonUnreachable(error)) throw error;
3030
+ assertSocketPathWithinLimit(getDaemonSocketPath());
3031
+ }
2303
3032
  async function resolveMruEntryName() {
2304
3033
  if (requireExplicitConnection()) throw new CliExitCodeError(EXIT_CODES.USAGE, "servers/show requires an entry name in non-interactive mode. Pass one (see mcpdo servers/list).", { code: "server_name_required" });
2305
3034
  let connections = [];
2306
3035
  try {
2307
3036
  connections = (await callDaemon("connections/list", {})).connections;
2308
3037
  } catch (error) {
2309
- if (!isDaemonUnreachable(error)) throw error;
3038
+ assertNoDaemonMeansNothingConnected(error);
2310
3039
  }
2311
3040
  const mru = connections.find((c) => c.isMru);
2312
3041
  if (!mru) throw new CliExitCodeError(EXIT_CODES.USAGE, "No entry name given and there is no most-recently-used connection to infer one from. Pass a catalog entry name (see mcpdo servers/list).", { code: "server_name_required" });
@@ -2455,7 +3184,7 @@ function registerConnect(program) {
2455
3184
  "both"
2456
3185
  ].includes(value)) throw new Error(`Invalid --elicit: ${value}. Use off, url, form, or both.`);
2457
3186
  return value;
2458
- }).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) => {
2459
3188
  const opts = program.opts();
2460
3189
  const { name: positionalConnection, rest } = splitConnectionTarget(target);
2461
3190
  const connectionName = stripAt(opts.connection) ?? positionalConnection ?? cmdOpts.server?.trim() ?? rest[0];
@@ -2484,6 +3213,10 @@ function registerConnect(program) {
2484
3213
  const { detail } = summarizeServerConfig(serverConfig);
2485
3214
  const name = stripAt(connectionName);
2486
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
+ });
2487
3220
  const { socketPath } = await ensureDaemon();
2488
3221
  const connectParams = {
2489
3222
  name,
@@ -2500,7 +3233,7 @@ function registerConnect(program) {
2500
3233
  } catch (error) {
2501
3234
  if (!(error instanceof CliExitCodeError) || error.envelope?.code !== "auth_required") throw error;
2502
3235
  if (opts.storedAuthOnly) throw error;
2503
- if (!(process.stdin.isTTY === true || process.stderr.isTTY === true) && !isCliAutoOpenForced()) {
3236
+ if (!(process.stdin.isTTY === true || process.stderr.isTTY === true) && !isCliAutoOpenForced() && cmdOpts.oauthDebug !== true) {
2504
3237
  const authUrl = await obtainPendingAuthUrl(serverConfig, serverSettings);
2505
3238
  const { socketPath: pendingSocketPath } = await ensureDaemon();
2506
3239
  const pending = await callDaemon("connect", {
@@ -2517,7 +3250,10 @@ function registerConnect(program) {
2517
3250
  });
2518
3251
  return;
2519
3252
  }
2520
- await authorizeInFrontend(serverConfig, serverSettings, { storedAuthOnly: false });
3253
+ await authorizeInFrontend(serverConfig, serverSettings, {
3254
+ storedAuthOnly: false,
3255
+ oauthDebug: cmdOpts.oauthDebug === true
3256
+ });
2521
3257
  const { socketPath: freshSocketPath } = await ensureDaemon();
2522
3258
  result = await callDaemon("connect", connectParams, {
2523
3259
  socketPath: freshSocketPath,
@@ -2544,6 +3280,24 @@ async function collectAuthNameIndex(opts) {
2544
3280
  }
2545
3281
  return buildAuthNameIndex(entries, connections);
2546
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
+ }
2547
3301
  function registerAuthCommands(program) {
2548
3302
  program.command(AUTH_HELPER_COMMAND, { hidden: true }).description("Internal: complete an OAuth sign-in (params JSON on stdin)").action(async () => {
2549
3303
  await runAuthHelper();
@@ -2607,42 +3361,30 @@ function registerAuthCommands(program) {
2607
3361
  });
2608
3362
  return;
2609
3363
  }
2610
- const trimmed = key.trim();
2611
- const storedKeys = (await listStoredAuth()).servers.map((s) => s.url);
2612
- if (storedKeys.includes(trimmed)) {
2613
- const result = await clearStoredAuth(trimmed);
2614
- await writeConnectionOutput(outOpts(opts), {
2615
- kind: "auth/clear",
2616
- result: { url: result.url }
2617
- });
2618
- return;
2619
- }
2620
- if (/^https?:\/\//i.test(trimmed)) {
2621
- const idpKey = idpOAuthStorageKey(trimmed);
2622
- const result = await clearStoredAuth(storedKeys.includes(idpKey) && !storedKeys.includes(normalizeServerUrl(trimmed)) ? idpKey : trimmed);
2623
- await writeConnectionOutput(outOpts(opts), {
2624
- kind: "auth/clear",
2625
- result: { url: result.url }
2626
- });
2627
- return;
2628
- }
2629
- const resolution = resolveFriendlyName(trimmed, await collectAuthNameIndex(opts));
2630
- switch (resolution.kind) {
2631
- case "no-url": throw new CliExitCodeError(EXIT_CODES.USAGE, `'${trimmed}' is a stdio server — it has no stored OAuth entry to clear.`, { code: "usage" });
2632
- 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" });
2633
- 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" });
2634
- case "url": {
2635
- const result = await clearStoredAuth(resolution.url);
2636
- await writeConnectionOutput(outOpts(opts), {
2637
- kind: "auth/clear",
2638
- result: {
2639
- url: result.url,
2640
- clearedByName: trimmed
2641
- }
2642
- });
2643
- 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 }
2644
3371
  }
2645
- }
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
+ });
2646
3388
  });
2647
3389
  program.command("auth/ema-status").description("Show enterprise-managed auth (EMA) configuration and IdP login state").action(async () => {
2648
3390
  const opts = program.opts();
@@ -2699,14 +3441,11 @@ function registerConnectionAdmin(program) {
2699
3441
  connections: result.connections
2700
3442
  });
2701
3443
  } catch (error) {
2702
- if (isDaemonUnreachable(error)) {
2703
- await writeConnectionOutput(outOpts(opts), {
2704
- kind: "connections/list",
2705
- connections: []
2706
- });
2707
- return;
2708
- }
2709
- throw error;
3444
+ assertNoDaemonMeansNothingConnected(error);
3445
+ await writeConnectionOutput(outOpts(opts), {
3446
+ kind: "connections/list",
3447
+ connections: []
3448
+ });
2710
3449
  }
2711
3450
  });
2712
3451
  program.command("connections/use").description("Set the MRU connection without an MCP RPC").argument("<connection>", "Connection @name / name").action(async (connectionArg) => {
@@ -2835,7 +3574,7 @@ function registerRpcCommands(program) {
2835
3574
  });
2836
3575
  break;
2837
3576
  case "tools/call":
2838
- 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 is above this many milliseconds, or when no call completed, 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);
2839
3578
  cmd.action(async (toolNamePos, toolArgsPos, o) => {
2840
3579
  const { toolName, toolArg } = resolveToolCallArgs({
2841
3580
  toolNameFlag: o.toolName,
@@ -2846,7 +3585,9 @@ function registerRpcCommands(program) {
2846
3585
  });
2847
3586
  const load = resolveLoadFlags({
2848
3587
  ...o,
2849
- toolName
3588
+ toolName,
3589
+ toolArg,
3590
+ toolMetadata: o.toolMetadata
2850
3591
  });
2851
3592
  await runRpc(program, method, {
2852
3593
  toolName,
@@ -2899,7 +3640,7 @@ function registerRpcCommands(program) {
2899
3640
  });
2900
3641
  break;
2901
3642
  case "prompts/get":
2902
- 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, {});
2903
3644
  cmd.action(async (promptPos, o) => {
2904
3645
  await runRpc(program, method, {
2905
3646
  promptName: o.promptName ?? promptPos,
@@ -3105,8 +3846,7 @@ function splitConnectionTarget(target) {
3105
3846
  rest: target
3106
3847
  };
3107
3848
  }
3108
- //#endregion
3109
- //#region src/mcp-bin.ts
3849
+ // src/mcp-bin.ts
3110
3850
  disallowMemorySecretStoreFallback();
3111
3851
  setSecretStorageWarningsQuiet(true);
3112
3852
  const __filename = fileURLToPath(import.meta.url);
@@ -3120,5 +3860,4 @@ function isMainModule() {
3120
3860
  }
3121
3861
  }
3122
3862
  if (isMainModule()) runMcp(process.argv).then(() => process.exit(0)).catch(handleError);
3123
- //#endregion
3124
3863
  export { runMcp };