@gleanwork/mcp-server-tester 1.0.1 → 1.1.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/dist/index.cjs CHANGED
@@ -3126,6 +3126,20 @@ var init_dist3 = __esm({
3126
3126
  };
3127
3127
  }
3128
3128
  });
3129
+
3130
+ // src/assertions/validators/types.ts
3131
+ var SnapshotSanitizers = {
3132
+ /** Replaces Unix timestamps (seconds and milliseconds) with a stable placeholder */
3133
+ TIMESTAMP: "timestamp",
3134
+ /** Replaces UUID v1-v5 strings with a stable placeholder */
3135
+ UUID: "uuid",
3136
+ /** Replaces ISO 8601 date/datetime strings with a stable placeholder */
3137
+ ISO_DATE: "iso-date",
3138
+ /** Replaces MongoDB ObjectId strings with a stable placeholder */
3139
+ OBJECT_ID: "objectId",
3140
+ /** Replaces JWT tokens with a stable placeholder */
3141
+ JWT: "jwt"
3142
+ };
3129
3143
  var MCPHostCapabilitiesSchema = zod.z.object({
3130
3144
  sampling: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
3131
3145
  roots: zod.z.object({
@@ -4411,7 +4425,7 @@ function escapeHtml(text) {
4411
4425
 
4412
4426
  // package.json
4413
4427
  var package_default = {
4414
- version: "1.0.1"};
4428
+ version: "1.1.0"};
4415
4429
 
4416
4430
  // src/mcp/clientFactory.ts
4417
4431
  function getRetryAfterDelayMs(err) {
@@ -5935,20 +5949,6 @@ async function validateJudge(response, config) {
5935
5949
  }
5936
5950
  }
5937
5951
 
5938
- // src/assertions/validators/types.ts
5939
- var SnapshotSanitizers = {
5940
- /** Replaces Unix timestamps (seconds and milliseconds) with a stable placeholder */
5941
- TIMESTAMP: "timestamp",
5942
- /** Replaces UUID v1-v5 strings with a stable placeholder */
5943
- UUID: "uuid",
5944
- /** Replaces ISO 8601 date/datetime strings with a stable placeholder */
5945
- ISO_DATE: "iso-date",
5946
- /** Replaces MongoDB ObjectId strings with a stable placeholder */
5947
- OBJECT_ID: "objectId",
5948
- /** Replaces JWT tokens with a stable placeholder */
5949
- JWT: "jwt"
5950
- };
5951
-
5952
5952
  // src/mcp/fixtures/mcpFixture.ts
5953
5953
  var DEFAULT_CALL_TIMEOUT_MS = 3e4;
5954
5954
  function withCallTimeout(promise, ms, opName) {
@@ -6994,6 +6994,7 @@ function parseStreamJson(stdout) {
6994
6994
  const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
6995
6995
  const toolCalls = [];
6996
6996
  const textParts = [];
6997
+ let usage;
6997
6998
  const conversationHistory = [];
6998
6999
  for (const line of lines) {
6999
7000
  let event;
@@ -7026,16 +7027,28 @@ function parseStreamJson(stdout) {
7026
7027
  }
7027
7028
  }
7028
7029
  }
7029
- if (event.type === "result" && typeof event.result === "string") {
7030
- if (textParts.length === 0) {
7030
+ if (event.type === "result") {
7031
+ if (typeof event.result === "string" && textParts.length === 0) {
7031
7032
  textParts.push(event.result);
7032
7033
  }
7034
+ if (event.usage) {
7035
+ usage = {
7036
+ inputTokens: event.usage.input_tokens ?? 0,
7037
+ outputTokens: event.usage.output_tokens ?? 0,
7038
+ totalCostUsd: event.total_cost_usd ?? 0,
7039
+ durationMs: event.duration_ms ?? 0,
7040
+ durationApiMs: event.duration_api_ms,
7041
+ cacheReadInputTokens: event.usage.cache_read_input_tokens,
7042
+ cacheCreationInputTokens: event.usage.cache_creation_input_tokens
7043
+ };
7044
+ }
7033
7045
  }
7034
7046
  if (event.type === "result" && event.is_error === true) {
7035
7047
  return {
7036
7048
  success: false,
7037
7049
  toolCalls,
7038
- error: typeof event.result === "string" ? event.result : "CLI host reported an error"
7050
+ error: typeof event.result === "string" ? event.result : "CLI host reported an error",
7051
+ usage
7039
7052
  };
7040
7053
  }
7041
7054
  }
@@ -7047,7 +7060,8 @@ function parseStreamJson(stdout) {
7047
7060
  success: true,
7048
7061
  toolCalls,
7049
7062
  response: response || void 0,
7050
- conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0
7063
+ conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0,
7064
+ usage
7051
7065
  };
7052
7066
  }
7053
7067
  function createJsonParser(paths) {
@@ -7293,6 +7307,216 @@ function buildBaselinePassMap(baseline) {
7293
7307
  }
7294
7308
  return map;
7295
7309
  }
7310
+ var KIND_DIRS = {
7311
+ "eval-runner-result": "eval-runs",
7312
+ "reporter-run": "reporter-runs",
7313
+ "eval-run-comparison": "comparisons/eval-runs",
7314
+ "server-comparison": "comparisons/servers"
7315
+ };
7316
+ function createEvalResultStore(config) {
7317
+ if (config.provider === "file") {
7318
+ return new FileEvalResultStore(config);
7319
+ }
7320
+ return new GCSEvalResultStore(config);
7321
+ }
7322
+ function resolveEvalResultStore(store) {
7323
+ return isEvalResultStore(store) ? store : createEvalResultStore(store);
7324
+ }
7325
+ function isEvalResultStore(value) {
7326
+ return typeof value === "object" && value !== null && "saveArtifact" in value && "loadArtifact" in value && "loadLatestArtifact" in value && "listArtifacts" in value;
7327
+ }
7328
+ function createStoredEvalArtifact(options) {
7329
+ const createdAt = options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
7330
+ return {
7331
+ schemaVersion: 1,
7332
+ kind: options.kind,
7333
+ id: options.id ?? createDefaultArtifactId(createdAt),
7334
+ createdAt,
7335
+ metadata: {
7336
+ ...defaultEnvironmentMetadata(),
7337
+ ...options.metadata ?? {}
7338
+ },
7339
+ data: options.data
7340
+ };
7341
+ }
7342
+ function createDefaultArtifactId(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
7343
+ const safeTimestamp = timestamp.replace(/[:.]/g, "-");
7344
+ const runNumber = process.env.GITHUB_RUN_NUMBER;
7345
+ const sha = process.env.GITHUB_SHA?.slice(0, 12);
7346
+ const suffix = runNumber ?? sha;
7347
+ return suffix ? `${safeTimestamp}-${suffix}` : safeTimestamp;
7348
+ }
7349
+ function defaultEnvironmentMetadata() {
7350
+ return {
7351
+ ...process.env.GITHUB_SHA !== void 0 && {
7352
+ gitHash: process.env.GITHUB_SHA
7353
+ },
7354
+ ...process.env.GITHUB_REF_NAME !== void 0 && {
7355
+ branch: process.env.GITHUB_REF_NAME
7356
+ },
7357
+ ...process.env.GITHUB_RUN_NUMBER !== void 0 && {
7358
+ runNumber: process.env.GITHUB_RUN_NUMBER
7359
+ },
7360
+ ...process.env.GITHUB_EVENT_NAME !== void 0 && {
7361
+ trigger: process.env.GITHUB_EVENT_NAME
7362
+ }
7363
+ };
7364
+ }
7365
+ var FileEvalResultStore = class {
7366
+ dir;
7367
+ constructor(config) {
7368
+ this.dir = config.dir;
7369
+ }
7370
+ async saveArtifact(artifact) {
7371
+ const artifactDir = path2.join(this.dir, KIND_DIRS[artifact.kind]);
7372
+ await fs$1.mkdir(artifactDir, { recursive: true });
7373
+ const serialized = JSON.stringify(artifact, null, 2);
7374
+ await fs$1.writeFile(
7375
+ path2.join(artifactDir, `${artifact.id}.json`),
7376
+ serialized,
7377
+ "utf8"
7378
+ );
7379
+ await fs$1.writeFile(path2.join(artifactDir, "latest.json"), serialized, "utf8");
7380
+ }
7381
+ async loadArtifact(kind, id) {
7382
+ const raw = await fs$1.readFile(
7383
+ path2.join(this.dir, KIND_DIRS[kind], `${id}.json`),
7384
+ "utf8"
7385
+ );
7386
+ return JSON.parse(raw);
7387
+ }
7388
+ async loadLatestArtifact(kind) {
7389
+ try {
7390
+ const raw = await fs$1.readFile(
7391
+ path2.join(this.dir, KIND_DIRS[kind], "latest.json"),
7392
+ "utf8"
7393
+ );
7394
+ return JSON.parse(raw);
7395
+ } catch (error) {
7396
+ if (isMissingFileError(error)) {
7397
+ return null;
7398
+ }
7399
+ throw error;
7400
+ }
7401
+ }
7402
+ async listArtifacts(kind, options = {}) {
7403
+ let files;
7404
+ try {
7405
+ files = await fs$1.readdir(path2.join(this.dir, KIND_DIRS[kind]));
7406
+ } catch (error) {
7407
+ if (isMissingFileError(error)) {
7408
+ return [];
7409
+ }
7410
+ throw error;
7411
+ }
7412
+ const summaries = await Promise.all(
7413
+ files.filter((f) => f.endsWith(".json") && f !== "latest.json").map(async (file) => {
7414
+ const raw = await fs$1.readFile(
7415
+ path2.join(this.dir, KIND_DIRS[kind], file),
7416
+ "utf8"
7417
+ );
7418
+ return toSummary(JSON.parse(raw));
7419
+ })
7420
+ );
7421
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7422
+ }
7423
+ };
7424
+ var GCSEvalResultStore = class {
7425
+ bucketName;
7426
+ prefix;
7427
+ storage;
7428
+ constructor(config) {
7429
+ this.bucketName = config.bucket;
7430
+ this.prefix = trimSlashes(config.prefix ?? "");
7431
+ }
7432
+ async saveArtifact(artifact) {
7433
+ const bucket = await this.getBucket();
7434
+ const serialized = JSON.stringify(artifact, null, 2);
7435
+ await bucket.file(
7436
+ this.objectPath(
7437
+ artifact.kind,
7438
+ `${encodeURIComponent(artifact.id)}.json`
7439
+ )
7440
+ ).save(serialized, {
7441
+ contentType: "application/json",
7442
+ resumable: false,
7443
+ validation: false
7444
+ });
7445
+ await bucket.file(this.objectPath(artifact.kind, "latest.json")).save(serialized, {
7446
+ contentType: "application/json",
7447
+ resumable: false,
7448
+ validation: false
7449
+ });
7450
+ }
7451
+ async loadArtifact(kind, id) {
7452
+ const bucket = await this.getBucket();
7453
+ const file = bucket.file(
7454
+ this.objectPath(kind, `${encodeURIComponent(id)}.json`)
7455
+ );
7456
+ const [buffer] = await file.download();
7457
+ return JSON.parse(buffer.toString("utf8"));
7458
+ }
7459
+ async loadLatestArtifact(kind) {
7460
+ const bucket = await this.getBucket();
7461
+ const file = bucket.file(this.objectPath(kind, "latest.json"));
7462
+ const [exists] = await file.exists();
7463
+ if (!exists) return null;
7464
+ const [buffer] = await file.download();
7465
+ return JSON.parse(buffer.toString("utf8"));
7466
+ }
7467
+ async listArtifacts(kind, options = {}) {
7468
+ const bucket = await this.getBucket();
7469
+ const [files] = await bucket.getFiles({
7470
+ prefix: this.objectPath(kind, "")
7471
+ });
7472
+ const summaries = await Promise.all(
7473
+ files.filter(
7474
+ (f) => f.name.endsWith(".json") && !f.name.endsWith("/latest.json")
7475
+ ).map(async (file) => {
7476
+ const [buffer] = await file.download();
7477
+ return toSummary(
7478
+ JSON.parse(buffer.toString("utf8"))
7479
+ );
7480
+ })
7481
+ );
7482
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7483
+ }
7484
+ async getBucket() {
7485
+ if (!this.storage) {
7486
+ const moduleName = "@google-cloud/storage";
7487
+ let Storage;
7488
+ try {
7489
+ const mod = await import(moduleName);
7490
+ Storage = mod.Storage;
7491
+ } catch (error) {
7492
+ throw new Error(
7493
+ `GCS result storage requires the optional \`@google-cloud/storage\` package. Install it and authenticate with Application Default Credentials via GOOGLE_APPLICATION_CREDENTIALS.
7494
+ Original error: ${error instanceof Error ? error.message : String(error)}`
7495
+ );
7496
+ }
7497
+ this.storage = new Storage();
7498
+ }
7499
+ return this.storage.bucket(this.bucketName);
7500
+ }
7501
+ objectPath(kind, filename) {
7502
+ const parts = [this.prefix, KIND_DIRS[kind], filename].filter(Boolean);
7503
+ return parts.join("/");
7504
+ }
7505
+ };
7506
+ function toSummary(artifact) {
7507
+ return {
7508
+ kind: artifact.kind,
7509
+ id: artifact.id,
7510
+ createdAt: artifact.createdAt,
7511
+ metadata: artifact.metadata
7512
+ };
7513
+ }
7514
+ function trimSlashes(value) {
7515
+ return value.replace(/^\/+|\/+$/g, "");
7516
+ }
7517
+ function isMissingFileError(error) {
7518
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
7519
+ }
7296
7520
  var execFileAsync = util.promisify(child_process.execFile);
7297
7521
  async function execFileNoThrow(file, args) {
7298
7522
  try {
@@ -7339,6 +7563,41 @@ function sumUsage(a, b) {
7339
7563
  }
7340
7564
 
7341
7565
  // src/evals/evalRunner.ts
7566
+ function createToolOverrideMCP(mcp, variant) {
7567
+ return {
7568
+ ...mcp,
7569
+ async listTools() {
7570
+ const tools = await mcp.listTools();
7571
+ const knownToolNames = new Set(tools.map((tool2) => tool2.name));
7572
+ const unknownToolNames = Object.keys(variant.tools).filter(
7573
+ (name15) => !knownToolNames.has(name15)
7574
+ );
7575
+ if (unknownToolNames.length > 0) {
7576
+ throw new Error(
7577
+ `[mcp-server-tester] toolOverrides variant "${variant.id}" references unknown tool(s): ` + unknownToolNames.join(", ")
7578
+ );
7579
+ }
7580
+ return tools.map((tool2) => {
7581
+ const override = variant.tools[tool2.name];
7582
+ if (!override) {
7583
+ return tool2;
7584
+ }
7585
+ return {
7586
+ ...tool2,
7587
+ ...override.description !== void 0 && {
7588
+ description: override.description
7589
+ },
7590
+ ...override.inputSchema !== void 0 && {
7591
+ inputSchema: override.inputSchema
7592
+ }
7593
+ };
7594
+ });
7595
+ },
7596
+ async callTool(name15, args) {
7597
+ return mcp.callTool(name15, args);
7598
+ }
7599
+ };
7600
+ }
7342
7601
  async function executeToolCall(evalCase, mcp) {
7343
7602
  const mode = evalCase.mode || "direct";
7344
7603
  try {
@@ -7523,9 +7782,12 @@ async function runExpectBlockValidations(expectBlock, response, config) {
7523
7782
  }
7524
7783
  return { expectations: results, toolPrecision, toolRecall };
7525
7784
  }
7526
- function buildRequest(evalCase) {
7785
+ function buildRequest(evalCase, toolOverrideVariantId) {
7527
7786
  const request = {};
7528
7787
  if (evalCase.description) request.description = evalCase.description;
7788
+ if (toolOverrideVariantId !== void 0) {
7789
+ request.toolOverrideVariantId = toolOverrideVariantId;
7790
+ }
7529
7791
  if (evalCase.mode === "mcp_host") {
7530
7792
  if (evalCase.scenario) request.scenario = evalCase.scenario;
7531
7793
  if (evalCase.mcpHostConfig) {
@@ -7590,7 +7852,7 @@ async function runSingleIteration(evalCase, context, options) {
7590
7852
  toolName: evalCase.scenario != null ? "mcp_host" : evalCase.toolName ?? "unknown",
7591
7853
  source: "eval",
7592
7854
  pass: didCasePass(error, expectationResults),
7593
- request: buildRequest(evalCase),
7855
+ request: buildRequest(evalCase, options.toolOverrideVariantId),
7594
7856
  response,
7595
7857
  error,
7596
7858
  expectations: expectationResults,
@@ -7669,7 +7931,8 @@ async function runEvalCase(evalCase, context, options = {}) {
7669
7931
  authType: context.mcp.authType,
7670
7932
  project: context.mcp.project,
7671
7933
  durationMs: 0,
7672
- tags: evalCase.tags
7934
+ tags: evalCase.tags,
7935
+ request: buildRequest(evalCase, options.toolOverrideVariantId)
7673
7936
  };
7674
7937
  const totalHostUsage = iterationResults.reduce(
7675
7938
  (acc, r) => sumUsage(acc, r.hostUsage),
@@ -7728,11 +7991,15 @@ async function runEvalDataset(options, context) {
7728
7991
  filterTags,
7729
7992
  saveResultsTo,
7730
7993
  omitResponsesFromBaseline = true,
7994
+ redactStoredResponses,
7995
+ resultStore,
7731
7996
  baselineResultsFrom,
7997
+ toolOverrides,
7732
7998
  mcpHostModel,
7733
7999
  judgeModel
7734
8000
  } = options;
7735
8001
  const startTime = Date.now();
8002
+ const effectiveContext = toolOverrides ? { ...context, mcp: createToolOverrideMCP(context.mcp, toolOverrides) } : context;
7736
8003
  const allSchemas = {
7737
8004
  ...dataset.schemas,
7738
8005
  ...schemas
@@ -7764,9 +8031,10 @@ async function runEvalDataset(options, context) {
7764
8031
  }
7765
8032
  }
7766
8033
  const effectiveCase = withIterations.judgeReps === void 0 && defaultJudgeReps !== void 0 ? { ...withIterations, judgeReps: defaultJudgeReps } : withIterations;
7767
- const result2 = await runEvalCase(effectiveCase, context, {
8034
+ const result2 = await runEvalCase(effectiveCase, effectiveContext, {
7768
8035
  datasetName: dataset.name,
7769
- schemas: allSchemas
8036
+ schemas: allSchemas,
8037
+ toolOverrideVariantId: toolOverrides?.id
7770
8038
  });
7771
8039
  if (onCaseComplete) {
7772
8040
  await onCaseComplete(result2);
@@ -7791,6 +8059,9 @@ async function runEvalDataset(options, context) {
7791
8059
  gitHash,
7792
8060
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7793
8061
  packageVersion: package_default.version,
8062
+ ...toolOverrides !== void 0 && {
8063
+ toolOverrideVariantId: toolOverrides.id
8064
+ },
7794
8065
  ...mcpHostModel !== void 0 && { mcpHostModel },
7795
8066
  ...judgeModel !== void 0 && { judgeModel }
7796
8067
  };
@@ -7809,7 +8080,7 @@ async function runEvalDataset(options, context) {
7809
8080
  };
7810
8081
  if (baselineResultsFrom) {
7811
8082
  try {
7812
- const baseline = await loadBaseline(baselineResultsFrom);
8083
+ const baseline = typeof baselineResultsFrom === "string" ? await loadBaseline(baselineResultsFrom) : await loadStoredBaseline(baselineResultsFrom, resultStore);
7813
8084
  const baselinePassRate = baseline.total > 0 ? baseline.passed / baseline.total : 0;
7814
8085
  const baselineMap = buildBaselinePassMap(baseline);
7815
8086
  const currentCaseIds = result.caseResults.map((cr) => cr.id);
@@ -7837,7 +8108,7 @@ async function runEvalDataset(options, context) {
7837
8108
  result.deltaPassRate = result.total > 0 ? result.passed / result.total - baselinePassRate : 0;
7838
8109
  } catch (err) {
7839
8110
  console.warn(
7840
- `[mcp-server-tester] Could not load baseline from ${baselineResultsFrom}: ${err instanceof Error ? err.message : String(err)}`
8111
+ `[mcp-server-tester] Could not load baseline from ${formatBaselineRef(baselineResultsFrom)}: ${err instanceof Error ? err.message : String(err)}`
7841
8112
  );
7842
8113
  }
7843
8114
  }
@@ -7852,9 +8123,26 @@ async function runEvalDataset(options, context) {
7852
8123
  result.datasetToolF1 = avgPrec + avgRecall > 0 ? 2 * avgPrec * avgRecall / (avgPrec + avgRecall) : 0;
7853
8124
  }
7854
8125
  if (saveResultsTo) {
7855
- await saveBaseline(result, saveResultsTo, {
7856
- omitResponses: omitResponsesFromBaseline
7857
- });
8126
+ if (typeof saveResultsTo === "string") {
8127
+ await saveBaseline(result, saveResultsTo, {
8128
+ omitResponses: omitResponsesFromBaseline
8129
+ });
8130
+ } else {
8131
+ await saveStoredEvalResult(result, saveResultsTo, {
8132
+ resultStore,
8133
+ omitResponses: redactStoredResponses ?? true,
8134
+ metadata: {
8135
+ datasetName: dataset.name,
8136
+ ...toolOverrides?.id !== void 0 && {
8137
+ toolOverrideVariantId: toolOverrides.id
8138
+ },
8139
+ ...mcpHostModel !== void 0 && { mcpHostModel },
8140
+ ...judgeModel !== void 0 && { judgeModel },
8141
+ ...gitHash !== void 0 && { gitHash },
8142
+ packageVersion: package_default.version
8143
+ }
8144
+ });
8145
+ }
7858
8146
  }
7859
8147
  if (context.testInfo) {
7860
8148
  await context.testInfo.attach("mcp-test-results", {
@@ -7868,6 +8156,50 @@ async function runEvalDataset(options, context) {
7868
8156
  }
7869
8157
  return result;
7870
8158
  }
8159
+ async function loadStoredBaseline(baselineResultsFrom, resultStore) {
8160
+ if (!resultStore) {
8161
+ throw new Error("resultStore is required for store-backed baselines");
8162
+ }
8163
+ const store = resolveEvalResultStore(resultStore);
8164
+ const artifact = baselineResultsFrom.ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8165
+ "eval-runner-result",
8166
+ baselineResultsFrom.ref.id
8167
+ );
8168
+ if (!artifact) {
8169
+ throw new Error("No latest eval run artifact found");
8170
+ }
8171
+ return artifact.data;
8172
+ }
8173
+ async function saveStoredEvalResult(result, saveResultsTo, options) {
8174
+ if (!options.resultStore) {
8175
+ throw new Error("resultStore is required for store-backed saves");
8176
+ }
8177
+ const store = resolveEvalResultStore(options.resultStore);
8178
+ const data = options.omitResponses ? omitResponsesFromResult(result) : result;
8179
+ const id = saveResultsTo.ref && saveResultsTo.ref !== "latest" ? saveResultsTo.ref.id : void 0;
8180
+ await store.saveArtifact(
8181
+ createStoredEvalArtifact({
8182
+ kind: "eval-runner-result",
8183
+ id,
8184
+ data,
8185
+ metadata: options.metadata
8186
+ })
8187
+ );
8188
+ }
8189
+ function omitResponsesFromResult(result) {
8190
+ return {
8191
+ ...result,
8192
+ caseResults: result.caseResults.map(
8193
+ ({ response: _response, ...rest }) => rest
8194
+ )
8195
+ };
8196
+ }
8197
+ function formatBaselineRef(baselineResultsFrom) {
8198
+ if (typeof baselineResultsFrom === "string") {
8199
+ return baselineResultsFrom;
8200
+ }
8201
+ return baselineResultsFrom.ref === "latest" ? "resultStore latest" : `resultStore ${baselineResultsFrom.ref.id}`;
8202
+ }
7871
8203
 
7872
8204
  // src/evals/serverComparison.ts
7873
8205
  async function runServerComparison(options, contextA, contextB) {
@@ -7909,7 +8241,7 @@ async function runServerComparison(options, contextA, contextB) {
7909
8241
  }
7910
8242
  const total = cases.length;
7911
8243
  const decidedCases = aWins + bWins + ties;
7912
- return {
8244
+ const comparison = {
7913
8245
  dataset: options.dataset.name,
7914
8246
  total,
7915
8247
  aWins,
@@ -7926,6 +8258,326 @@ async function runServerComparison(options, contextA, contextB) {
7926
8258
  serverBResult: resultB,
7927
8259
  durationMs: Date.now() - startTime
7928
8260
  };
8261
+ if (options.comparisonStore) {
8262
+ await saveServerComparison({
8263
+ store: options.comparisonStore,
8264
+ comparison,
8265
+ id: options.comparisonId,
8266
+ metadata: {
8267
+ datasetName: options.dataset.name,
8268
+ ...options.comparisonMetadata ?? {}
8269
+ },
8270
+ redactStoredResponses: options.redactStoredResponses
8271
+ });
8272
+ }
8273
+ return comparison;
8274
+ }
8275
+ async function saveServerComparison(options) {
8276
+ const store = resolveEvalResultStore(options.store);
8277
+ const data = options.redactStoredResponses ? redactResponses(options.comparison) : options.comparison;
8278
+ const artifact = createStoredEvalArtifact({
8279
+ kind: "server-comparison",
8280
+ id: options.id,
8281
+ data,
8282
+ metadata: {
8283
+ datasetName: options.comparison.dataset,
8284
+ ...options.metadata ?? {}
8285
+ }
8286
+ });
8287
+ await store.saveArtifact(artifact);
8288
+ return artifact;
8289
+ }
8290
+ function redactResponses(value) {
8291
+ return JSON.parse(
8292
+ JSON.stringify(
8293
+ value,
8294
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8295
+ )
8296
+ );
8297
+ }
8298
+
8299
+ // src/evals/evalRunComparison.ts
8300
+ function compareEvalRuns(options) {
8301
+ const { baseline, candidate, labels } = options;
8302
+ const candidateMap = new Map(
8303
+ candidate.caseResults.map((result) => [result.id, result])
8304
+ );
8305
+ const cases = [];
8306
+ const seenIds = /* @__PURE__ */ new Set();
8307
+ for (const baselineCase of baseline.caseResults) {
8308
+ seenIds.add(baselineCase.id);
8309
+ const candidateCase = candidateMap.get(baselineCase.id);
8310
+ if (!candidateCase) {
8311
+ cases.push({
8312
+ id: baselineCase.id,
8313
+ outcome: "MISSING_FROM_CANDIDATE",
8314
+ baseline: baselineCase
8315
+ });
8316
+ continue;
8317
+ }
8318
+ cases.push({
8319
+ id: baselineCase.id,
8320
+ outcome: compareCaseOutcome(baselineCase.pass, candidateCase.pass),
8321
+ baseline: baselineCase,
8322
+ candidate: candidateCase
8323
+ });
8324
+ }
8325
+ for (const candidateCase of candidate.caseResults) {
8326
+ if (seenIds.has(candidateCase.id)) {
8327
+ continue;
8328
+ }
8329
+ cases.push({
8330
+ id: candidateCase.id,
8331
+ outcome: "MISSING_FROM_BASELINE",
8332
+ candidate: candidateCase
8333
+ });
8334
+ }
8335
+ const baselinePassRate = passRate(baseline);
8336
+ const candidatePassRate = passRate(candidate);
8337
+ return {
8338
+ baselineLabel: labels?.baseline ?? "baseline",
8339
+ candidateLabel: labels?.candidate ?? candidate.metadata?.toolOverrideVariantId ?? "candidate",
8340
+ baselinePassRate,
8341
+ candidatePassRate,
8342
+ deltaPassRate: candidatePassRate - baselinePassRate,
8343
+ ...metricDelta(
8344
+ "ToolPrecision",
8345
+ baseline.datasetToolPrecision,
8346
+ candidate.datasetToolPrecision
8347
+ ),
8348
+ ...metricDelta(
8349
+ "ToolRecall",
8350
+ baseline.datasetToolRecall,
8351
+ candidate.datasetToolRecall
8352
+ ),
8353
+ ...metricDelta("ToolF1", baseline.datasetToolF1, candidate.datasetToolF1),
8354
+ cases,
8355
+ improvedCases: cases.filter((c) => c.outcome === "IMPROVED"),
8356
+ regressedCases: cases.filter((c) => c.outcome === "REGRESSED"),
8357
+ unchangedPasses: cases.filter((c) => c.outcome === "UNCHANGED_PASS"),
8358
+ unchangedFailures: cases.filter((c) => c.outcome === "UNCHANGED_FAIL"),
8359
+ missingFromBaseline: cases.filter(
8360
+ (c) => c.outcome === "MISSING_FROM_BASELINE"
8361
+ ),
8362
+ missingFromCandidate: cases.filter(
8363
+ (c) => c.outcome === "MISSING_FROM_CANDIDATE"
8364
+ )
8365
+ };
8366
+ }
8367
+ async function loadStoredEvalRunnerResult(storeLike, ref) {
8368
+ const store = resolveEvalResultStore(storeLike);
8369
+ const artifact = ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8370
+ "eval-runner-result",
8371
+ ref.id
8372
+ );
8373
+ if (!artifact) {
8374
+ throw new Error("No latest eval run artifact found");
8375
+ }
8376
+ return artifact;
8377
+ }
8378
+ async function saveEvalRunComparison(options) {
8379
+ const store = resolveEvalResultStore(options.store);
8380
+ const data = options.redactStoredResponses ? redactResponses2(options.comparison) : options.comparison;
8381
+ const artifact = createStoredEvalArtifact({
8382
+ kind: "eval-run-comparison",
8383
+ id: options.id,
8384
+ data,
8385
+ metadata: {
8386
+ labels: {
8387
+ baseline: options.comparison.baselineLabel,
8388
+ candidate: options.comparison.candidateLabel
8389
+ },
8390
+ ...options.metadata ?? {}
8391
+ }
8392
+ });
8393
+ await store.saveArtifact(artifact);
8394
+ return artifact;
8395
+ }
8396
+ function compareCaseOutcome(baselinePass, candidatePass) {
8397
+ if (!baselinePass && candidatePass) return "IMPROVED";
8398
+ if (baselinePass && !candidatePass) return "REGRESSED";
8399
+ return baselinePass ? "UNCHANGED_PASS" : "UNCHANGED_FAIL";
8400
+ }
8401
+ function passRate(result) {
8402
+ return result.total > 0 ? result.passed / result.total : 0;
8403
+ }
8404
+ function metricDelta(name15, baselineValue, candidateValue) {
8405
+ const result = {};
8406
+ if (baselineValue !== void 0) {
8407
+ result[`baseline${name15}`] = baselineValue;
8408
+ }
8409
+ if (candidateValue !== void 0) {
8410
+ result[`candidate${name15}`] = candidateValue;
8411
+ }
8412
+ if (baselineValue !== void 0 && candidateValue !== void 0) {
8413
+ result[`delta${name15}`] = candidateValue - baselineValue;
8414
+ }
8415
+ return result;
8416
+ }
8417
+ function redactResponses2(value) {
8418
+ return JSON.parse(
8419
+ JSON.stringify(
8420
+ value,
8421
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8422
+ )
8423
+ );
8424
+ }
8425
+
8426
+ // src/evals/variantExperiment.ts
8427
+ async function runVariantExperiment(options, context) {
8428
+ const metric = options.metric ?? "passRate";
8429
+ const maxRounds = options.maxRounds ?? 1;
8430
+ const minImprovement = options.minImprovement ?? 0;
8431
+ const allowRegressions = options.allowRegressions ?? false;
8432
+ const baseline = await runEvalDataset(
8433
+ buildRunOptions(options, void 0),
8434
+ context
8435
+ );
8436
+ const baselineValue = readMetric(baseline, metric);
8437
+ if (baselineValue === void 0) {
8438
+ throw new Error(
8439
+ `Metric '${metric}' is unavailable: the dataset produced no tool precision/recall data. Add mcp_host cases with toolsTriggered expectations, or use metric 'passRate'.`
8440
+ );
8441
+ }
8442
+ const rounds = [];
8443
+ let bestSoFar;
8444
+ let bestAttempted;
8445
+ let reason = "max-rounds";
8446
+ for (let round = 0; round < maxRounds; round++) {
8447
+ const variants = await gatherVariants(options, {
8448
+ round,
8449
+ baseline,
8450
+ metric,
8451
+ history: rounds,
8452
+ bestSoFar
8453
+ });
8454
+ if (variants.length === 0) {
8455
+ reason = round === 0 ? "no-variants" : "no-improvement";
8456
+ break;
8457
+ }
8458
+ const candidates = [];
8459
+ for (const variant of variants) {
8460
+ const candidate = await scoreVariant(
8461
+ options,
8462
+ context,
8463
+ baseline,
8464
+ baselineValue,
8465
+ metric,
8466
+ allowRegressions,
8467
+ variant
8468
+ );
8469
+ candidates.push(candidate);
8470
+ bestAttempted = pickBetter(bestAttempted, candidate, true);
8471
+ }
8472
+ const roundBest = candidates.reduce(
8473
+ (best, candidate) => pickBetter(best, candidate, false),
8474
+ void 0
8475
+ );
8476
+ rounds.push({ round, candidates, best: roundBest });
8477
+ if (roundBest) {
8478
+ const improvement = roundBest.metricValue - (bestSoFar?.metricValue ?? baselineValue);
8479
+ bestSoFar = pickBetter(bestSoFar, roundBest, false);
8480
+ if (improvement < minImprovement) {
8481
+ reason = "no-improvement";
8482
+ break;
8483
+ }
8484
+ }
8485
+ }
8486
+ const winner = bestSoFar;
8487
+ const proposalSource = winner ?? bestAttempted;
8488
+ const proposal = proposalSource ? buildProposal(metric, baselineValue, proposalSource, winner !== void 0) : void 0;
8489
+ return {
8490
+ metric,
8491
+ baseline,
8492
+ rounds,
8493
+ winner,
8494
+ proposal,
8495
+ converged: true,
8496
+ reason
8497
+ };
8498
+ }
8499
+ async function gatherVariants(options, context) {
8500
+ if (context.round === 0 && options.variants && options.variants.length > 0) {
8501
+ return options.variants;
8502
+ }
8503
+ if (options.proposeVariants) {
8504
+ return options.proposeVariants(context);
8505
+ }
8506
+ return [];
8507
+ }
8508
+ async function scoreVariant(options, context, baseline, baselineValue, metric, allowRegressions, variant) {
8509
+ const result = await runEvalDataset(
8510
+ buildRunOptions(options, variant),
8511
+ context
8512
+ );
8513
+ const comparison = compareEvalRuns({
8514
+ baseline,
8515
+ candidate: result,
8516
+ labels: { candidate: variant.id }
8517
+ });
8518
+ const metricValue = readMetric(result, metric) ?? baselineValue;
8519
+ const disqualified = !allowRegressions && comparison.regressedCases.length > 0;
8520
+ return {
8521
+ variant,
8522
+ result,
8523
+ comparison,
8524
+ metricValue,
8525
+ metricDelta: metricValue - baselineValue,
8526
+ disqualified
8527
+ };
8528
+ }
8529
+ function pickBetter(incumbent, challenger, includeDisqualified) {
8530
+ if (!includeDisqualified && challenger.disqualified) {
8531
+ return incumbent;
8532
+ }
8533
+ if (!incumbent) {
8534
+ return challenger;
8535
+ }
8536
+ return challenger.metricValue > incumbent.metricValue ? challenger : incumbent;
8537
+ }
8538
+ function buildProposal(metric, baselineValue, source, isWinner) {
8539
+ let recommendation;
8540
+ if (isWinner) {
8541
+ recommendation = source.metricDelta > 0 ? "apply" : "inconclusive";
8542
+ } else {
8543
+ recommendation = source.disqualified ? "reject" : "inconclusive";
8544
+ }
8545
+ return {
8546
+ variantId: source.variant.id,
8547
+ metric,
8548
+ baselineValue,
8549
+ candidateValue: source.metricValue,
8550
+ delta: source.metricDelta,
8551
+ toolChanges: source.variant.tools,
8552
+ improvedCaseIds: source.comparison.improvedCases.map((c) => c.id),
8553
+ regressedCaseIds: source.comparison.regressedCases.map((c) => c.id),
8554
+ recommendation
8555
+ };
8556
+ }
8557
+ function readMetric(result, metric) {
8558
+ switch (metric) {
8559
+ case "passRate":
8560
+ return result.total > 0 ? result.passed / result.total : 0;
8561
+ case "toolF1":
8562
+ return result.datasetToolF1;
8563
+ case "toolPrecision":
8564
+ return result.datasetToolPrecision;
8565
+ case "toolRecall":
8566
+ return result.datasetToolRecall;
8567
+ }
8568
+ }
8569
+ function buildRunOptions(options, toolOverrides) {
8570
+ return {
8571
+ dataset: options.dataset,
8572
+ toolOverrides,
8573
+ defaultLlmIterations: options.defaultLlmIterations,
8574
+ defaultJudgeReps: options.defaultJudgeReps,
8575
+ concurrency: options.concurrency,
8576
+ filterTags: options.filterTags,
8577
+ schemas: options.schemas,
8578
+ mcpHostModel: options.mcpHostModel,
8579
+ judgeModel: options.judgeModel
8580
+ };
7929
8581
  }
7930
8582
 
7931
8583
  // src/spec/conformanceChecks.ts
@@ -8105,15 +8757,22 @@ exports.DiscoveryError = DiscoveryError;
8105
8757
  exports.ENV_VAR_NAMES = ENV_VAR_NAMES;
8106
8758
  exports.EvalCaseSchema = EvalCaseSchema;
8107
8759
  exports.EvalDatasetSchema = EvalDatasetSchema;
8760
+ exports.FileEvalResultStore = FileEvalResultStore;
8761
+ exports.GCSEvalResultStore = GCSEvalResultStore;
8108
8762
  exports.MCPConfigSchema = MCPConfigSchema;
8109
8763
  exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
8110
8764
  exports.SnapshotSanitizers = SnapshotSanitizers;
8111
8765
  exports.clearJudgeRegistry = clearJudgeRegistry;
8112
8766
  exports.closeMCPClient = closeMCPClient;
8767
+ exports.compareEvalRuns = compareEvalRuns;
8768
+ exports.createDefaultArtifactId = createDefaultArtifactId;
8769
+ exports.createEvalResultStore = createEvalResultStore;
8113
8770
  exports.createJudge = createJudge;
8114
8771
  exports.createMCPClientForConfig = createMCPClientForConfig;
8115
8772
  exports.createMCPFixture = createMCPFixture;
8773
+ exports.createStoredEvalArtifact = createStoredEvalArtifact;
8116
8774
  exports.createTokenAuthHeaders = createTokenAuthHeaders;
8775
+ exports.defaultEnvironmentMetadata = defaultEnvironmentMetadata;
8117
8776
  exports.discoverAuthorizationServer = discoverAuthorizationServer;
8118
8777
  exports.discoverProtectedResource = discoverProtectedResource;
8119
8778
  exports.expect = expect;
@@ -8124,6 +8783,7 @@ exports.getResponseSizeBytes = getResponseSizeBytes;
8124
8783
  exports.hasValidTokens = hasValidTokens;
8125
8784
  exports.injectTokens = injectTokens;
8126
8785
  exports.isBuiltInRubric = isBuiltInRubric;
8786
+ exports.isEvalResultStore = isEvalResultStore;
8127
8787
  exports.isHttpConfig = isHttpConfig;
8128
8788
  exports.isProviderAvailable = isProviderAvailable;
8129
8789
  exports.isStdioConfig = isStdioConfig;
@@ -8132,6 +8792,7 @@ exports.isTokenExpiringSoon = isTokenExpiringSoon;
8132
8792
  exports.loadBaseline = loadBaseline;
8133
8793
  exports.loadEvalDataset = loadEvalDataset;
8134
8794
  exports.loadEvalDatasetFromObject = loadEvalDatasetFromObject;
8795
+ exports.loadStoredEvalRunnerResult = loadStoredEvalRunnerResult;
8135
8796
  exports.loadTokens = loadTokens;
8136
8797
  exports.loadTokensFromEnv = loadTokensFromEnv;
8137
8798
  exports.mcpAuthTest = test2;
@@ -8142,12 +8803,16 @@ exports.performOAuthSetup = performOAuthSetup;
8142
8803
  exports.performOAuthSetupIfNeeded = performOAuthSetupIfNeeded;
8143
8804
  exports.refreshAccessToken = refreshAccessToken;
8144
8805
  exports.registerJudge = registerJudge;
8806
+ exports.resolveEvalResultStore = resolveEvalResultStore;
8145
8807
  exports.resolveRubric = resolveRubric;
8146
8808
  exports.runConformanceChecks = runConformanceChecks;
8147
8809
  exports.runEvalCase = runEvalCase;
8148
8810
  exports.runEvalDataset = runEvalDataset;
8149
8811
  exports.runServerComparison = runServerComparison;
8812
+ exports.runVariantExperiment = runVariantExperiment;
8150
8813
  exports.saveBaseline = saveBaseline;
8814
+ exports.saveEvalRunComparison = saveEvalRunComparison;
8815
+ exports.saveServerComparison = saveServerComparison;
8151
8816
  exports.simulateMCPHost = simulateMCPHost;
8152
8817
  exports.test = test;
8153
8818
  exports.validateAccessToken = validateAccessToken;