@mcpeak/cli 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  let node_crypto = require("node:crypto");
2
+ let node_fs = require("node:fs");
2
3
  let node_fs_promises = require("node:fs/promises");
3
4
  let node_os = require("node:os");
4
5
  let node_path = require("node:path");
5
6
  let _mcpeak_runner = require("@mcpeak/runner");
6
- let node_fs = require("node:fs");
7
7
  let node_child_process = require("node:child_process");
8
8
  let _mcpeak_core = require("@mcpeak/core");
9
9
  let node_url = require("node:url");
@@ -11,13 +11,16 @@ let node_readline_promises = require("node:readline/promises");
11
11
  let node_util = require("node:util");
12
12
  let _mcpeak_generate = require("@mcpeak/generate");
13
13
  //#region ../audit/package.json
14
- var version$2 = "0.3.1";
14
+ var version$3 = "0.3.1";
15
15
  //#endregion
16
16
  //#region ../optimize/package.json
17
- var version$1 = "0.2.2";
17
+ var version$2 = "0.2.2";
18
+ //#endregion
19
+ //#region ../perf/package.json
20
+ var version$1 = "0.1.0";
18
21
  //#endregion
19
22
  //#region package.json
20
- var version = "0.14.1";
23
+ var version = "0.15.0";
21
24
  //#endregion
22
25
  //#region src/audit-sandbox-target.ts
23
26
  /**
@@ -738,6 +741,41 @@ stdio 서버를 띄울 때 GITHUB_TOKEN 같은 흔한 비밀 이름에 가짜
738
741
  --sandbox 없이는 도구 정의의 문형과 프로토콜 표면만 봅니다. 바꿔 말한 지시와 서버 코드의 실제
739
742
  행위는 보지 못하므로 발견 0 이 안전하다는 뜻은 아닙니다. --sandbox 는 한 번의 실행에서 관측된
740
743
  행위만 봅니다. 판정은 이 컴퓨터 안에서만 하고 도구 정의를 외부로 보내지 않습니다.`;
744
+ const PERF_USAGE = "사용법: mcpeak perf <suite.json> (-- <executable> [args...] | --command <executable> [--arg <value> ...] [--env <NAME> ...]) [--repeat <n>] [--interval-ms <ms>] [--tool <name> ...] [--baseline <path> [--update-baseline]] [--threshold-pct <n>] [--threshold-ms <n>] [--json]";
745
+ /**
746
+ * perf 옵션 설명. 설계 §8.4 의 옵션 표를 옮긴 것이다. 사용법 한 줄로는 기준선 판정의 허용치가
747
+ * 무엇에 걸리는지, 간격 기본값 0 이 무슨 뜻인지 알 수 없다. 둘 다 결과를 잘못 읽게 하는 값이다.
748
+ * 원격 옵션 설명(`REMOTE_TARGET_OPTIONS`)은 넣지 않는다. perf 는 그 옵션을 거절한다.
749
+ */
750
+ const PERF_OPTIONS = `옵션:
751
+ -- <executable> [args...]
752
+ \`--\` 뒤는 전부 서버를 띄울 명령입니다. 첫 토큰이 실행 파일,
753
+ 나머지가 그 인자입니다. \`--command\`/\`--arg\` 와 같은 일을 하며
754
+ 함께 쓸 수 없습니다
755
+ --repeat <n> 케이스당 호출 수입니다. 기본값은 5 입니다. 1 이상 100 이하 정수
756
+ --interval-ms <ms> 호출 사이 대기입니다. 기본값은 0 입니다. 0 이상 60000 이하 정수.
757
+ 리포트 설정 줄에 표시됩니다
758
+ --tool <name> 그 도구의 케이스만 잽니다. 반복할 수 있습니다
759
+ --baseline <path> 기준 파일입니다. 있으면 비교하고, --update-baseline 과 함께면
760
+ 씁니다
761
+ --update-baseline 측정 결과로 기준 파일을 씁니다. 비교하지 않습니다.
762
+ --baseline 과 함께 써야 하고 --tool 과는 함께 쓸 수 없습니다
763
+ --threshold-pct <n> 서버 안 p50 회귀의 비율 허용치입니다. 기본값은 50 입니다.
764
+ 0 이상 1000 이하 정수
765
+ --threshold-ms <n> 서버 안 p50 회귀의 절대 허용치입니다. 기본값은 20 입니다.
766
+ 0 이상 60000 이하 정수. 기준의 (1 + 비율/100)배에 이 값을 더한
767
+ 것을 넘으면 회귀입니다
768
+ --json 리포트 대신 결과 JSON 하나를 stdout 에 냅니다
769
+ ${ENV_FORWARD_OPTION}
770
+
771
+ 측정 대상은 명세의 callTool 케이스 중 거절(isError: true)을 기대하지 않는 것입니다. 서버 프로세스
772
+ 안에 관측 코드를 실어야 하므로 --url 과 --header-env 는 받지 않습니다.
773
+
774
+ --interval-ms 기본값 0 은 서버의 레이트리미터 대기가 서버 안 시간으로 보일 수 있다는 뜻입니다.
775
+ 어떤 양수도 그 서버의 한도를 아는 척하는 값이라 기본을 0 으로 두고 설정 줄에 적습니다.
776
+
777
+ 종료 코드는 측정 완료 0, 기준선 회귀 2, 사용 오류·연결 실패·명세 오류·기준 파일 오류 1 입니다.
778
+ 진단은 종료 코드를 바꾸지 않습니다.`;
741
779
  const GLOBAL_HELP = `MCPeak — MCP 서버 테스트 프레임워크
742
780
 
743
781
  사용법: mcpeak <명령> [옵션]
@@ -749,6 +787,7 @@ const GLOBAL_HELP = `MCPeak — MCP 서버 테스트 프레임워크
749
787
  repair 실패한 test 실행의 번들로 서버 코드의 원인 후보를 제안받습니다.
750
788
  optimize 기능을 바꾸지 않고 MCP 서버의 도구 정의를 줄인 오버레이를 만듭니다.
751
789
  audit MCP 서버를 쓰기 전에 도구 정의와 프로토콜의 보안 위험을 점검합니다.
790
+ perf 도구 호출 지연을 외부 대기와 서버 안 시간으로 나누고 회귀를 잡습니다.
752
791
 
753
792
  옵션:
754
793
  -h, --help 도움말을 표시합니다.
@@ -767,13 +806,20 @@ const GLOBAL_HELP = `MCPeak — MCP 서버 테스트 프레임워크
767
806
  * "어떤 명령이 있나" 를 모르는 상태다. 특정 서브커맨드의 플래그 목록을 붙이면 사용자가
768
807
  * 묻지도 않은 명령의 사용법 200 자를 읽고 나서야 목록에 닿는다.
769
808
  */
770
- const commandDiscovery = "사용 가능한 명령: test, generate, repair, optimize, audit. 전체 도움말: mcpeak --help";
809
+ const commandDiscovery = "사용 가능한 명령: test, generate, repair, optimize, audit, perf. 전체 도움말: mcpeak --help";
771
810
  const TEST_USAGE_HINT = `${TEST_USAGE} ${commandDiscovery}`;
772
811
  const GENERATE_USAGE_HINT = `${GENERATE_USAGE} ${commandDiscovery}`;
773
812
  const REPAIR_USAGE_HINT = `${REPAIR_USAGE} ${commandDiscovery}`;
774
813
  const OPTIMIZE_USAGE_HINT = `${OPTIMIZE_USAGE} ${commandDiscovery}`;
775
814
  const AUDIT_USAGE_HINT = `${AUDIT_USAGE} ${commandDiscovery}`;
815
+ const PERF_USAGE_HINT = `${PERF_USAGE} ${commandDiscovery}`;
776
816
  function commandHelp(command) {
817
+ if (command === "perf") return `perf — 도구 호출 지연을 외부 대기와 서버 안 시간으로 나누고 회귀를 잡습니다.
818
+
819
+ ${PERF_USAGE}
820
+
821
+ ${PERF_OPTIONS}
822
+ `;
777
823
  if (command === "test") return `test — JSON 테스트 명세로 MCP 서버를 실행하고 검증합니다.
778
824
 
779
825
  ${TEST_USAGE}
@@ -2133,14 +2179,14 @@ function resetFailure(error) {
2133
2179
  * 노드 오류의 `code`. 문자열(`ENOENT`)이거나 숫자일 수 있고, 둘 다 아니면 우리가 아는 것이
2134
2180
  * 없다. `junitWriteFailure` 가 쓰던 것을 꺼내 다른 실패 생성기와 함께 쓴다(#276).
2135
2181
  */
2136
- function errnoOf(error) {
2182
+ function errnoOf$1(error) {
2137
2183
  const code = error?.code;
2138
2184
  return typeof code === "string" || typeof code === "number" ? String(code) : "알 수 없음";
2139
2185
  }
2140
2186
  function junitWriteFailure(path, error) {
2141
2187
  return {
2142
2188
  code: "JUNIT_WRITE_FAILED",
2143
- message: `JUnit XML 파일을 쓰지 못했습니다: ${escapeTerminalText(path)}\n errno: ${escapeTerminalText(errnoOf(error))}`,
2189
+ message: `JUnit XML 파일을 쓰지 못했습니다: ${escapeTerminalText(path)}\n errno: ${escapeTerminalText(errnoOf$1(error))}`,
2144
2190
  hint: dictionary.JUNIT_WRITE_FAILED.hint
2145
2191
  };
2146
2192
  }
@@ -2151,7 +2197,7 @@ function junitWriteFailure(path, error) {
2151
2197
  function repairBundleWriteFailure(path, error) {
2152
2198
  return {
2153
2199
  code: "REPAIR_BUNDLE_WRITE_FAILED",
2154
- message: `repair 번들 파일을 쓰지 못했습니다: ${escapeTerminalText(path)}\n errno: ${escapeTerminalText(errnoOf(error))}`,
2200
+ message: `repair 번들 파일을 쓰지 못했습니다: ${escapeTerminalText(path)}\n errno: ${escapeTerminalText(errnoOf$1(error))}`,
2155
2201
  hint: dictionary.REPAIR_BUNDLE_WRITE_FAILED.hint
2156
2202
  };
2157
2203
  }
@@ -2160,9 +2206,9 @@ function repairBundleWriteFailure(path, error) {
2160
2206
  * 없으면 권한을 고친다. 둘을 한 문장으로 묶으면 사용자에게 둘 다 확인하게 만든다(#276).
2161
2207
  * 어느 쪽인지 아는데 안 말할 이유가 없다.
2162
2208
  */
2163
- function suiteReadFailure(path, error) {
2209
+ function suiteReadFailure$1(path, error) {
2164
2210
  const shown = escapeTerminalText(path);
2165
- const errno = errnoOf(error);
2211
+ const errno = errnoOf$1(error);
2166
2212
  if (errno === "ENOENT") return {
2167
2213
  code: "SUITE_READ_FAILED",
2168
2214
  message: `테스트 명세 파일이 그 경로에 없습니다: ${shown}`,
@@ -2193,7 +2239,7 @@ function suiteReadFailure(path, error) {
2193
2239
  * 인코딩을 아예 언급하지 않는다 — 한 번 꺼내면 그것부터 확인하러 가고, 여기서 할 일은
2194
2240
  * 경로를 고치는 것 하나다.
2195
2241
  */
2196
- function suiteFormatFailure(path) {
2242
+ function suiteFormatFailure$1(path) {
2197
2243
  const ext = (0, node_path.extname)(path);
2198
2244
  return {
2199
2245
  code: "SUITE_FORMAT_UNSUPPORTED",
@@ -2326,12 +2372,12 @@ async function runCliCore(argv, dependencies, childEnv) {
2326
2372
  ...dictionary.CLI_INTERNAL_ERROR
2327
2373
  });
2328
2374
  }
2329
- if ((0, node_path.extname)(input.suitePath).toLowerCase() !== ".json") return writeFailure(dependencies, suiteFormatFailure(input.suitePath));
2375
+ if ((0, node_path.extname)(input.suitePath).toLowerCase() !== ".json") return writeFailure(dependencies, suiteFormatFailure$1(input.suitePath));
2330
2376
  let bytes;
2331
2377
  try {
2332
2378
  bytes = await dependencies.readFile(input.suitePath);
2333
2379
  } catch (error) {
2334
- return writeFailure(dependencies, suiteReadFailure(input.suitePath, error));
2380
+ return writeFailure(dependencies, suiteReadFailure$1(input.suitePath, error));
2335
2381
  }
2336
2382
  let decoded;
2337
2383
  try {
@@ -3205,18 +3251,18 @@ function externalModeOf(argv) {
3205
3251
  //#endregion
3206
3252
  //#region src/audit-command.ts
3207
3253
  /** 서버 연결 stderr 진단의 줄 수. `test` 의 `--stderr-lines` 기본값과 같다. */
3208
- const STDERR_LINES$1 = 20;
3254
+ const STDERR_LINES$2 = 20;
3209
3255
  const PROBE_POLICIES = [
3210
3256
  "readonly",
3211
3257
  "none",
3212
3258
  "all"
3213
3259
  ];
3214
- var UsageError$3 = class extends Error {};
3260
+ var UsageError$4 = class extends Error {};
3215
3261
  /**
3216
3262
  * `test` 는 받지만 `audit` 에는 뜻이 없는 옵션. `parseTestCommand` 에 넘기기 전에 여기서
3217
3263
  * 거절한다. 넘기면 조용히 통과하거나 "test 옵션" 이라는 남의 이름으로 거절된다.
3218
3264
  */
3219
- const TEST_ONLY_OPTIONS$1 = /* @__PURE__ */ new Set([
3265
+ const TEST_ONLY_OPTIONS$2 = /* @__PURE__ */ new Set([
3220
3266
  "--junit",
3221
3267
  "--repair-bundle",
3222
3268
  "--determinism",
@@ -3226,7 +3272,7 @@ const TEST_ONLY_OPTIONS$1 = /* @__PURE__ */ new Set([
3226
3272
  "--record-session"
3227
3273
  ]);
3228
3274
  /** 대상 옵션 중 값을 다음 토큰으로 받는 것. 그 값이 `--json` 처럼 생겨도 옵션으로 읽지 않는다. */
3229
- const TARGET_VALUE_OPTIONS$1 = /* @__PURE__ */ new Set([
3275
+ const TARGET_VALUE_OPTIONS$2 = /* @__PURE__ */ new Set([
3230
3276
  "--command",
3231
3277
  "--arg",
3232
3278
  "--env",
@@ -3296,57 +3342,57 @@ function parseAuditCommand(argv) {
3296
3342
  }
3297
3343
  const name = token.startsWith("--") && token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
3298
3344
  if (name === "--probe") {
3299
- if (probe !== void 0) throw new UsageError$3("`--probe`은 한 번만 사용할 수 있습니다.");
3345
+ if (probe !== void 0) throw new UsageError$4("`--probe`은 한 번만 사용할 수 있습니다.");
3300
3346
  const value = token === "--probe" ? argv[++index] : token.slice(8);
3301
- if (!PROBE_POLICIES.includes(value)) throw new UsageError$3("--probe 값은 readonly, none, all 중 하나입니다.");
3347
+ if (!PROBE_POLICIES.includes(value)) throw new UsageError$4("--probe 값은 readonly, none, all 중 하나입니다.");
3302
3348
  probe = value;
3303
3349
  continue;
3304
3350
  }
3305
3351
  if (name === "--baseline") {
3306
- if (baselinePath !== void 0) throw new UsageError$3("`--baseline`은 한 번만 사용할 수 있습니다.");
3352
+ if (baselinePath !== void 0) throw new UsageError$4("`--baseline`은 한 번만 사용할 수 있습니다.");
3307
3353
  const value = token === "--baseline" ? argv[++index] : token.slice(11);
3308
- if (value === void 0 || value.trim() === "" || value.startsWith("--")) throw new UsageError$3("`--baseline` 옵션 값이 필요합니다.");
3354
+ if (value === void 0 || value.trim() === "" || value.startsWith("--")) throw new UsageError$4("`--baseline` 옵션 값이 필요합니다.");
3309
3355
  baselinePath = value;
3310
3356
  continue;
3311
3357
  }
3312
3358
  if (FLAG_OPTIONS.includes(name)) {
3313
3359
  const flag = name;
3314
- if (token !== name) throw new UsageError$3(`\`${name}\`은 값을 받지 않습니다.`);
3315
- if (flags[flag]) throw new UsageError$3(`\`${name}\`은 한 번만 사용할 수 있습니다.`);
3360
+ if (token !== name) throw new UsageError$4(`\`${name}\`은 값을 받지 않습니다.`);
3361
+ if (flags[flag]) throw new UsageError$4(`\`${name}\`은 한 번만 사용할 수 있습니다.`);
3316
3362
  flags[flag] = true;
3317
3363
  continue;
3318
3364
  }
3319
3365
  if (SANDBOX_PATH_OPTIONS.includes(name)) {
3320
3366
  const option = name;
3321
- if (paths[option] !== void 0) throw new UsageError$3(`\`${name}\`은 한 번만 사용할 수 있습니다.`);
3367
+ if (paths[option] !== void 0) throw new UsageError$4(`\`${name}\`은 한 번만 사용할 수 있습니다.`);
3322
3368
  const value = token === name ? argv[++index] : token.slice(name.length + 1);
3323
- if (value === void 0 || value.trim() === "" || value.startsWith("--")) throw new UsageError$3(`\`${name}\` 옵션 값이 필요합니다.`);
3369
+ if (value === void 0 || value.trim() === "" || value.startsWith("--")) throw new UsageError$4(`\`${name}\` 옵션 값이 필요합니다.`);
3324
3370
  paths[option] = value;
3325
3371
  continue;
3326
3372
  }
3327
3373
  if (name === "--allow-host") {
3328
3374
  const value = token === name ? argv[++index] : token.slice(name.length + 1);
3329
- if (value === void 0 || token === name && value.startsWith("--")) throw new UsageError$3("`--allow-host` 옵션 값이 필요합니다.");
3330
- if (value.length > 253 || !HOSTNAME.test(value)) throw new UsageError$3(`--allow-host 값은 호스트 이름이어야 합니다(예: api.example.com): '${escapeTerminalText$2(value)}'`);
3375
+ if (value === void 0 || token === name && value.startsWith("--")) throw new UsageError$4("`--allow-host` 옵션 값이 필요합니다.");
3376
+ if (value.length > 253 || !HOSTNAME.test(value)) throw new UsageError$4(`--allow-host 값은 호스트 이름이어야 합니다(예: api.example.com): '${escapeTerminalText$2(value)}'`);
3331
3377
  allowHosts.push(value);
3332
3378
  continue;
3333
3379
  }
3334
3380
  if (name === "--url") usesUrl = true;
3335
- if (TEST_ONLY_OPTIONS$1.has(name) || token.startsWith("-") && !TARGET_VALUE_OPTIONS$1.has(name)) throw new UsageError$3(`지원하지 않는 audit 옵션 '${escapeTerminalText$2(name)}'입니다.`);
3381
+ if (TEST_ONLY_OPTIONS$2.has(name) || token.startsWith("-") && !TARGET_VALUE_OPTIONS$2.has(name)) throw new UsageError$4(`지원하지 않는 audit 옵션 '${escapeTerminalText$2(name)}'입니다.`);
3336
3382
  targetArgv.push(token);
3337
- if (TARGET_VALUE_OPTIONS$1.has(token) && index + 1 < argv.length) targetArgv.push(argv[++index]);
3383
+ if (TARGET_VALUE_OPTIONS$2.has(token) && index + 1 < argv.length) targetArgv.push(argv[++index]);
3338
3384
  }
3339
3385
  const updateBaseline = flags["--update-baseline"];
3340
3386
  const json = flags["--json"];
3341
- if (updateBaseline && baselinePath === void 0) throw new UsageError$3("--update-baseline 은 --baseline 과 함께 써야 합니다.");
3387
+ if (updateBaseline && baselinePath === void 0) throw new UsageError$4("--update-baseline 은 --baseline 과 함께 써야 합니다.");
3342
3388
  const sandbox = parseSandbox(flags, paths, allowHosts, usesUrl);
3343
3389
  let target;
3344
3390
  try {
3345
3391
  target = parseTestCommand([PLACEHOLDER_SUITE$1, ...targetArgv]).target;
3346
3392
  } catch (error) {
3347
- const message = usageMessageOf$1(error);
3393
+ const message = usageMessageOf$2(error);
3348
3394
  if (message === void 0) throw error;
3349
- throw new UsageError$3(message);
3395
+ throw new UsageError$4(message);
3350
3396
  }
3351
3397
  return Object.freeze({
3352
3398
  target,
@@ -3363,13 +3409,13 @@ function parseSandbox(flags, paths, allowHosts, usesUrl) {
3363
3409
  if (!flags["--sandbox"]) {
3364
3410
  const given = (option) => option === "--allow-host" ? allowHosts.length > 0 : option === "--compare-host" ? flags[option] : paths[option] !== void 0;
3365
3411
  const orphan = SANDBOX_ONLY_OPTIONS.find(given);
3366
- if (orphan !== void 0) throw new UsageError$3(`${orphan} 은 --sandbox 와 함께 써야 합니다.`);
3412
+ if (orphan !== void 0) throw new UsageError$4(`${orphan} 은 --sandbox 와 함께 써야 합니다.`);
3367
3413
  return;
3368
3414
  }
3369
- if (usesUrl) throw new UsageError$3("--sandbox 는 --url 과 함께 쓸 수 없습니다. 격리는 프로세스를 띄우는 대상(--, --command)에만 적용됩니다.");
3415
+ if (usesUrl) throw new UsageError$4("--sandbox 는 --url 과 함께 쓸 수 없습니다. 격리는 프로세스를 띄우는 대상(--, --command)에만 적용됩니다.");
3370
3416
  const record = paths["--sandbox-session"];
3371
3417
  const replay = paths["--sandbox-replay"];
3372
- if (record !== void 0 && replay !== void 0) throw new UsageError$3("--sandbox-session 과 --sandbox-replay 는 함께 쓸 수 없습니다. 녹화와 재생은 따로 실행하세요.");
3418
+ if (record !== void 0 && replay !== void 0) throw new UsageError$4("--sandbox-session 과 --sandbox-replay 는 함께 쓸 수 없습니다. 녹화와 재생은 따로 실행하세요.");
3373
3419
  const session = record !== void 0 ? {
3374
3420
  mode: "record",
3375
3421
  path: record
@@ -3386,14 +3432,14 @@ function parseSandbox(flags, paths, allowHosts, usesUrl) {
3386
3432
  };
3387
3433
  }
3388
3434
  /** `parseTestCommand` 가 던진 사용 오류의 문장. 그 오류 클래스는 export 되지 않아 모양으로 본다. */
3389
- function usageMessageOf$1(error) {
3435
+ function usageMessageOf$2(error) {
3390
3436
  if (typeof error !== "object" || error === null || !("failure" in error)) return void 0;
3391
3437
  const failure = error.failure;
3392
3438
  if (typeof failure !== "object" || failure === null) return void 0;
3393
3439
  const { code, message } = failure;
3394
3440
  return code === "CLI_USAGE" && typeof message === "string" ? message : void 0;
3395
3441
  }
3396
- function usageFailure$1(deps, message) {
3442
+ function usageFailure$2(deps, message) {
3397
3443
  deps.writeStderr(`오류 [CLI_USAGE]: ${message}\n해결: ${AUDIT_USAGE_HINT}\n`);
3398
3444
  return 1;
3399
3445
  }
@@ -3450,13 +3496,13 @@ async function runAuditCommand(argv, deps) {
3450
3496
  try {
3451
3497
  input = parseAuditCommand(argv[0] === "audit" ? argv.slice(1) : argv);
3452
3498
  } catch (error) {
3453
- if (!(error instanceof UsageError$3)) throw error;
3454
- return usageFailure$1(deps, error.message);
3499
+ if (!(error instanceof UsageError$4)) throw error;
3500
+ return usageFailure$2(deps, error.message);
3455
3501
  }
3456
3502
  try {
3457
3503
  await preflight(input.target, deps.readEnv);
3458
3504
  } catch (error) {
3459
- if (error instanceof ConnectTargetError) return usageFailure$1(deps, error.message);
3505
+ if (error instanceof ConnectTargetError) return usageFailure$2(deps, error.message);
3460
3506
  throw error;
3461
3507
  }
3462
3508
  const forwardedEnv = {};
@@ -3513,7 +3559,7 @@ async function runAuditCommand(argv, deps) {
3513
3559
  try {
3514
3560
  sandbox = await sandboxOptions(input, deps);
3515
3561
  } catch (error) {
3516
- if (error instanceof SandboxTargetError) return usageFailure$1(deps, error.message);
3562
+ if (error instanceof SandboxTargetError) return usageFailure$2(deps, error.message);
3517
3563
  throw error;
3518
3564
  }
3519
3565
  let report;
@@ -3546,14 +3592,14 @@ async function runAuditCommand(argv, deps) {
3546
3592
  } catch (error) {
3547
3593
  const kept = cleanupFailureReport(error);
3548
3594
  if (kept === void 0) return writeAuditFailure(deps, input.target, error, connectError, runnerConnectFailed);
3549
- writeReport(deps, input, kept);
3595
+ writeReport$1(deps, input, kept);
3550
3596
  deps.writeStderr(`${escapeLines(error.message)}\n`);
3551
3597
  return 1;
3552
3598
  }
3553
- writeReport(deps, input, report);
3599
+ writeReport$1(deps, input, report);
3554
3600
  return report.exitCode;
3555
3601
  }
3556
- function writeReport(deps, input, report) {
3602
+ function writeReport$1(deps, input, report) {
3557
3603
  deps.writeStdout(input.json ? `${JSON.stringify(report, null, 2)}\n` : escapeLines(deps.renderReport(report)));
3558
3604
  }
3559
3605
  /**
@@ -3625,7 +3671,7 @@ function writeAuditFailure(deps, target, error, connectError, runnerConnectFaile
3625
3671
  `);
3626
3672
  return 1;
3627
3673
  }
3628
- const core = coreErrorOf$1(error);
3674
+ const core = coreErrorOf$2(error);
3629
3675
  const cause = core?.message ?? (error instanceof Error ? error.message : String(error));
3630
3676
  const hint = core?.hint ?? "mcpeak test 로 같은 서버가 tools/list 에 답하는지 먼저 확인하세요.";
3631
3677
  deps.writeStderr(`오류 [AUDIT_FAILED]: 서버에 붙은 뒤 점검 도중 실패했습니다: ${escapeTerminalText$2(describeTarget(target))}\n→ ${escapeTerminalText$2(cause.split("\n")[0] ?? "")}\n해결: ${escapeTerminalText$2(hint)}\n`);
@@ -3645,20 +3691,20 @@ function errorCode(error) {
3645
3691
  }
3646
3692
  /** core 오류가 들고 온 진단 블록. `test`·`optimize` 의 연결 실패와 같은 규칙이다. */
3647
3693
  function writeDiagnostics(deps, error) {
3648
- const core = coreErrorOf$1(error);
3694
+ const core = coreErrorOf$2(error);
3649
3695
  const processBlock = processDiagnostics(core?.diagnostics);
3650
3696
  if (processBlock !== void 0 && hasDiagnosticContent(processBlock)) {
3651
- const block = renderProcessDiagnostics(processBlock, { maxLines: STDERR_LINES$1 });
3697
+ const block = renderProcessDiagnostics(processBlock, { maxLines: STDERR_LINES$2 });
3652
3698
  if (block !== "") deps.writeStderr(`\n${block}`);
3653
3699
  }
3654
3700
  const remote = httpDiagnostics(core?.diagnostics);
3655
3701
  if (remote !== void 0) deps.writeStderr(`\n${renderHttpDiagnostics(remote)}`);
3656
3702
  }
3657
3703
  /** `McpClientError` 를 AggregateError 안까지 찾는다. 연결 정리 실패가 겹치면 그렇게 감싸인다. */
3658
- function coreErrorOf$1(error) {
3704
+ function coreErrorOf$2(error) {
3659
3705
  if (error instanceof AggregateError) {
3660
3706
  for (const nested of error.errors) {
3661
- const found = coreErrorOf$1(nested);
3707
+ const found = coreErrorOf$2(nested);
3662
3708
  if (found !== void 0) return found;
3663
3709
  }
3664
3710
  return;
@@ -4797,7 +4843,7 @@ function selectRepairTargets(options) {
4797
4843
  }
4798
4844
  //#endregion
4799
4845
  //#region src/generate-command.ts
4800
- var UsageError$2 = class extends Error {};
4846
+ var UsageError$3 = class extends Error {};
4801
4847
  /**
4802
4848
  * 출력 경로에 이미 파일이 있어 저장을 멈춘 경우. 다른 I/O 실패와 사용자 조치가 다르므로
4803
4849
  * 타입으로 갈라 둔다. 뭉뚱그리면 "저장하지 못했습니다"만 남아 어떤 파일이 왜 막았는지 모른다.
@@ -4918,11 +4964,11 @@ const removedGenerateOptions = /* @__PURE__ */ new Set(["--cassette", "--record"
4918
4964
  const removedGenerateOptionMessage = (option) => `\`${option}\` generate 옵션은 Tool 카세트와 함께 제거되었습니다(ADR-0059). 시험 실행은 서버를 직접 호출합니다. 서버의 외부 HTTP 호출을 녹화하려면 \`mcpeak test <suite.json> --command <executable> --record-session <path>\`를, 재생하려면 \`mcpeak test <suite.json> --command <executable> --session <path>\`를 사용하세요. 서버를 대신하려면 \`mcpeak test <suite.json> --command mcpeak-mock --arg <mock.json>\`을 사용하세요.`;
4919
4965
  function optionValue$1(argv, index, option) {
4920
4966
  const item = argv[index];
4921
- if (item === void 0) throw new UsageError$2(`\`${option}\` 옵션 값이 필요합니다.`);
4967
+ if (item === void 0) throw new UsageError$3(`\`${option}\` 옵션 값이 필요합니다.`);
4922
4968
  const equals = item.indexOf("=");
4923
4969
  if (equals > 0) return [item.slice(equals + 1), index];
4924
4970
  const value = argv[index + 1];
4925
- if (value === void 0 || option !== "--arg" && value.startsWith("--")) throw new UsageError$2(`\`${option}\` 옵션 값이 필요합니다.`);
4971
+ if (value === void 0 || option !== "--arg" && value.startsWith("--")) throw new UsageError$3(`\`${option}\` 옵션 값이 필요합니다.`);
4926
4972
  return [value, index + 1];
4927
4973
  }
4928
4974
  /**
@@ -4953,21 +4999,21 @@ function parseGenerateCommand(argv) {
4953
4999
  * `--arg` 가 값 검사에 두던 특수분기가 이 경로에는 필요 없다.
4954
5000
  */
4955
5001
  if (item === "--") {
4956
- if (values.has("--command")) throw new UsageError$2("`--command` 와 `--` 를 함께 쓸 수 없습니다.\n→ 둘 다 서버를 띄울 명령을 정하므로 대상이 둘이 됩니다.\n→ `--` 뒤에는 실행 파일과 인자를 그대로 적습니다.");
4957
- if (args.length > 0) throw new UsageError$2("`--arg` 와 `--` 를 함께 쓸 수 없습니다.\n→ `--arg` 로 준 값이 `--` 뒤의 인자 앞에 끼어듭니다.\n→ `--` 를 쓰면 인자도 그 뒤에 모두 적습니다.");
5002
+ if (values.has("--command")) throw new UsageError$3("`--command` 와 `--` 를 함께 쓸 수 없습니다.\n→ 둘 다 서버를 띄울 명령을 정하므로 대상이 둘이 됩니다.\n→ `--` 뒤에는 실행 파일과 인자를 그대로 적습니다.");
5003
+ if (args.length > 0) throw new UsageError$3("`--arg` 와 `--` 를 함께 쓸 수 없습니다.\n→ `--arg` 로 준 값이 `--` 뒤의 인자 앞에 끼어듭니다.\n→ `--` 를 쓰면 인자도 그 뒤에 모두 적습니다.");
4958
5004
  const rest = argv.slice(index + 1);
4959
5005
  const executable = rest[0];
4960
- if (executable === void 0 || executable === "") throw new UsageError$2("`--` 뒤에 실행할 명령이 없습니다.\n→ `-- <executable> [args...]` 처럼 첫 토큰에 실행 파일을 적으세요.");
5006
+ if (executable === void 0 || executable === "") throw new UsageError$3("`--` 뒤에 실행할 명령이 없습니다.\n→ `-- <executable> [args...]` 처럼 첫 토큰에 실행 파일을 적으세요.");
4961
5007
  values.set("--command", executable);
4962
5008
  args.push(...rest.slice(1));
4963
5009
  break;
4964
5010
  }
4965
5011
  const option = item.includes("=") ? item.slice(0, item.indexOf("=")) : item;
4966
- if (!option.startsWith("--")) throw new UsageError$2(`추가 위치 인자 '${item}'는 허용되지 않습니다.`);
4967
- if (removedGenerateOptions.has(option)) throw new UsageError$2(removedGenerateOptionMessage(option));
4968
- if (!optionNames$1.has(option)) throw new UsageError$2(`지원하지 않는 generate 옵션 '${option}'입니다.`);
5012
+ if (!option.startsWith("--")) throw new UsageError$3(`추가 위치 인자 '${item}'는 허용되지 않습니다.`);
5013
+ if (removedGenerateOptions.has(option)) throw new UsageError$3(removedGenerateOptionMessage(option));
5014
+ if (!optionNames$1.has(option)) throw new UsageError$3(`지원하지 않는 generate 옵션 '${option}'입니다.`);
4969
5015
  if (flagNames$1.has(option)) {
4970
- if (item !== option || flags.has(option)) throw new UsageError$2(`\`${option}\`는 한 번만 사용할 수 있습니다.`);
5016
+ if (item !== option || flags.has(option)) throw new UsageError$3(`\`${option}\`는 한 번만 사용할 수 있습니다.`);
4971
5017
  flags.add(option);
4972
5018
  continue;
4973
5019
  }
@@ -4979,57 +5025,57 @@ function parseGenerateCommand(argv) {
4979
5025
  }
4980
5026
  if (option === "--header-env") {
4981
5027
  const parsed = parseHeaderEnvOption(value);
4982
- if (!parsed.ok) throw new UsageError$2(parsed.message);
5028
+ if (!parsed.ok) throw new UsageError$3(parsed.message);
4983
5029
  const rejected = headerEnv.add(parsed.value.header, parsed.value.envName);
4984
- if (rejected !== void 0) throw new UsageError$2(rejected);
5030
+ if (rejected !== void 0) throw new UsageError$3(rejected);
4985
5031
  continue;
4986
5032
  }
4987
5033
  if (option === "--env") {
4988
5034
  const parsed = parseEnvForwardOption(value);
4989
- if (!parsed.ok) throw new UsageError$2(parsed.message);
5035
+ if (!parsed.ok) throw new UsageError$3(parsed.message);
4990
5036
  const rejected = envForward.add(parsed.value);
4991
- if (rejected !== void 0) throw new UsageError$2(rejected);
5037
+ if (rejected !== void 0) throw new UsageError$3(rejected);
4992
5038
  continue;
4993
5039
  }
4994
- if (values.has(option)) throw new UsageError$2(`\`${option}\`는 한 번만 사용할 수 있습니다.`);
5040
+ if (values.has(option)) throw new UsageError$3(`\`${option}\`는 한 번만 사용할 수 있습니다.`);
4995
5041
  values.set(option, value);
4996
5042
  }
4997
5043
  const out = values.get("--out");
4998
- if (out === void 0) throw new UsageError$2("`--out` 옵션이 필요합니다.");
5044
+ if (out === void 0) throw new UsageError$3("`--out` 옵션이 필요합니다.");
4999
5045
  const derived = deriveSuiteName(out);
5000
5046
  for (const option of ["--suite-id", "--name"]) {
5001
5047
  if (values.get(option) !== void 0) continue;
5002
- if (derived === void 0) throw new UsageError$2(`\`${option}\` 옵션이 필요합니다.\n→ \`--out\` 파일명에서 이름을 뽑을 수 없어 직접 지정해야 합니다.`);
5048
+ if (derived === void 0) throw new UsageError$3(`\`${option}\` 옵션이 필요합니다.\n→ \`--out\` 파일명에서 이름을 뽑을 수 없어 직접 지정해야 합니다.`);
5003
5049
  values.set(option, derived);
5004
5050
  }
5005
5051
  const command = values.get("--command");
5006
5052
  const rawUrl = values.get("--url");
5007
- if (command !== void 0 && rawUrl !== void 0) throw new UsageError$2("`--command` 와 `--url` 은 함께 쓸 수 없습니다.\n→ `--command` 는 서버를 프로세스로 띄우고, `--url` 은 이미 떠 있는 원격 서버에 붙습니다.\n→ 둘 중 무엇에서 명세를 뽑을지 하나만 고르세요.");
5008
- if (command === void 0 && rawUrl === void 0) throw new UsageError$2("`--command` 또는 `--url` 옵션이 필요합니다.");
5009
- if (rawUrl !== void 0 && args.length > 0) throw new UsageError$2("`--arg` 는 `--url` 과 함께 쓸 수 없습니다.\n→ `--arg` 는 우리가 띄우는 프로세스에 넘길 인자입니다. 원격 서버에는 띄울 프로세스가 없습니다.");
5010
- if (rawUrl !== void 0 && envForward.snapshot().length > 0) throw new UsageError$2("`--env` 는 `--url` 과 함께 쓸 수 없습니다.\n→ `--env` 는 우리가 띄우는 프로세스의 환경에 넣을 변수입니다. 원격 서버에는 띄울 프로세스가 없습니다.");
5011
- if (command !== void 0 && !headerEnv.isEmpty()) throw new UsageError$2("`--header-env` 는 `--url` 과 함께만 쓸 수 있습니다.\n→ 헤더는 HTTP 요청에 실립니다. `--command` 로 띄운 서버와는 stdio 로 이야기합니다.");
5053
+ if (command !== void 0 && rawUrl !== void 0) throw new UsageError$3("`--command` 와 `--url` 은 함께 쓸 수 없습니다.\n→ `--command` 는 서버를 프로세스로 띄우고, `--url` 은 이미 떠 있는 원격 서버에 붙습니다.\n→ 둘 중 무엇에서 명세를 뽑을지 하나만 고르세요.");
5054
+ if (command === void 0 && rawUrl === void 0) throw new UsageError$3("`--command` 또는 `--url` 옵션이 필요합니다.");
5055
+ if (rawUrl !== void 0 && args.length > 0) throw new UsageError$3("`--arg` 는 `--url` 과 함께 쓸 수 없습니다.\n→ `--arg` 는 우리가 띄우는 프로세스에 넘길 인자입니다. 원격 서버에는 띄울 프로세스가 없습니다.");
5056
+ if (rawUrl !== void 0 && envForward.snapshot().length > 0) throw new UsageError$3("`--env` 는 `--url` 과 함께 쓸 수 없습니다.\n→ `--env` 는 우리가 띄우는 프로세스의 환경에 넣을 변수입니다. 원격 서버에는 띄울 프로세스가 없습니다.");
5057
+ if (command !== void 0 && !headerEnv.isEmpty()) throw new UsageError$3("`--header-env` 는 `--url` 과 함께만 쓸 수 있습니다.\n→ 헤더는 HTTP 요청에 실립니다. `--command` 로 띄운 서버와는 stdio 로 이야기합니다.");
5012
5058
  const url = (() => {
5013
5059
  if (rawUrl === void 0) return void 0;
5014
5060
  const parsed = parseUrlOption(rawUrl);
5015
- if (!parsed.ok) throw new UsageError$2(parsed.message);
5061
+ if (!parsed.ok) throw new UsageError$3(parsed.message);
5016
5062
  return parsed.value;
5017
5063
  })();
5018
5064
  const outPath = values.get("--out");
5019
- if (!outPath.toLowerCase().endsWith(".json")) throw new UsageError$2("`--out`은 .json 파일이어야 합니다.");
5065
+ if (!outPath.toLowerCase().endsWith(".json")) throw new UsageError$3("`--out`은 .json 파일이어야 합니다.");
5020
5066
  const rawProvider = values.get("--provider");
5021
- if (rawProvider !== void 0 && rawProvider !== "codex" && rawProvider !== "claude") throw new UsageError$2("`--provider`는 codex 또는 claude여야 합니다.");
5022
- if (values.has("--model") && rawProvider === void 0) throw new UsageError$2("`--model`은 `--provider`와 함께만 사용할 수 있습니다.");
5067
+ if (rawProvider !== void 0 && rawProvider !== "codex" && rawProvider !== "claude") throw new UsageError$3("`--provider`는 codex 또는 claude여야 합니다.");
5068
+ if (values.has("--model") && rawProvider === void 0) throw new UsageError$3("`--model`은 `--provider`와 함께만 사용할 수 있습니다.");
5023
5069
  const dryRun = !flags.has("--no-dry-run");
5024
5070
  const repair = !flags.has("--no-repair");
5025
- if (!dryRun && !repair) throw new UsageError$2("`--no-dry-run`과 `--no-repair`는 함께 사용할 수 없습니다.");
5071
+ if (!dryRun && !repair) throw new UsageError$3("`--no-dry-run`과 `--no-repair`는 함께 사용할 수 없습니다.");
5026
5072
  const diagnoseRejections = flags.has("--diagnose-rejections");
5027
- if (diagnoseRejections && !dryRun) throw new UsageError$2("`--diagnose-rejections`는 `--no-dry-run`과 함께 사용할 수 없습니다. 시험 실행이 없으면 확인할 거절이 없습니다.");
5073
+ if (diagnoseRejections && !dryRun) throw new UsageError$3("`--diagnose-rejections`는 `--no-dry-run`과 함께 사용할 수 없습니다. 시험 실행이 없으면 확인할 거절이 없습니다.");
5028
5074
  const resetCmd = values.get("--reset-cmd");
5029
- if (!dryRun && resetCmd !== void 0) throw new UsageError$2("`--no-dry-run`과 `--reset-cmd`는 함께 사용할 수 없습니다.");
5030
- if (resetCmd !== void 0 && resetCmd.trim() === "") throw new UsageError$2("`--reset-cmd` 옵션 값이 필요합니다.");
5075
+ if (!dryRun && resetCmd !== void 0) throw new UsageError$3("`--no-dry-run`과 `--reset-cmd`는 함께 사용할 수 없습니다.");
5076
+ if (resetCmd !== void 0 && resetCmd.trim() === "") throw new UsageError$3("`--reset-cmd` 옵션 값이 필요합니다.");
5031
5077
  const fixturesPath = values.get("--fixtures");
5032
- if (fixturesPath !== void 0 && fixturesPath.trim() === "") throw new UsageError$2("`--fixtures` 옵션 값이 필요합니다.");
5078
+ if (fixturesPath !== void 0 && fixturesPath.trim() === "") throw new UsageError$3("`--fixtures` 옵션 값이 필요합니다.");
5033
5079
  return Object.freeze({
5034
5080
  suiteId: values.get("--suite-id"),
5035
5081
  name: values.get("--name"),
@@ -6702,7 +6748,7 @@ async function runGenerateCommand(argv, deps) {
6702
6748
  try {
6703
6749
  input = parseGenerateCommand(argv[0] === "generate" ? argv.slice(1) : argv);
6704
6750
  } catch (error) {
6705
- const message = error instanceof UsageError$2 ? error.message : "generate 입력을 해석할 수 없습니다.";
6751
+ const message = error instanceof UsageError$3 ? error.message : "generate 입력을 해석할 수 없습니다.";
6706
6752
  deps.writeStderr(`오류 [CLI_USAGE]: ${message}\n해결: ${GENERATE_USAGE_HINT}\n`);
6707
6753
  return 1;
6708
6754
  }
@@ -6896,7 +6942,7 @@ function nodeReviewIO(input = process.stdin, output = process.stdout) {
6896
6942
  //#endregion
6897
6943
  //#region src/optimize-command.ts
6898
6944
  /** 서버 연결 stderr 진단의 줄 수. `test` 의 `--stderr-lines` 기본값과 같다. */
6899
- const STDERR_LINES = 20;
6945
+ const STDERR_LINES$1 = 20;
6900
6946
  /** 오버레이가 기록하는 tools 외 능력(§5.1 `otherCapabilities`). 이 밖의 키는 프록시와 무관하다. */
6901
6947
  const OTHER_CAPABILITIES = [
6902
6948
  "completions",
@@ -6904,7 +6950,7 @@ const OTHER_CAPABILITIES = [
6904
6950
  "prompts",
6905
6951
  "resources"
6906
6952
  ];
6907
- var UsageError$1 = class extends Error {
6953
+ var UsageError$2 = class extends Error {
6908
6954
  hint;
6909
6955
  constructor(message, hint = OPTIMIZE_USAGE_HINT) {
6910
6956
  super(message);
@@ -6915,7 +6961,7 @@ var UsageError$1 = class extends Error {
6915
6961
  * `test` 는 받지만 `optimize` 에는 뜻이 없는 옵션. `parseTestCommand` 에 넘기기 전에 여기서
6916
6962
  * 거절한다. 넘기면 조용히 통과하거나 "test 옵션" 이라는 남의 이름으로 거절된다.
6917
6963
  */
6918
- const TEST_ONLY_OPTIONS = /* @__PURE__ */ new Set([
6964
+ const TEST_ONLY_OPTIONS$1 = /* @__PURE__ */ new Set([
6919
6965
  "--junit",
6920
6966
  "--repair-bundle",
6921
6967
  "--determinism",
@@ -6925,7 +6971,7 @@ const TEST_ONLY_OPTIONS = /* @__PURE__ */ new Set([
6925
6971
  "--record-session"
6926
6972
  ]);
6927
6973
  /** 대상 옵션 중 값을 다음 토큰으로 받는 것. 그 값이 `--junit` 처럼 생겨도 옵션으로 읽지 않는다. */
6928
- const TARGET_VALUE_OPTIONS = /* @__PURE__ */ new Set([
6974
+ const TARGET_VALUE_OPTIONS$1 = /* @__PURE__ */ new Set([
6929
6975
  "--command",
6930
6976
  "--arg",
6931
6977
  "--env",
@@ -6946,31 +6992,31 @@ function parseOptimizeCommand(argv) {
6946
6992
  }
6947
6993
  const name = token.startsWith("--") && token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
6948
6994
  if (name === "--out") {
6949
- if (outPath !== void 0) throw new UsageError$1("`--out`은 한 번만 사용할 수 있습니다.");
6995
+ if (outPath !== void 0) throw new UsageError$2("`--out`은 한 번만 사용할 수 있습니다.");
6950
6996
  const value = token === "--out" ? argv[++index] : token.slice(6);
6951
- if (value === void 0 || value.trim() === "" || value.startsWith("--")) throw new UsageError$1("`--out` 옵션 값이 필요합니다.");
6997
+ if (value === void 0 || value.trim() === "" || value.startsWith("--")) throw new UsageError$2("`--out` 옵션 값이 필요합니다.");
6952
6998
  outPath = value;
6953
6999
  continue;
6954
7000
  }
6955
7001
  if (name === "--json") {
6956
- if (token !== "--json") throw new UsageError$1("`--json`은 값을 받지 않습니다.");
6957
- if (json) throw new UsageError$1("`--json`은 한 번만 사용할 수 있습니다.");
7002
+ if (token !== "--json") throw new UsageError$2("`--json`은 값을 받지 않습니다.");
7003
+ if (json) throw new UsageError$2("`--json`은 한 번만 사용할 수 있습니다.");
6958
7004
  json = true;
6959
7005
  continue;
6960
7006
  }
6961
- if (TEST_ONLY_OPTIONS.has(name)) throw new UsageError$1(`지원하지 않는 optimize 옵션 '${escapeTerminalText$2(name)}'입니다.`);
6962
- if (token.startsWith("-") && !TARGET_VALUE_OPTIONS.has(name)) throw new UsageError$1(`지원하지 않는 optimize 옵션 '${escapeTerminalText$2(name)}'입니다.`);
7007
+ if (TEST_ONLY_OPTIONS$1.has(name)) throw new UsageError$2(`지원하지 않는 optimize 옵션 '${escapeTerminalText$2(name)}'입니다.`);
7008
+ if (token.startsWith("-") && !TARGET_VALUE_OPTIONS$1.has(name)) throw new UsageError$2(`지원하지 않는 optimize 옵션 '${escapeTerminalText$2(name)}'입니다.`);
6963
7009
  targetArgv.push(token);
6964
- if (TARGET_VALUE_OPTIONS.has(token) && index + 1 < argv.length) targetArgv.push(argv[++index]);
7010
+ if (TARGET_VALUE_OPTIONS$1.has(token) && index + 1 < argv.length) targetArgv.push(argv[++index]);
6965
7011
  }
6966
- if (outPath === void 0) throw new UsageError$1("--out <overlay.json> 이 필요합니다. 오버레이를 어디에 쓸지 이 도구는 정하지 않습니다.", "mcpeak optimize --out ./server.optimize.json -- <서버 명령>");
7012
+ if (outPath === void 0) throw new UsageError$2("--out <overlay.json> 이 필요합니다. 오버레이를 어디에 쓸지 이 도구는 정하지 않습니다.", "mcpeak optimize --out ./server.optimize.json -- <서버 명령>");
6967
7013
  let target;
6968
7014
  try {
6969
7015
  target = parseTestCommand([PLACEHOLDER_SUITE, ...targetArgv]).target;
6970
7016
  } catch (error) {
6971
- const message = usageMessageOf(error);
7017
+ const message = usageMessageOf$1(error);
6972
7018
  if (message === void 0) throw error;
6973
- throw new UsageError$1(message);
7019
+ throw new UsageError$2(message);
6974
7020
  }
6975
7021
  return Object.freeze({
6976
7022
  outPath,
@@ -6979,14 +7025,14 @@ function parseOptimizeCommand(argv) {
6979
7025
  });
6980
7026
  }
6981
7027
  /** `parseTestCommand` 가 던진 사용 오류의 문장. 그 오류 클래스는 export 되지 않아 모양으로 본다. */
6982
- function usageMessageOf(error) {
7028
+ function usageMessageOf$1(error) {
6983
7029
  if (typeof error !== "object" || error === null || !("failure" in error)) return void 0;
6984
7030
  const failure = error.failure;
6985
7031
  if (typeof failure !== "object" || failure === null) return void 0;
6986
7032
  const { code, message } = failure;
6987
7033
  return code === "CLI_USAGE" && typeof message === "string" ? message : void 0;
6988
7034
  }
6989
- function usageFailure(deps, message, hint) {
7035
+ function usageFailure$1(deps, message, hint) {
6990
7036
  deps.writeStderr(`오류 [CLI_USAGE]: ${message}\n해결: ${hint}\n`);
6991
7037
  return 1;
6992
7038
  }
@@ -6995,8 +7041,8 @@ async function runOptimizeCommand(argv, deps) {
6995
7041
  try {
6996
7042
  input = parseOptimizeCommand(argv[0] === "optimize" ? argv.slice(1) : argv);
6997
7043
  } catch (error) {
6998
- if (!(error instanceof UsageError$1)) throw error;
6999
- return usageFailure(deps, error.message, error.hint);
7044
+ if (!(error instanceof UsageError$2)) throw error;
7045
+ return usageFailure$1(deps, error.message, error.hint);
7000
7046
  }
7001
7047
  let serverInfo;
7002
7048
  const capture = (connection) => {
@@ -7015,8 +7061,8 @@ async function runOptimizeCommand(argv, deps) {
7015
7061
  ...deps.readEnv === void 0 ? {} : { readEnv: deps.readEnv }
7016
7062
  });
7017
7063
  } catch (error) {
7018
- if (error instanceof ConnectTargetError) return usageFailure(deps, error.message, OPTIMIZE_USAGE_HINT);
7019
- return writeCoreFailure(deps, "MCP_CONNECTION_FAILED", void 0, error, {
7064
+ if (error instanceof ConnectTargetError) return usageFailure$1(deps, error.message, OPTIMIZE_USAGE_HINT);
7065
+ return writeCoreFailure$1(deps, "MCP_CONNECTION_FAILED", void 0, error, {
7020
7066
  message: "MCP 서버 연결에 실패했습니다.",
7021
7067
  hint: "command 실행 가능 여부와 MCP 서버 설정을 확인하세요."
7022
7068
  });
@@ -7025,13 +7071,13 @@ async function runOptimizeCommand(argv, deps) {
7025
7071
  try {
7026
7072
  tools = await connection.client.listTools();
7027
7073
  } catch (error) {
7028
- await closeQuietly(connection);
7029
- return writeCoreFailure(deps, "MCP_LIST_TOOLS_FAILED", `서버의 tools/list 를 읽지 못했습니다: ${escapeTerminalText$2(describeTarget(input.target))}`, error, {
7074
+ await closeQuietly$1(connection);
7075
+ return writeCoreFailure$1(deps, "MCP_LIST_TOOLS_FAILED", `서버의 tools/list 를 읽지 못했습니다: ${escapeTerminalText$2(describeTarget(input.target))}`, error, {
7030
7076
  message: "서버가 tools/list 요청에 답하지 않았습니다.",
7031
7077
  hint: "서버가 tools 능력을 광고하는지 확인하세요."
7032
7078
  });
7033
7079
  }
7034
- await closeQuietly(connection);
7080
+ await closeQuietly$1(connection);
7035
7081
  const info = serverInfo;
7036
7082
  const optimizeInput = {
7037
7083
  tools,
@@ -7068,6 +7114,566 @@ function isOptimizeError(error, code) {
7068
7114
  * 꼬리 또는 HTTP 엔드포인트). 연결 실패를 사용자가 가장 먼저 보는 명령이 이것일 수 있어서
7069
7115
  * 진단을 빼지 않는다. `test-command.ts` 의 연결 실패 분기와 같은 규칙이다.
7070
7116
  */
7117
+ function writeCoreFailure$1(deps, code, lead, error, fallback) {
7118
+ const core = coreErrorOf$1(error);
7119
+ const message = core === void 0 ? fallback.message : escapeTerminalText$2(core.message);
7120
+ const hint = core === void 0 ? fallback.hint : escapeTerminalText$2(core.hint);
7121
+ const head = core === void 0 ? code : `${code}/${escapeTerminalText$2(core.code)}`;
7122
+ deps.writeStderr(`오류 [${head}]: ${lead === void 0 ? message : `${lead}\n→ ${message}`}\n해결: ${hint}\n`);
7123
+ const processBlock = processDiagnostics(core?.diagnostics);
7124
+ if (processBlock !== void 0 && hasDiagnosticContent(processBlock)) {
7125
+ const block = renderProcessDiagnostics(processBlock, { maxLines: STDERR_LINES$1 });
7126
+ if (block !== "") deps.writeStderr(`\n${block}`);
7127
+ }
7128
+ const remote = httpDiagnostics(core?.diagnostics);
7129
+ if (remote !== void 0) deps.writeStderr(`\n${renderHttpDiagnostics(remote)}`);
7130
+ return 1;
7131
+ }
7132
+ /** `McpClientError` 를 AggregateError 안까지 찾는다. 연결 정리 실패가 겹치면 그렇게 감싸인다. */
7133
+ function coreErrorOf$1(error) {
7134
+ if (error instanceof AggregateError) {
7135
+ for (const nested of error.errors) {
7136
+ const found = coreErrorOf$1(nested);
7137
+ if (found !== void 0) return found;
7138
+ }
7139
+ return;
7140
+ }
7141
+ if (typeof error === "object" && error !== null && error.name === "McpClientError") {
7142
+ const { code, message, hint, diagnostics } = error;
7143
+ if (typeof code === "string" && typeof message === "string" && typeof hint === "string") return {
7144
+ code,
7145
+ message,
7146
+ hint,
7147
+ diagnostics
7148
+ };
7149
+ }
7150
+ }
7151
+ /**
7152
+ * 도구 목록을 읽은 뒤의 종료. 실패해도 결과를 버리지 않는다. 정상 종료가 안 되면 강제로
7153
+ * 끝내 자식이 남지 않게 하고, 그것도 안 되면 더 할 수 있는 일이 없다.
7154
+ */
7155
+ async function closeQuietly$1(connection) {
7156
+ try {
7157
+ await connection.close();
7158
+ } catch {
7159
+ await connection.forceClose().catch(() => {});
7160
+ }
7161
+ }
7162
+ //#endregion
7163
+ //#region src/perf-command.ts
7164
+ /**
7165
+ * `mcpeak perf` 서브커맨드. 명세의 callTool 케이스를 실제 입력으로 반복 호출해 응답 시간을 외부
7166
+ * HTTP 대기와 서버 안 시간으로 나누고, 기준선이 있으면 회귀를 판정한다(설계 §3, §7, §8.4~§8.6).
7167
+ *
7168
+ * 대상 옵션의 해석은 `test` 의 `parseTestCommand` 를 쓴다. 다만 `--url`·`--header-env` 는 다른
7169
+ * 어떤 검사보다 먼저 거절한다. 측정은 서버 프로세스 안에 관측 코드를 실어야 하는데 우리가 띄우지
7170
+ * 않은 프로세스에는 실을 수 없기 때문이다.
7171
+ *
7172
+ * 명세 오류와 연결 실패 문장은 `test-command.ts`·`optimize-command.ts` 의 비공개 함수가 만든다.
7173
+ * 그 파일은 다른 서브커맨드의 것이라 export 를 위해 고치지 않고 같은 문장을 여기서 만든다.
7174
+ * 어긋나면 perf-command.test.ts 의 동등성 테스트가 잡는다(설계 §8.5).
7175
+ *
7176
+ * `@mcpeak/perf` 를 값으로 import 하지 않는다. 정적으로 import 하면 `test` 경로가 그 패키지를
7177
+ * 함께 로드한다. 의존성 주입 형태는 `optimize-command.ts` 를 따른다.
7178
+ */
7179
+ /** 서버 연결 stderr 진단의 줄 수. `test` 의 `--stderr-lines` 기본값과 같다. */
7180
+ const STDERR_LINES = 20;
7181
+ /** 케이스에도 명세에도 타임아웃이 없을 때. `runner` 실행기와 같은 값이다(설계 §3.2 5항). */
7182
+ const DEFAULT_TIMEOUT_MS = 1e4;
7183
+ /** 추적 디렉터리 접두(설계 §8.6). */
7184
+ const TRACE_DIR_PREFIX = "mcpeak-perf-";
7185
+ const NO_OBSERVATION_HINT = "→ Node 서버인지, NODE_OPTIONS 가 서버까지 전달되는지 확인하세요.";
7186
+ var UsageError$1 = class extends Error {
7187
+ hint;
7188
+ constructor(message, hint = PERF_USAGE_HINT) {
7189
+ super(message);
7190
+ this.hint = hint;
7191
+ }
7192
+ };
7193
+ /** 원격 대상 거절. 해결 줄이 사용법 전체가 아니라 바로 쓸 수 있는 명령 모양이다(설계 §8.5). */
7194
+ var RemoteTargetError = class extends Error {};
7195
+ /** `test` 는 받지만 perf 에는 뜻이 없는 옵션. 넘기면 "test 옵션" 이라는 남의 이름으로 거절된다. */
7196
+ const TEST_ONLY_OPTIONS = /* @__PURE__ */ new Set([
7197
+ "--junit",
7198
+ "--repair-bundle",
7199
+ "--determinism",
7200
+ "--reset-cmd",
7201
+ "--stderr-lines",
7202
+ "--session",
7203
+ "--record-session"
7204
+ ]);
7205
+ /** 대상 옵션 중 값을 다음 토큰으로 받는 것. 그 값이 `--junit` 처럼 생겨도 옵션으로 읽지 않는다. */
7206
+ const TARGET_VALUE_OPTIONS = /* @__PURE__ */ new Set([
7207
+ "--command",
7208
+ "--arg",
7209
+ "--env"
7210
+ ]);
7211
+ const REMOTE_OPTIONS = /* @__PURE__ */ new Set(["--url", "--header-env"]);
7212
+ /** 값을 받는 숫자 옵션과 범위(설계 §8.4). */
7213
+ const NUMBER_OPTIONS = {
7214
+ "--repeat": {
7215
+ min: 1,
7216
+ max: 100,
7217
+ fallback: 5
7218
+ },
7219
+ "--interval-ms": {
7220
+ min: 0,
7221
+ max: 6e4,
7222
+ fallback: 0
7223
+ },
7224
+ "--threshold-pct": {
7225
+ min: 0,
7226
+ max: 1e3,
7227
+ fallback: 50
7228
+ },
7229
+ "--threshold-ms": {
7230
+ min: 0,
7231
+ max: 6e4,
7232
+ fallback: 20
7233
+ }
7234
+ };
7235
+ const isNumberOption = (name) => name in NUMBER_OPTIONS;
7236
+ function optionName(token) {
7237
+ return token.startsWith("--") && token.includes("=") ? token.slice(0, token.indexOf("=")) : token;
7238
+ }
7239
+ function parsePerfCommand(argv) {
7240
+ const separator = argv.indexOf("--");
7241
+ if ((separator === -1 ? argv : argv.slice(0, separator)).some((token) => REMOTE_OPTIONS.has(optionName(token)))) throw new RemoteTargetError();
7242
+ const numbers = /* @__PURE__ */ new Map();
7243
+ const tools = [];
7244
+ let baselinePath;
7245
+ let updateBaseline = false;
7246
+ let json = false;
7247
+ const positionals = [];
7248
+ const targetArgv = [];
7249
+ const readValue = (token, name, index) => {
7250
+ const value = token === name ? argv[index + 1] : token.slice(`${name}=`.length);
7251
+ if (value === void 0 || value.trim() === "" || value.startsWith("--")) throw new UsageError$1(`\`${name}\` 옵션 값이 필요합니다.`);
7252
+ return {
7253
+ value,
7254
+ next: token === name ? index + 1 : index
7255
+ };
7256
+ };
7257
+ const once = (name, seen) => {
7258
+ if (seen) throw new UsageError$1(`\`${name}\`은 한 번만 사용할 수 있습니다.`);
7259
+ };
7260
+ const flag = (token, name) => {
7261
+ if (token !== name) throw new UsageError$1(`\`${name}\`은 값을 받지 않습니다.`);
7262
+ };
7263
+ for (let index = 0; index < argv.length; index++) {
7264
+ const token = argv[index];
7265
+ if (token === "--") {
7266
+ targetArgv.push(...argv.slice(index));
7267
+ break;
7268
+ }
7269
+ const name = optionName(token);
7270
+ if (isNumberOption(name)) {
7271
+ once(name, numbers.has(name));
7272
+ const { value, next } = readValue(token, name, index);
7273
+ index = next;
7274
+ const { min, max } = NUMBER_OPTIONS[name];
7275
+ const parsed = /^\d+$/.test(value) ? Number(value) : NaN;
7276
+ if (!Number.isSafeInteger(parsed) || parsed < min || parsed > max) throw new UsageError$1(`\`${name}\` 는 ${min} 이상 ${max} 이하의 정수여야 합니다. 받은 값: '${escapeTerminalText$2(value)}'`);
7277
+ numbers.set(name, parsed);
7278
+ continue;
7279
+ }
7280
+ if (name === "--tool") {
7281
+ const { value, next } = readValue(token, name, index);
7282
+ index = next;
7283
+ tools.push(value);
7284
+ continue;
7285
+ }
7286
+ if (name === "--baseline") {
7287
+ once(name, baselinePath !== void 0);
7288
+ const { value, next } = readValue(token, name, index);
7289
+ index = next;
7290
+ baselinePath = value;
7291
+ continue;
7292
+ }
7293
+ if (name === "--update-baseline") {
7294
+ flag(token, name);
7295
+ once(name, updateBaseline);
7296
+ updateBaseline = true;
7297
+ continue;
7298
+ }
7299
+ if (name === "--json") {
7300
+ flag(token, name);
7301
+ once(name, json);
7302
+ json = true;
7303
+ continue;
7304
+ }
7305
+ if (TEST_ONLY_OPTIONS.has(name) || token.startsWith("-") && !TARGET_VALUE_OPTIONS.has(name)) throw new UsageError$1(`지원하지 않는 perf 옵션 '${escapeTerminalText$2(name)}'입니다.`);
7306
+ if (!token.startsWith("-")) {
7307
+ positionals.push(token);
7308
+ continue;
7309
+ }
7310
+ targetArgv.push(token);
7311
+ if (TARGET_VALUE_OPTIONS.has(token) && index + 1 < argv.length) targetArgv.push(argv[++index]);
7312
+ }
7313
+ if (updateBaseline && baselinePath === void 0) throw new UsageError$1("--update-baseline 은 --baseline <path> 와 함께 써야 합니다.");
7314
+ if (updateBaseline && tools.length > 0) throw new UsageError$1("--update-baseline 은 --tool 과 함께 쓸 수 없습니다. 기준은 명세 전체로 갱신합니다.");
7315
+ const [suitePath, ...rest] = positionals;
7316
+ let target;
7317
+ try {
7318
+ target = parseTestCommand([
7319
+ suitePath ?? "",
7320
+ ...rest,
7321
+ ...targetArgv
7322
+ ]).target;
7323
+ } catch (error) {
7324
+ const message = usageMessageOf(error);
7325
+ if (message === void 0) throw error;
7326
+ throw new UsageError$1(message);
7327
+ }
7328
+ const number = (name) => numbers.get(name) ?? NUMBER_OPTIONS[name].fallback;
7329
+ return Object.freeze({
7330
+ suitePath,
7331
+ target,
7332
+ repeat: number("--repeat"),
7333
+ intervalMs: number("--interval-ms"),
7334
+ tools: Object.freeze([...tools]),
7335
+ ...baselinePath === void 0 ? {} : { baselinePath },
7336
+ updateBaseline,
7337
+ thresholdPct: number("--threshold-pct"),
7338
+ thresholdMs: number("--threshold-ms"),
7339
+ json
7340
+ });
7341
+ }
7342
+ /** `parseTestCommand` 가 던진 사용 오류의 문장. 그 오류 클래스는 export 되지 않아 모양으로 본다. */
7343
+ function usageMessageOf(error) {
7344
+ if (typeof error !== "object" || error === null || !("failure" in error)) return void 0;
7345
+ const failure = error.failure;
7346
+ if (typeof failure !== "object" || failure === null) return void 0;
7347
+ const { code, message } = failure;
7348
+ return code === "CLI_USAGE" && typeof message === "string" ? message : void 0;
7349
+ }
7350
+ function usageFailure(deps, message, hint) {
7351
+ deps.writeStderr(`오류 [CLI_USAGE]: ${message}\n해결: ${hint}\n`);
7352
+ return 1;
7353
+ }
7354
+ function writeSuiteFailure(deps, failure) {
7355
+ let text = `오류 [${escapeTerminalText$2(failure.code)}]: ${failure.message}\n해결: ${failure.hint}`;
7356
+ for (const issue of failure.issues ?? []) text += `\n- [${escapeTerminalText$2(issue.code)}] ${escapeTerminalText$2(issue.path)}: ${escapeTerminalText$2(issue.message)}\n 해결: ${escapeTerminalText$2(issue.hint)}`;
7357
+ deps.writeStderr(`${text}\n`);
7358
+ return 1;
7359
+ }
7360
+ function errnoOf(error) {
7361
+ const code = error?.code;
7362
+ return typeof code === "string" || typeof code === "number" ? String(code) : "알 수 없음";
7363
+ }
7364
+ function suiteReadFailure(path, error) {
7365
+ const shown = escapeTerminalText$2(path);
7366
+ const errno = errnoOf(error);
7367
+ if (errno === "ENOENT") return {
7368
+ code: "SUITE_READ_FAILED",
7369
+ message: `테스트 명세 파일이 그 경로에 없습니다: ${shown}`,
7370
+ hint: "경로와 파일 이름을 확인하세요. 상대 경로는 명령을 실행한 디렉터리 기준입니다."
7371
+ };
7372
+ if (errno === "EACCES" || errno === "EPERM") return {
7373
+ code: "SUITE_READ_FAILED",
7374
+ message: `테스트 명세 파일을 읽을 권한이 없습니다: ${shown}`,
7375
+ hint: "그 파일의 읽기 권한을 확인하세요. 경로 자체는 찾았습니다."
7376
+ };
7377
+ if (errno === "EISDIR") return {
7378
+ code: "SUITE_READ_FAILED",
7379
+ message: `그 경로는 파일이 아니라 디렉터리입니다: ${shown}`,
7380
+ hint: "스위트 JSON 파일의 경로를 주세요."
7381
+ };
7382
+ return {
7383
+ code: "SUITE_READ_FAILED",
7384
+ message: `테스트 명세 파일을 읽지 못했습니다: ${shown}\n errno: ${escapeTerminalText$2(errno)}`,
7385
+ hint: "명세 경로와 읽기 권한을 확인하세요."
7386
+ };
7387
+ }
7388
+ function suiteFormatFailure(path) {
7389
+ const ext = (0, node_path.extname)(path);
7390
+ return {
7391
+ code: "SUITE_FORMAT_UNSUPPORTED",
7392
+ message: ext === "" ? `테스트 명세는 .json 파일이어야 합니다. 준 경로에 확장자가 없습니다: ${escapeTerminalText$2(path)}` : `테스트 명세는 .json 파일이어야 합니다. 준 확장자: ${escapeTerminalText$2(ext)}\n 경로: ${escapeTerminalText$2(path)}`,
7393
+ hint: "경로가 .json 으로 끝나는지 확인하세요. 이 단계는 파일을 열지 않고 확장자만 봅니다."
7394
+ };
7395
+ }
7396
+ const INTERNAL_FAILURE = {
7397
+ code: "CLI_INTERNAL_ERROR",
7398
+ message: "예상하지 못한 CLI 내부 오류가 발생했습니다.",
7399
+ hint: "다시 실행한 뒤 재현 정보와 함께 이슈를 보고하세요."
7400
+ };
7401
+ /** 명세를 읽고 검증한다. 순서와 문장은 test 와 같다(설계 §3.2 1항, §8.5). */
7402
+ async function loadSuite(path, deps) {
7403
+ if ((0, node_path.extname)(path).toLowerCase() !== ".json") return suiteFormatFailure(path);
7404
+ let bytes;
7405
+ try {
7406
+ bytes = await deps.readFile(path);
7407
+ } catch (error) {
7408
+ return suiteReadFailure(path, error);
7409
+ }
7410
+ let decoded;
7411
+ try {
7412
+ decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
7413
+ } catch {
7414
+ return {
7415
+ code: "SUITE_ENCODING_INVALID",
7416
+ message: `테스트 명세 파일이 유효한 UTF-8이 아닙니다: ${escapeTerminalText$2(path)}`,
7417
+ hint: "명세를 UTF-8 JSON으로 다시 저장하세요."
7418
+ };
7419
+ }
7420
+ let parsed;
7421
+ try {
7422
+ parsed = JSON.parse(decoded);
7423
+ } catch (error) {
7424
+ const reason = error instanceof Error ? error.message : String(error);
7425
+ return {
7426
+ code: "SUITE_JSON_INVALID",
7427
+ message: `테스트 명세의 JSON 문법이 유효하지 않습니다: ${escapeTerminalText$2(path)}\n ${escapeTerminalText$2(reason)}`,
7428
+ hint: "JSON 문법과 쉼표, 따옴표를 확인하세요."
7429
+ };
7430
+ }
7431
+ let validated;
7432
+ try {
7433
+ validated = deps.validateSuite(parsed);
7434
+ } catch {
7435
+ return INTERNAL_FAILURE;
7436
+ }
7437
+ if (!validated.valid) return {
7438
+ code: "SUITE_VALIDATION_FAILED",
7439
+ message: "MCP 테스트 명세가 유효하지 않습니다.",
7440
+ hint: "아래 명세 오류를 모두 수정하세요.",
7441
+ issues: validated.issues
7442
+ };
7443
+ return validated.value;
7444
+ }
7445
+ /**
7446
+ * 측정 대상은 callTool 케이스 중 `{ type: "isError", expected: true }` 단언이 없는 것이다. 거절을
7447
+ * 기대하는 케이스는 거절 경로의 시간이라 뺀다(설계 §3.1). 타임아웃 우선순위는 runner 실행기와 같다.
7448
+ */
7449
+ function planOf(suite, input) {
7450
+ const calls = [];
7451
+ for (const spec of suite.cases) {
7452
+ if (spec.operation.type !== "callTool") continue;
7453
+ const operation = spec.operation;
7454
+ if (spec.assertions.some((a) => a.type === "isError" && a.expected === true)) continue;
7455
+ if (input.tools.length > 0 && !input.tools.includes(operation.tool)) continue;
7456
+ calls.push({
7457
+ caseId: spec.id,
7458
+ tool: operation.tool,
7459
+ input: operation.input,
7460
+ timeoutMs: spec.timeoutMs ?? suite.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS
7461
+ });
7462
+ }
7463
+ return {
7464
+ calls,
7465
+ repeat: input.repeat,
7466
+ intervalMs: input.intervalMs
7467
+ };
7468
+ }
7469
+ function isPerfError(error) {
7470
+ return error instanceof Error && error.name === "PerfError" && typeof error.code === "string";
7471
+ }
7472
+ /**
7473
+ * 기준 파일을 측정 전에 읽는다. 측정을 다 하고 나서 기준 파일 오타로 결과를 버리지 않기 위해서다
7474
+ * (설계 §8.5). 없음은 CLI 가 `ENOENT` 로 판정한다.
7475
+ */
7476
+ async function loadBaseline(path, deps) {
7477
+ const invalid = (reason) => `오류 [PERF_BASELINE_INVALID]: 기준 파일을 읽을 수 없습니다: ${escapeTerminalText$2(path)} (${reason})\n→ mcpeak perf 가 쓴 파일인지 확인하고, 아니면 --update-baseline 으로 다시 만드세요.
7478
+ `;
7479
+ let bytes;
7480
+ try {
7481
+ bytes = await deps.readFile(path);
7482
+ } catch (error) {
7483
+ if (errnoOf(error) === "ENOENT") return `오류 [PERF_BASELINE_MISSING]: 기준 파일이 없습니다: ${escapeTerminalText$2(path)}\n→ 처음이면 --update-baseline 을 함께 주어 기준을 만드세요.
7484
+ `;
7485
+ return invalid(escapeTerminalText$2(errnoOf(error)));
7486
+ }
7487
+ try {
7488
+ return deps.perf.parseBaseline(new TextDecoder("utf-8").decode(bytes));
7489
+ } catch (error) {
7490
+ if (isPerfError(error)) return invalid(escapeTerminalText$2(error.message));
7491
+ throw error;
7492
+ }
7493
+ }
7494
+ async function runPerfCommand(argv, deps) {
7495
+ let input;
7496
+ try {
7497
+ input = parsePerfCommand(argv[0] === "perf" ? argv.slice(1) : argv);
7498
+ } catch (error) {
7499
+ if (error instanceof RemoteTargetError) {
7500
+ deps.writeStderr("오류 [CLI_USAGE]: perf 는 원격(Streamable HTTP) 서버를 지원하지 않습니다.\n→ 측정은 서버 프로세스 안에 관측 코드를 실어야 하는데, 우리가 띄우지 않은 프로세스에는 실을 수 없습니다.\n해결: mcpeak perf <suite.json> -- <서버 명령>\n");
7501
+ return 1;
7502
+ }
7503
+ if (!(error instanceof UsageError$1)) throw error;
7504
+ return usageFailure(deps, error.message, error.hint);
7505
+ }
7506
+ const suite = await loadSuite(input.suitePath, deps);
7507
+ if ("code" in suite) return writeSuiteFailure(deps, suite);
7508
+ const plan = planOf(suite, input);
7509
+ if (plan.calls.length === 0) {
7510
+ if (input.tools.length > 0) return usageFailure(deps, `--tool ${input.tools.map(escapeTerminalText$2).join(", ")} 에 해당하는 측정 케이스가 없습니다.`, "명세의 operation.tool 값과 --tool 값을 맞추세요.");
7511
+ return usageFailure(deps, "측정할 케이스가 없습니다. 명세의 callTool 케이스 중 거절(isError: true)을 기대하지 않는 것이 없습니다.", "mcpeak generate 로 성공 케이스를 만들거나 명세를 확인하세요.");
7512
+ }
7513
+ let baseline;
7514
+ if (input.baselinePath !== void 0 && !input.updateBaseline) {
7515
+ const loaded = await loadBaseline(input.baselinePath, deps);
7516
+ if (typeof loaded === "string") {
7517
+ deps.writeStderr(loaded);
7518
+ return 1;
7519
+ }
7520
+ if (input.tools.length > 0) {
7521
+ const planned = new Set(plan.calls.map((call) => call.caseId));
7522
+ baseline = {
7523
+ ...loaded,
7524
+ cases: loaded.cases.filter((row) => planned.has(row.id))
7525
+ };
7526
+ } else baseline = loaded;
7527
+ }
7528
+ const traceDir = await deps.makeTempDir((0, node_path.join)((0, node_os.tmpdir)(), TRACE_DIR_PREFIX));
7529
+ const session = {};
7530
+ let detachSignals;
7531
+ const detach = () => {
7532
+ detachSignals?.();
7533
+ detachSignals = void 0;
7534
+ };
7535
+ detachSignals = deps.onSignal(async (signal) => {
7536
+ if (session.interrupted === void 0) {
7537
+ session.interrupted = signal;
7538
+ const connection = session.connection;
7539
+ if (connection !== void 0) await closeQuietly(connection);
7540
+ }
7541
+ try {
7542
+ deps.removeDirSync(traceDir);
7543
+ } catch {}
7544
+ detach();
7545
+ deps.killSelf(signal);
7546
+ });
7547
+ try {
7548
+ return await measureAndReport(input, plan, baseline, traceDir, deps, session);
7549
+ } finally {
7550
+ detach();
7551
+ await deps.removeDir(traceDir);
7552
+ }
7553
+ }
7554
+ /** 셸이 신호 종료에 보는 값(128 + 신호 번호). 실제로는 재발신이 먼저 프로세스를 끝낸다. */
7555
+ const SIGNAL_EXIT_CODES = {
7556
+ SIGINT: 130,
7557
+ SIGTERM: 143,
7558
+ SIGHUP: 129
7559
+ };
7560
+ async function measureAndReport(input, plan, baseline, traceDir, deps, session) {
7561
+ const env = deps.perf.childEnvironment({
7562
+ traceDir,
7563
+ existingNodeOptions: deps.readEnv("NODE_OPTIONS")
7564
+ });
7565
+ const connectStart = deps.now();
7566
+ let connection;
7567
+ try {
7568
+ connection = await openConnection(input.target, {
7569
+ connectStdio: deps.connectStdio,
7570
+ readEnv: deps.readEnv
7571
+ }, { env });
7572
+ } catch (error) {
7573
+ if (error instanceof ConnectTargetError) return usageFailure(deps, error.message, PERF_USAGE_HINT);
7574
+ return writeCoreFailure(deps, "MCP_CONNECTION_FAILED", void 0, error, {
7575
+ message: "MCP 서버 연결에 실패했습니다.",
7576
+ hint: "command 실행 가능 여부와 MCP 서버 설정을 확인하세요."
7577
+ });
7578
+ }
7579
+ const connectEnd = deps.now();
7580
+ session.connection = connection;
7581
+ let tools;
7582
+ const listStart = deps.now();
7583
+ try {
7584
+ tools = await connection.client.listTools();
7585
+ } catch (error) {
7586
+ session.connection = void 0;
7587
+ await closeQuietly(connection);
7588
+ return writeCoreFailure(deps, "MCP_LIST_TOOLS_FAILED", `서버의 tools/list 를 읽지 못했습니다: ${escapeTerminalText$2(describeTarget(input.target))}`, error, {
7589
+ message: "서버가 tools/list 요청에 답하지 않았습니다.",
7590
+ hint: "서버가 tools 능력을 광고하는지 확인하세요."
7591
+ });
7592
+ }
7593
+ const listEnd = deps.now();
7594
+ deps.writeStderr(`측정: 케이스 ${plan.calls.length}개 × ${plan.repeat}회\n`);
7595
+ let measurements;
7596
+ try {
7597
+ measurements = await deps.perf.measure({
7598
+ client: connection.client,
7599
+ availableTools: new Set(tools.map((tool) => tool.name)),
7600
+ plan,
7601
+ now: deps.now,
7602
+ sleep: deps.sleep,
7603
+ onProgress: (done, total) => {
7604
+ if (session.interrupted !== void 0) return;
7605
+ const index = Math.floor((done - 1) / plan.repeat);
7606
+ const iteration = (done - 1) % plan.repeat + 1;
7607
+ deps.writeStderr(`측정 중 ${done}/${total} · ${escapeTerminalText$2(plan.calls[index]?.caseId ?? "")} (${iteration}/${plan.repeat}회)\n`);
7608
+ }
7609
+ });
7610
+ } finally {
7611
+ session.connection = void 0;
7612
+ await closeQuietly(connection);
7613
+ }
7614
+ if (session.interrupted !== void 0) return SIGNAL_EXIT_CODES[session.interrupted];
7615
+ deps.writeStderr(`측정 완료: 호출 ${measurements.length}회\n`);
7616
+ let entries;
7617
+ try {
7618
+ ({entries} = await deps.perf.readTrace(traceDir));
7619
+ } catch (error) {
7620
+ if (session.interrupted !== void 0) return SIGNAL_EXIT_CODES[session.interrupted];
7621
+ throw error;
7622
+ }
7623
+ if (session.interrupted !== void 0) return SIGNAL_EXIT_CODES[session.interrupted];
7624
+ let report = deps.perf.analyze({
7625
+ plan,
7626
+ measurements,
7627
+ trace: entries,
7628
+ coldStart: {
7629
+ connectMs: Math.round(connectEnd - connectStart),
7630
+ listToolsMs: Math.round(listEnd - listStart)
7631
+ },
7632
+ server: {
7633
+ command: describeTarget(input.target),
7634
+ toolCount: tools.length
7635
+ },
7636
+ generatorVersion: deps.generatorVersion
7637
+ });
7638
+ const observed = report.settings.observation === "preload";
7639
+ if (input.baselinePath !== void 0 && input.updateBaseline) {
7640
+ if (!observed) return unavailable(deps, report, input, "기준 파일을 쓰지 않았습니다.");
7641
+ try {
7642
+ await deps.writeFile(input.baselinePath, `${JSON.stringify(deps.perf.toBaseline(report), null, 2)}\n`);
7643
+ } catch (error) {
7644
+ writeReport(deps, report, input);
7645
+ const code = errnoOf(error);
7646
+ deps.writeStderr(`오류 [PERF_BASELINE_WRITE_FAILED]: 기준 파일을 쓰지 못했습니다 (${escapeTerminalText$2(code)}). 경로: ${escapeTerminalText$2(input.baselinePath)}\n해결: --baseline 경로의 디렉터리가 있는지와 쓰기 권한을 확인하세요.
7647
+ `);
7648
+ return 1;
7649
+ }
7650
+ } else if (input.baselinePath !== void 0 && baseline !== void 0) {
7651
+ if (!observed) return unavailable(deps, report, input, "기준선과 비교하지 않았습니다.");
7652
+ report = deps.perf.compareBaseline(report, baseline, {
7653
+ path: input.baselinePath,
7654
+ thresholdPct: input.thresholdPct,
7655
+ thresholdMs: input.thresholdMs
7656
+ });
7657
+ }
7658
+ writeReport(deps, report, input);
7659
+ return report.exitCode;
7660
+ }
7661
+ function writeReport(deps, report, input) {
7662
+ deps.writeStdout(input.json ? `${JSON.stringify(report, null, 2)}\n` : deps.perf.renderReport(report));
7663
+ }
7664
+ /**
7665
+ * 관측이 없어 기준선을 쓰거나 비교할 수 없다(설계 §8.5, 계획 E9). 잰 응답 시간은 버리지 않고
7666
+ * 리포트를 먼저 낸 뒤 종료 코드 1 로 끝낸다.
7667
+ */
7668
+ function unavailable(deps, report, input, what) {
7669
+ writeReport(deps, report, input);
7670
+ deps.writeStderr(`오류 [PERF_BASELINE_UNAVAILABLE]: 외부 호출을 관측하지 못해 ${what}\n${NO_OBSERVATION_HINT}\n`);
7671
+ return 1;
7672
+ }
7673
+ /**
7674
+ * core 오류를 optimize 와 같은 모양으로 낸다: 한 줄 머리, 해결 줄, 그리고 진단 블록. 문장은
7675
+ * optimize-command.ts 의 같은 이름 함수와 같아야 한다(설계 §8.5).
7676
+ */
7071
7677
  function writeCoreFailure(deps, code, lead, error, fallback) {
7072
7678
  const core = coreErrorOf(error);
7073
7679
  const message = core === void 0 ? fallback.message : escapeTerminalText$2(core.message);
@@ -7102,10 +7708,7 @@ function coreErrorOf(error) {
7102
7708
  };
7103
7709
  }
7104
7710
  }
7105
- /**
7106
- * 도구 목록을 읽은 뒤의 종료. 실패해도 결과를 버리지 않는다. 정상 종료가 안 되면 강제로
7107
- * 끝내 자식이 남지 않게 하고, 그것도 안 되면 더 할 수 있는 일이 없다.
7108
- */
7711
+ /** 정상 종료가 안 되면 강제로 끝내 자식이 남지 않게 한다. 그것도 안 되면 더 할 수 있는 일이 없다. */
7109
7712
  async function closeQuietly(connection) {
7110
7713
  try {
7111
7714
  await connection.close();
@@ -7330,7 +7933,8 @@ const COMMANDS = [
7330
7933
  "record",
7331
7934
  "mock",
7332
7935
  "optimize",
7333
- "audit"
7936
+ "audit",
7937
+ "perf"
7334
7938
  ];
7335
7939
  const unavailableDependencies = {
7336
7940
  readFile: async () => {
@@ -7404,7 +8008,7 @@ function nodeOptimizeDependencies(core, optimize) {
7404
8008
  readEnv: (name) => process.env[name],
7405
8009
  optimize: optimize.optimize,
7406
8010
  renderReport: optimize.renderReport,
7407
- generatorVersion: version$1,
8011
+ generatorVersion: version$2,
7408
8012
  writeFile: (path, text) => (0, node_fs_promises.writeFile)(path, text, "utf8"),
7409
8013
  writeStdout: (text) => void process.stdout.write(text),
7410
8014
  writeStderr: (text) => void process.stderr.write(text)
@@ -7430,7 +8034,7 @@ function nodeAuditDependencies(core, audit) {
7430
8034
  writeFile: (path, text) => (0, node_fs_promises.writeFile)(path, text, "utf8"),
7431
8035
  audit: audit.audit,
7432
8036
  renderReport: audit.renderReport,
7433
- generatorVersion: version$2,
8037
+ generatorVersion: version$3,
7434
8038
  writeStdout: (text) => void process.stdout.write(text),
7435
8039
  writeStderr: (text) => void process.stderr.write(text),
7436
8040
  fetch: globalThis.fetch,
@@ -7450,13 +8054,73 @@ function nodeAuditDependencies(core, audit) {
7450
8054
  }
7451
8055
  };
7452
8056
  }
8057
+ /**
8058
+ * `perf` 실행 의존성. `nodeOptimizeDependencies` 와 같은 이유로 함수로 뺀다. 주입 자체를 테스트가
8059
+ * 단언할 수 있어야 빠뜨린 배선이 초록으로 남지 않는다(설계 §8.6).
8060
+ *
8061
+ * `now` 는 `performance.timeOrigin + performance.now()` 다. 자식 프리로드가 같은 시계로 요청
8062
+ * 시각을 적으므로 이 시계여야 호출 창과 요청이 한 축에 놓인다(설계 §2.1). `sleep` 은 signal 이
8063
+ * abort 되면 타이머만 해제하고 끝나지 않는다. 남은 타이머가 CLI 종료를 늦추지 않게 하기 위해서다.
8064
+ * `generatorVersion` 은 리포트의 `generator` 가 `@mcpeak/perf` 이므로 그 패키지의 버전이고, 읽는
8065
+ * 방식은 optimize 와 같다.
8066
+ */
8067
+ function nodePerfDependencies(core, runner, perf) {
8068
+ return {
8069
+ connectStdio: core.connectStdio,
8070
+ readEnv: (name) => process.env[name],
8071
+ readFile: (path) => (0, node_fs_promises.readFile)(path),
8072
+ writeFile: (path, text) => (0, node_fs_promises.writeFile)(path, text, "utf8"),
8073
+ makeTempDir: (prefix) => (0, node_fs_promises.mkdtemp)(prefix),
8074
+ removeDir: (path) => (0, node_fs_promises.rm)(path, {
8075
+ recursive: true,
8076
+ force: true
8077
+ }),
8078
+ removeDirSync: (path) => (0, node_fs.rmSync)(path, {
8079
+ recursive: true,
8080
+ force: true
8081
+ }),
8082
+ onSignal: (handler) => {
8083
+ const listener = (signal) => void handler(signal);
8084
+ process.on("SIGINT", listener);
8085
+ process.on("SIGTERM", listener);
8086
+ process.on("SIGHUP", listener);
8087
+ return () => {
8088
+ process.off("SIGINT", listener);
8089
+ process.off("SIGTERM", listener);
8090
+ process.off("SIGHUP", listener);
8091
+ };
8092
+ },
8093
+ killSelf: (signal) => void process.kill(process.pid, signal),
8094
+ now: () => performance.timeOrigin + performance.now(),
8095
+ sleep: (ms, signal) => new Promise((resolve) => {
8096
+ if (signal?.aborted === true) return;
8097
+ const timer = setTimeout(resolve, ms);
8098
+ signal?.addEventListener("abort", () => clearTimeout(timer), { once: true });
8099
+ }),
8100
+ validateSuite: runner.validateMcpSuite,
8101
+ perf: {
8102
+ measure: perf.measure,
8103
+ readTrace: perf.readTrace,
8104
+ analyze: perf.analyze,
8105
+ compareBaseline: perf.compareBaseline,
8106
+ toBaseline: perf.toBaseline,
8107
+ parseBaseline: perf.parseBaseline,
8108
+ renderReport: perf.renderReport,
8109
+ childEnvironment: perf.childEnvironment
8110
+ },
8111
+ generatorVersion: version$1,
8112
+ writeStdout: (text) => void process.stdout.write(text),
8113
+ writeStderr: (text) => void process.stderr.write(text)
8114
+ };
8115
+ }
7453
8116
  /** `mcpeak help <이름>` 으로 볼 수 있는 명령. 위 두 갈래가 같은 목록을 봐야 한다. */
7454
8117
  const HELP_TOPICS = [
7455
8118
  "test",
7456
8119
  "generate",
7457
8120
  "repair",
7458
8121
  "optimize",
7459
- "audit"
8122
+ "audit",
8123
+ "perf"
7460
8124
  ];
7461
8125
  const isHelpTopic = (value) => HELP_TOPICS.includes(value);
7462
8126
  async function run(argv) {
@@ -7593,6 +8257,22 @@ async function run(argv) {
7593
8257
  }
7594
8258
  return runAuditCommand(argv, nodeAuditDependencies(core, audit));
7595
8259
  }
8260
+ if (argv[0] === "perf") {
8261
+ let core;
8262
+ let runner;
8263
+ let perf;
8264
+ try {
8265
+ [core, runner, perf] = await Promise.all([
8266
+ import("@mcpeak/core"),
8267
+ import("@mcpeak/runner"),
8268
+ import("@mcpeak/perf")
8269
+ ]);
8270
+ } catch {
8271
+ process.stderr.write("오류 [PERF_RUNTIME_UNAVAILABLE]: @mcpeak/perf 를 로드하지 못했습니다.\n해결: 의존성을 설치한 뒤 다시 실행하세요.\n");
8272
+ return 1;
8273
+ }
8274
+ return runPerfCommand(argv, nodePerfDependencies(core, runner, perf));
8275
+ }
7596
8276
  if (argv[0] !== "test") return runCli(argv, unavailableDependencies);
7597
8277
  try {
7598
8278
  if (!parseTestCommand(argv.slice(1)).suitePath.toLowerCase().endsWith(".json")) return runCli(argv, unavailableDependencies);
@@ -7653,6 +8333,12 @@ Object.defineProperty(exports, "nodeOptimizeDependencies", {
7653
8333
  return nodeOptimizeDependencies;
7654
8334
  }
7655
8335
  });
8336
+ Object.defineProperty(exports, "nodePerfDependencies", {
8337
+ enumerable: true,
8338
+ get: function() {
8339
+ return nodePerfDependencies;
8340
+ }
8341
+ });
7656
8342
  Object.defineProperty(exports, "nodeRepairDependencies", {
7657
8343
  enumerable: true,
7658
8344
  get: function() {
@@ -7719,6 +8405,12 @@ Object.defineProperty(exports, "runOptimizeCommand", {
7719
8405
  return runOptimizeCommand;
7720
8406
  }
7721
8407
  });
8408
+ Object.defineProperty(exports, "runPerfCommand", {
8409
+ enumerable: true,
8410
+ get: function() {
8411
+ return runPerfCommand;
8412
+ }
8413
+ });
7722
8414
  Object.defineProperty(exports, "runRepairCommand", {
7723
8415
  enumerable: true,
7724
8416
  get: function() {