@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.
- package/clients/cli/build/index.js +4906 -2205
- package/clients/mcpdo/build/mcp-bin.js +891 -180
- package/clients/mcpdo/build/mcpdod.js +2 -3
- package/clients/mcpdo/build/{server-Dv339Ejx.js → server-B-CUVIBj.js} +2750 -1039
- package/clients/tui/build/index.js +1354 -533
- package/clients/web/build/index.js +239 -159
- package/clients/web/dist/assets/index-BZ94R6fh.js +1599 -0
- package/clients/web/dist/assets/{index-Dval2a7I.css → index-tBehH5iF.css} +1 -1
- package/clients/web/dist/assets/{prism-light-CoVRM9R2.js → prism-light-Crps--E8.js} +1 -1
- package/clients/web/dist/assets/worker-json-DEGsizKa.js +7859 -0
- package/clients/web/dist/index.html +2 -2
- package/package.json +4 -1
- package/skills/mcpdo/SKILL.md +50 -3
- package/clients/web/dist/assets/index-BCud2uMC.js +0 -1594
- package/clients/web/dist/assets/worker-json-8CDXFfik.js +0 -1631
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
function
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
if (
|
|
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
|
-
|
|
570
|
-
|
|
705
|
+
source,
|
|
706
|
+
summary
|
|
571
707
|
};
|
|
572
708
|
}
|
|
573
|
-
function
|
|
574
|
-
|
|
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
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
581
|
-
|
|
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
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
593
|
-
|
|
951
|
+
from,
|
|
952
|
+
to,
|
|
953
|
+
durationSec
|
|
594
954
|
};
|
|
595
955
|
}
|
|
596
|
-
function
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
955
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
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",
|
|
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
|
-
|
|
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 };
|