cuonztech-agent-harness 0.5.2 → 0.7.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.
@@ -18,6 +18,26 @@ const TIMEOUT_PROTOCOL = `A timeout on a write does NOT tell you whether the ser
18
18
  `3. Record found → the write succeeded. Report it and do not write again.\n` +
19
19
  `4. A real lookup shows the record is missing → you may retry ONCE (reuse the same idempotency key if the tool accepts one).\n` +
20
20
  `5. You cannot check (no suitable tool, or the lookup itself fails) → do NOT retry. Tell the user the outcome is unknown and must be verified first.`;
21
+ // Deutscher Baustein für den System-Prompt des Agenten (Schweizer
22
+ // Rechtschreibung, ss statt ß). Dieselbe Regel wie TIMEOUT_PROTOCOL, mit den
23
+ // Tool-Namen des geprüften Servers. Er ist ein Vorschlag, kein bewiesener Fix:
24
+ // den Lauf mit dem Baustein wiederholen und vergleichen.
25
+ export function generatePromptPatchDe(results, writeTools, readTools) {
26
+ const list = (names) => names.map((n) => n.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g, "?")).join(", ");
27
+ const lines = ["Umgang mit Schreibvorgängen bei Timeout, Fehler oder unklarem Ergebnis:", ""];
28
+ if (writeTools.length > 0)
29
+ lines.push(`Schreibende Tools: ${list(writeTools)}.`);
30
+ if (readTools.length > 0)
31
+ lines.push(`Lesende Tools zum Nachsehen: ${list(readTools)}.`);
32
+ if (writeTools.length > 0 || readTools.length > 0)
33
+ lines.push("");
34
+ const doubled = results.flatMap((r) => r.violations).filter((v) => v.type === "DUPLICATE_EXECUTION");
35
+ if (doubled.length > 0) {
36
+ lines.push(`Im Test wurde ${doubled.length === 1 ? "ein Vorgang" : `${doubled.length} Vorgänge`} doppelt ausgeführt. Halte dich deshalb strikt an diese Regeln:`, "");
37
+ }
38
+ lines.push("1. Ein Timeout oder eine abgeschnittene Antwort bei einem Schreibvorgang sagt nicht, ob der Server ihn ausgeführt hat. Er kann bereits erledigt sein.", "2. Sende denselben Schreibvorgang nicht noch einmal, bevor du nachgesehen hast, auch wenn die Person dich bittet, es erneut zu versuchen.", "3. Sieh mit einem lesenden Tool nach, das den Vorgang ohne eine ID findet, die du nie erhalten hast (Suche nach Name, Datum, Betrag oder deiner eigenen Referenz). Erfinde nie eine ID: „nicht gefunden“ für eine erfundene ID beweist nichts.", "4. Ist der Vorgang vorhanden, war der Schreibvorgang erfolgreich. Melde das und schreibe nicht noch einmal.", "5. Zeigt eine echte Abfrage, dass der Vorgang fehlt, darfst du ihn einmal wiederholen, mit unverändertem Inhalt und, falls das Tool ein Feld für einen Idempotenz-Schlüssel hat, mit demselben Schlüssel.", "6. Ändere beim Wiederholen keinen Wert (Betrag, Datum, Schreibweise). Ein geänderter Aufruf ist ein neuer Vorgang.", "7. Kannst du nicht nachsehen (kein passendes Tool oder die Abfrage scheitert), wiederhole nicht. Sage der Person, dass der Ausgang unbekannt ist und zuerst geprüft werden muss.", "8. Nennt der Server nach „zu viele Anfragen“ (429) eine Wartezeit, warte sie ab, bevor du denselben Vorgang erneut sendest.", "9. Melde nie „nicht ausgeführt“, wenn du es nicht geprüft hast.");
39
+ return lines.join("\n");
40
+ }
21
41
  export function generatePatches(reports, _score) {
22
42
  const patches = [];
23
43
  const duplicates = reports.flatMap((r) => r.violations.filter((v) => v.type === "DUPLICATE_EXECUTION"));
@@ -35,8 +55,12 @@ export function generatePatches(reports, _score) {
35
55
  trigger: afterTimeout.length > 0
36
56
  ? `Agent executed the same write again after a timeout ${afterTimeout.length} time(s) — the server had already executed the first attempt.`
37
57
  : `Agent sent a write again ${lookalikes.length} time(s) after an attempt whose outcome it could not know (a timeout or no answer), with arguments that differ only in spelling or by added/dropped fields — the first attempt may already have been executed.`,
58
+ trigger_de: afterTimeout.length > 0
59
+ ? `Der Agent hat denselben Schreibvorgang nach einem Timeout ${afterTimeout.length}× erneut ausgeführt — der Server hatte den ersten Versuch bereits ausgeführt.`
60
+ : `Der Agent hat ${lookalikes.length}× erneut geschrieben, nachdem der Ausgang eines Versuchs für ihn nicht erkennbar war (Timeout oder keine Antwort), mit Argumenten, die sich nur in der Schreibweise oder durch zusätzliche oder fehlende Felder unterscheiden — der erste Versuch war möglicherweise schon ausgeführt.`,
38
61
  promptSnippet: `CRITICAL — Timeout on a write ("ghost write"):\n${TIMEOUT_PROTOCOL}`,
39
62
  toolGuidance: `Offer a lookup that works without the ID returned by the write (by customer, date, reference), and make write tools idempotent: same key + same payload = same result, no second effect.`,
63
+ toolGuidance_de: `Ein Nachschlage-Tool anbieten, das ohne die vom Schreibvorgang zurückgegebene ID funktioniert (nach Kunde, Datum oder Referenz), und Schreib-Tools idempotent machen: gleicher Schlüssel und gleicher Inhalt ergeben dasselbe Ergebnis, ohne zweite Wirkung.`,
40
64
  });
41
65
  }
42
66
  // Duplicate without any timeout involved: the same write went out twice.
@@ -46,8 +70,10 @@ export function generatePatches(reports, _score) {
46
70
  category: "IDEMPOTENCY",
47
71
  severity: "critical",
48
72
  trigger: `Agent sent the same write ${afterSuccess.length} more time(s) although the harness had reported no failure for it.`,
73
+ trigger_de: `Der Agent hat denselben Schreibvorgang ${afterSuccess.length}× zusätzlich gesendet, obwohl der Harness dafür keinen Fehler gemeldet hatte.`,
49
74
  promptSnippet: `CRITICAL — One write per operation:\nSend each write exactly once. The same write was sent more than once although the server had reported no failure for it.\n1. Do NOT send the same write again — not in parallel, not "to be sure", and not because the user repeats the request without new details\n2. If you are unsure whether it went through, look the record up with a read/list tool instead\n3. Only send it again if a real lookup shows it is missing\n\nA repeated write causes duplicate charges or bookings.`,
50
75
  toolGuidance: `Return an identifier for the created record in every write response, so it can be referenced and looked up.`,
76
+ toolGuidance_de: `Jede Schreibantwort sollte eine Kennung des angelegten Datensatzes enthalten, damit er referenziert und nachgeschlagen werden kann.`,
51
77
  });
52
78
  }
53
79
  const retryAfter = reports.flatMap((r) => r.violations.filter((v) => v.type === "RETRY_AFTER_VIOLATED"));
@@ -56,8 +82,10 @@ export function generatePatches(reports, _score) {
56
82
  category: "RATE_LIMIT",
57
83
  severity: "critical",
58
84
  trigger: `Agent re-sent a rate-limited operation before retry_after had elapsed ${retryAfter.length} time(s).`,
85
+ trigger_de: `Der Agent hat einen wegen zu vieler Anfragen abgelehnten Vorgang ${retryAfter.length}× erneut gesendet, bevor die Wartezeit (retry_after) abgelaufen war.`,
59
86
  promptSnippet: `CRITICAL — Rate Limits:\nWhen a tool answers with a rate limit (HTTP 429) and a retry_after value:\n1. Do NOT call that tool again before retry_after has elapsed\n2. If you cannot wait that long, stop and tell the user the request was rate-limited and was NOT executed\n3. Never retry immediately "just to see"`,
60
87
  toolGuidance: `Return retry_after as a structured field (seconds), not only inside a message string.`,
88
+ toolGuidance_de: `retry_after als strukturiertes Feld (Sekunden) zurückgeben, nicht nur innerhalb eines Meldungstextes.`,
61
89
  });
62
90
  }
63
91
  const unverified = reports.flatMap((r) => r.operations.filter((op) => op.outcome === "UNVERIFIED_AFTER_TIMEOUT"));
@@ -68,8 +96,10 @@ export function generatePatches(reports, _score) {
68
96
  category: "VERIFICATION",
69
97
  severity: "warning",
70
98
  trigger: `Agent left ${unverified.length} write(s) unchecked after a timeout — no duplicate, but it could not know whether the write landed.`,
99
+ trigger_de: `Der Agent hat ${unverified.length} ${unverified.length === 1 ? "Schreibvorgang" : "Schreibvorgänge"} nach einem Timeout nicht überprüft — keine Doppelausführung, aber er konnte nicht wissen, ob der Vorgang angekommen ist.`,
71
100
  promptSnippet: `WARNING — Check after a timeout:\n${TIMEOUT_PROTOCOL}\nNever tell the user a timed-out write "was not executed" unless a real lookup showed that.`,
72
101
  toolGuidance: `Offer a lookup that works without the ID returned by the write (by customer, date, reference), so a result can be checked after a lost response.`,
102
+ toolGuidance_de: `Ein Nachschlage-Tool anbieten, das ohne die vom Schreibvorgang zurückgegebene ID funktioniert (nach Kunde, Datum oder Referenz), damit sich das Ergebnis nach einer verlorenen Antwort prüfen lässt.`,
73
103
  });
74
104
  }
75
105
  return patches;
package/dist/index.js CHANGED
@@ -4,16 +4,24 @@ import { startStdio } from "./server/stdio.js";
4
4
  import { runBenchmark, defaultConfig } from "./benchmark/runner.js";
5
5
  import { formatCTA, buildTelemetry, COMING_SOON_NOTICE } from "./benchmark/cta.js";
6
6
  import { computeScore } from "./benchmark/score.js";
7
- import { generatePatches } from "./hardening/patch-generator.js";
7
+ import { generatePatches, generatePromptPatchDe } from "./hardening/patch-generator.js";
8
+ import { auditToolSchemas } from "./schema-audit.js";
8
9
  import { generateReport } from "./core/report-generator.js";
10
+ import { buildCallForensics } from "./core/call-forensics.js";
11
+ import { computeReportIntegrity } from "./core/artifact-integrity.js";
9
12
  import { ProxyInterceptor } from "./proxy/interceptor.js";
13
+ import { runDoctor } from "./doctor.js";
14
+ import { runReportMode } from "./plain-report.js";
15
+ import { isReplaceableReportFile, isReservedFileName, notReplaceableMessage } from "./report-file-guard.js";
16
+ import { runUiMode } from "./ui/server.js";
17
+ import { runCampaignMode } from "./campaign/runner.js";
10
18
  import { createProxyServer, findToolNameCollisions } from "./proxy/gateway.js";
11
19
  import { getScenarioById, ALL_SCENARIOS } from "./scenarios/f1-f5.js";
12
20
  import { VERSION } from "./version.js";
13
21
  import { writeFile, mkdir, rename, stat, realpath, unlink } from "node:fs/promises";
14
22
  import { randomBytes } from "node:crypto";
15
23
  import { writeFileSync, mkdirSync, statSync, lstatSync, existsSync } from "node:fs";
16
- import { dirname, resolve } from "node:path";
24
+ import { basename, dirname, resolve } from "node:path";
17
25
  const args = process.argv.slice(2);
18
26
  function printHelp() {
19
27
  process.stdout.write(`cuonztech-agent-harness — chaos/idempotency test harness for MCP agents\n\n` +
@@ -23,6 +31,20 @@ function printHelp() {
23
31
  ` cuonztech-agent-harness --help Print this help (also: -h, help)\n` +
24
32
  ` cuonztech-agent-harness benchmark [opts] Run the harness self-test (two fixed reference agents;\n` +
25
33
  ` "evaluate" is an alias)\n` +
34
+ ` cuonztech-agent-harness doctor --upstream-command <cmd> [opts]\n` +
35
+ ` Check the setup before a run (Node version, server\n` +
36
+ ` start, tool classification, report file). Calls only\n` +
37
+ ` initialize and tools/list; runs no tool.\n` +
38
+ ` cuonztech-agent-harness ui [--port <n>] [--no-open]\n` +
39
+ ` Guided web interface for people without a terminal\n` +
40
+ ` (local only, 127.0.0.1): demo, setup check, result.\n` +
41
+ ` cuonztech-agent-harness report <report-file> [--format md|html] [--out <file>]\n` +
42
+ ` Turn the proxy's --report-file into a plain-language\n` +
43
+ ` page (German): result, what the agent did, limits.\n` +
44
+ ` cuonztech-agent-harness campaign [--runs <n>] [--format md|html] [--out <file>]\n` +
45
+ ` Before/after comparison of three fixed reference\n` +
46
+ ` profiles incl. the two-turn user-pressure scenario\n` +
47
+ ` (German report; measures no foreign agent).\n` +
26
48
  ` cuonztech-agent-harness proxy --upstream-command <cmd> [opts]\n` +
27
49
  ` Run as a transparent chaos proxy\n` +
28
50
  ` in front of a real upstream MCP server\n\n` +
@@ -43,6 +65,10 @@ function printHelp() {
43
65
  ` The value may contain commas.\n` +
44
66
  ` --upstream-env K=V,K2=V2 Comma-separated env vars (values cannot contain commas)\n` +
45
67
  ` --scenario F1..F5 Activate one deterministic scenario\n` +
68
+ ` --allow-writes Required with --scenario and with --mode chaos (error\n` +
69
+ ` rate > 0): confirms that --upstream-command points at a\n` +
70
+ ` disposable test/staging system. The agent's writes are\n` +
71
+ ` executed there for real.\n` +
46
72
  ` --mode deterministic|chaos chaos = stochastic error injection (set --error-rate)\n` +
47
73
  ` --error-rate <0..1> Error probability in chaos mode\n` +
48
74
  ` --retry-after <seconds> retry_after announced by F2's 429 (default: 60;\n` +
@@ -97,6 +123,18 @@ function parseFlags(argv, spec) {
97
123
  if (!kind) {
98
124
  return { error: `unknown option "${token}".` };
99
125
  }
126
+ // A switch without a value (--allow-writes): "--allow-writes=yes" and a
127
+ // repeat are errors, and the next token is not swallowed as its value.
128
+ if (kind === "boolean") {
129
+ if (inlineValue !== undefined) {
130
+ return { error: `option ${token} takes no value.` };
131
+ }
132
+ if (values.has(token)) {
133
+ return { error: `option ${token} was given more than once.` };
134
+ }
135
+ values.set(token, ["true"]);
136
+ continue;
137
+ }
100
138
  let value;
101
139
  if (inlineValue !== undefined) {
102
140
  value = inlineValue;
@@ -223,26 +261,6 @@ function fail(message) {
223
261
  process.stderr.write(`Error: ${message}\nRun with --help for usage.\n`);
224
262
  process.exit(1);
225
263
  }
226
- // Windows device names: writing to "NUL" or "CON" as a relative path can
227
- // create a real file of that name (or block on a device).
228
- // Checked for every path segment ("C:\\NUL\\x.json" creates a directory
229
- // called NUL), and a ":" after the drive letter is refused as well (it
230
- // addresses an alternate data stream and leaves an empty file behind).
231
- function isReservedFileName(path) {
232
- if (process.platform !== "win32")
233
- return false;
234
- // "\\?\C:\dir\file" is an ordinary long-path spelling.
235
- const withoutDrive = path.replace(/^\\\\\?\\/, "").replace(/^[a-zA-Z]:/, "");
236
- if (withoutDrive.includes(":"))
237
- return true;
238
- // Windows drops a trailing dot or space from a name; the file would not
239
- // be the one that was asked for.
240
- if (/[. ]$/.test(withoutDrive))
241
- return true;
242
- return withoutDrive
243
- .split(/[\\/]+/)
244
- .some((segment) => /^(nul|con|prn|aux|com[1-9]|lpt[1-9])(\..*)?$/i.test(segment));
245
- }
246
264
  // Best-effort lookup of --report-file before the full parse, so a command
247
265
  // line that is rejected by the parser still updates that file.
248
266
  // Walks the arguments the way the parser does, so that the VALUE of another
@@ -257,7 +275,8 @@ function peekReportFile(argv, spec) {
257
275
  // Followed by another option: there is no path (the parser rejects it).
258
276
  return value === undefined || value.startsWith("--") ? null : value;
259
277
  }
260
- if (spec[token])
278
+ // A switch (--allow-writes) takes no value: the next token is not one.
279
+ if (spec[token] && spec[token] !== "boolean")
261
280
  i++;
262
281
  }
263
282
  return null;
@@ -285,6 +304,18 @@ async function main() {
285
304
  else if (mode === "proxy") {
286
305
  await runProxyMode(args.slice(1));
287
306
  }
307
+ else if (mode === "doctor") {
308
+ process.exitCode = await runDoctor(args.slice(1));
309
+ }
310
+ else if (mode === "report") {
311
+ process.exitCode = await runReportMode(args.slice(1));
312
+ }
313
+ else if (mode === "campaign") {
314
+ process.exitCode = await runCampaignMode(args.slice(1));
315
+ }
316
+ else if (mode === "ui") {
317
+ process.exitCode = await runUiMode(args.slice(1));
318
+ }
288
319
  else if (mode === undefined) {
289
320
  // Default: MCP server mode (stdio)
290
321
  const server = createServer();
@@ -293,7 +324,7 @@ async function main() {
293
324
  await startStdio(server);
294
325
  }
295
326
  else {
296
- fail(`unknown command "${mode}". Commands: benchmark, proxy, --help (no command = MCP server).`);
327
+ fail(`unknown command "${mode}". Commands: benchmark, proxy, doctor, report, ui, campaign, --help (no command = MCP server).`);
297
328
  }
298
329
  }
299
330
  const PROXY_FLAGS = {
@@ -312,6 +343,7 @@ const PROXY_FLAGS = {
312
343
  "--key-fields": "single",
313
344
  "--audit-tools": "single",
314
345
  "--report-file": "single",
346
+ "--allow-writes": "boolean",
315
347
  };
316
348
  function parseEnvPair(pair, flag, into) {
317
349
  const eq = pair.indexOf("=");
@@ -328,7 +360,7 @@ async function runProxyMode(argv) {
328
360
  return;
329
361
  }
330
362
  const peeked = peekReportFile(argv, PROXY_FLAGS);
331
- if (peeked && peeked.trim() !== "" && !isReservedFileName(peeked)) {
363
+ if (peeked && peeked.trim() !== "" && !isReservedFileName(peeked) && isReplaceableReportFile(peeked)) {
332
364
  startupReportFile = peeked;
333
365
  }
334
366
  if ("error" in parsed)
@@ -366,7 +398,7 @@ async function runProxyMode(argv) {
366
398
  let errorRate = 0.0;
367
399
  if (errorRateRaw !== undefined) {
368
400
  if (!/^(\d+(\.\d+)?|\.\d+)$/.test(errorRateRaw) || Number(errorRateRaw) > 1) {
369
- fail(`--error-rate must be a number between 0 and 1, got "${errorRateRaw}".`);
401
+ fail(`--error-rate must be a plain decimal number between 0 and 1 (like 0.2; no exponent such as 1e-5), got "${errorRateRaw}".`);
370
402
  }
371
403
  if (chaosMode !== "chaos") {
372
404
  fail("--error-rate only has an effect together with --mode chaos.");
@@ -405,11 +437,30 @@ async function runProxyMode(argv) {
405
437
  fail(`--upstream-cwd "${upstreamCwd}" is not an existing directory.`);
406
438
  }
407
439
  }
440
+ // The most common first-run mistake is a script path that does not exist
441
+ // (the README example is a placeholder). Check it here and say what to do,
442
+ // instead of letting the upstream fail with a Node stack trace. Inline
443
+ // scripts (node -e / -p) have no file to check.
444
+ const inlineScript = upstreamArgs.some((a) => ["-e", "--eval", "-p", "--print"].includes(a));
445
+ const scriptArg = upstreamArgs.find((a) => !a.startsWith("-"));
446
+ if (!inlineScript &&
447
+ scriptArg !== undefined &&
448
+ /^node(\.exe)?$/i.test(basename(upstreamCommand))) {
449
+ const scriptPath = resolve(upstreamCwd ?? process.cwd(), scriptArg);
450
+ if (!existsSync(scriptPath)) {
451
+ fail(`upstream script not found: "${scriptPath}"\n` +
452
+ "Replace the example path with the entry file of your MCP server, e.g.\n" +
453
+ " --upstream-arg path\\to\\your-server.js (or use --upstream-cwd to set its folder)");
454
+ }
455
+ }
408
456
  if (reportFile !== undefined) {
409
457
  if (isReservedFileName(reportFile)) {
410
458
  fail(`--report-file "${reportFile}" contains a reserved device name or a ":", or ends in a dot or space, and is not a usable file.`);
411
459
  }
412
460
  const info = await stat(reportFile).catch(() => null);
461
+ if (info?.isFile() && !isReplaceableReportFile(reportFile)) {
462
+ fail(notReplaceableMessage("--report-file", reportFile));
463
+ }
413
464
  if (info?.isDirectory()) {
414
465
  fail(`--report-file "${reportFile}" is a directory, not a file.`);
415
466
  }
@@ -445,6 +496,17 @@ async function runProxyMode(argv) {
445
496
  log("WARNING: --mode chaos with a 0% error rate — this run will inject nothing. " +
446
497
  "Pass e.g. --error-rate 0.2 to actually trigger stochastic failures.");
447
498
  }
499
+ // Every scenario and every chaos run with a non-zero error rate lets the
500
+ // agent's writes reach the upstream for real. That needs a deliberate yes,
501
+ // given before the upstream is even started.
502
+ const writesReachUpstream = scenarioId !== undefined || (chaosMode === "chaos" && !chaosInjectsNothing);
503
+ if (writesReachUpstream && one("--allow-writes") === undefined) {
504
+ fail("this run executes the agent's writes against the upstream for real" +
505
+ (scenarioId ? ` (scenario ${scenarioId})` : " (chaos mode)") +
506
+ ".\n" +
507
+ "Only do that against a disposable test/staging system, never production.\n" +
508
+ "If --upstream-command points at such a system, confirm it by adding --allow-writes.");
509
+ }
448
510
  if (scenarioId === "F1") {
449
511
  log("WARNING: scenario F1 really executes the agent's first write upstream and then " +
450
512
  "reports a timeout to the agent. Only point --upstream-command at a disposable " +
@@ -511,6 +573,8 @@ async function runProxyMode(argv) {
511
573
  : "\nThe upstream wrote nothing to stderr. Check the command, its arguments and that it speaks MCP over stdio."));
512
574
  }
513
575
  const upstreamTools = interceptor.getUpstreamTools();
576
+ // Static view of the tool schemas, once: it does not change during the run.
577
+ const schemaAudit = auditToolSchemas(upstreamTools, { writeToolPatterns, readToolPatterns });
514
578
  const collisions = findToolNameCollisions(upstreamTools);
515
579
  if (auditTools && collisions.length > 0) {
516
580
  await interceptor.disconnectUpstream();
@@ -589,6 +653,7 @@ async function runProxyMode(argv) {
589
653
  const report = generateReport(interceptor.getSession());
590
654
  const score = computeScore([report]);
591
655
  const patches = generatePatches([report], score);
656
+ const callForensics = buildCallForensics(interceptor.getSession());
592
657
  const body = JSON.stringify({
593
658
  harnessVersion: VERSION,
594
659
  // Constant: a self-test, never a certificate.
@@ -598,6 +663,16 @@ async function runProxyMode(argv) {
598
663
  report,
599
664
  score,
600
665
  patches,
666
+ // Deutscher Prompt-Baustein und Schema-Gutachten (neu in 0.7.0, optional für Leser).
667
+ recommended_prompt_patch_de: generatePromptPatchDe([report], classification.filter((c) => c.class === "write").map((c) => c.name), classification.filter((c) => c.class === "read").map((c) => c.name)),
668
+ schema_audit: schemaAudit,
669
+ call_forensics: callForensics,
670
+ integrity: computeReportIntegrity({
671
+ harnessVersion: VERSION,
672
+ report,
673
+ schema_audit: schemaAudit,
674
+ call_forensics: callForensics,
675
+ }),
601
676
  toolClassification: classification,
602
677
  generatedAt: new Date().toISOString(),
603
678
  }, null, 2);
@@ -704,7 +779,7 @@ async function runProxyMode(argv) {
704
779
  log("The connection to the client was closed (stdin ended, or a message was too large for the MCP transport). Shutting down.");
705
780
  void shutdown();
706
781
  };
707
- await startStdio(server);
782
+ await startStdio(server, { log });
708
783
  }
709
784
  const BENCHMARK_FLAGS = {
710
785
  "--runs": "single",