@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.
- package/README.md +1 -1
- package/clients/cli/build/index.js +5481 -1847
- package/clients/mcpdo/build/mcp-bin.js +925 -186
- package/clients/mcpdo/build/mcpdod.js +2 -3
- package/clients/mcpdo/build/{server-BHuKGkMj.js → server-B-CUVIBj.js} +3544 -995
- package/clients/tui/build/index.js +2126 -570
- package/clients/web/build/index.js +459 -280
- package/clients/web/dist/assets/fredoka-normal-hebrew-CjvktpUN.woff2 +0 -0
- package/clients/web/dist/assets/fredoka-normal-latin-DOQG_Jwn.woff2 +0 -0
- package/clients/web/dist/assets/fredoka-normal-latin-ext-tYNUQHO6.woff2 +0 -0
- package/clients/web/dist/assets/index-BZ94R6fh.js +1599 -0
- package/clients/web/dist/assets/{index-dqp8Sd8W.css → index-tBehH5iF.css} +1 -1
- package/clients/web/dist/assets/{prism-light-BIhJtJkv.js → prism-light-Crps--E8.js} +1 -1
- package/clients/web/dist/assets/roboto-mono-italic-cyrillic-C1E63de8.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-italic-cyrillic-ext-DR9ovEqT.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-italic-greek-Cs2u2nVh.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-italic-latin-D_P-kX1t.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-italic-latin-ext-CErTcBzw.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-italic-vietnamese-D4UvzJMM.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-normal-cyrillic-DfzWIUPd.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-normal-cyrillic-ext-D8QBEkUe.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-normal-greek-DlC4X1lQ.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-normal-latin-BAKpfWyj.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-normal-latin-ext-CSSVNFqV.woff2 +0 -0
- package/clients/web/dist/assets/roboto-mono-normal-vietnamese-DFkHeOHB.woff2 +0 -0
- package/clients/web/dist/assets/worker-json-DEGsizKa.js +7859 -0
- package/clients/web/dist/fonts/Fredoka-OFL.txt +93 -0
- package/clients/web/dist/fonts/RobotoMono-OFL.txt +93 -0
- package/clients/web/dist/index.html +2 -5
- package/clients/web/static/sandbox_proxy.html +177 -6
- package/package.json +8 -4
- package/skills/mcpdo/SKILL.md +63 -2
- package/clients/web/dist/assets/index-BqEbhCDX.js +0 -1587
- 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() {
|
|
@@ -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)
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
function
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
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`);
|
|
555
704
|
return {
|
|
556
|
-
|
|
557
|
-
|
|
705
|
+
source,
|
|
706
|
+
summary
|
|
558
707
|
};
|
|
559
708
|
}
|
|
560
|
-
function
|
|
561
|
-
|
|
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
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
580
|
-
|
|
951
|
+
from,
|
|
952
|
+
to,
|
|
953
|
+
durationSec
|
|
581
954
|
};
|
|
582
955
|
}
|
|
583
|
-
function
|
|
584
|
-
if (
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
942
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
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
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
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
|
|
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",
|
|
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
|
-
|
|
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 };
|