@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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs/promises';
2
- import { readFile, mkdir, writeFile } from 'fs/promises';
2
+ import { readFile, mkdir, writeFile, readdir } from 'fs/promises';
3
3
  import * as path2 from 'path';
4
- import { dirname } from 'path';
4
+ import { dirname, join } from 'path';
5
5
  import * as z4 from 'zod/v4';
6
6
  import { ZodFirstPartyTypeKind } from 'zod/v3';
7
7
  import { z } from 'zod';
@@ -3099,6 +3099,20 @@ var init_dist3 = __esm({
3099
3099
  };
3100
3100
  }
3101
3101
  });
3102
+
3103
+ // src/assertions/validators/types.ts
3104
+ var SnapshotSanitizers = {
3105
+ /** Replaces Unix timestamps (seconds and milliseconds) with a stable placeholder */
3106
+ TIMESTAMP: "timestamp",
3107
+ /** Replaces UUID v1-v5 strings with a stable placeholder */
3108
+ UUID: "uuid",
3109
+ /** Replaces ISO 8601 date/datetime strings with a stable placeholder */
3110
+ ISO_DATE: "iso-date",
3111
+ /** Replaces MongoDB ObjectId strings with a stable placeholder */
3112
+ OBJECT_ID: "objectId",
3113
+ /** Replaces JWT tokens with a stable placeholder */
3114
+ JWT: "jwt"
3115
+ };
3102
3116
  var MCPHostCapabilitiesSchema = z.object({
3103
3117
  sampling: z.record(z.string(), z.unknown()).optional(),
3104
3118
  roots: z.object({
@@ -4384,7 +4398,7 @@ function escapeHtml(text) {
4384
4398
 
4385
4399
  // package.json
4386
4400
  var package_default = {
4387
- version: "1.0.1"};
4401
+ version: "1.1.0"};
4388
4402
 
4389
4403
  // src/mcp/clientFactory.ts
4390
4404
  function getRetryAfterDelayMs(err) {
@@ -5908,20 +5922,6 @@ async function validateJudge(response, config) {
5908
5922
  }
5909
5923
  }
5910
5924
 
5911
- // src/assertions/validators/types.ts
5912
- var SnapshotSanitizers = {
5913
- /** Replaces Unix timestamps (seconds and milliseconds) with a stable placeholder */
5914
- TIMESTAMP: "timestamp",
5915
- /** Replaces UUID v1-v5 strings with a stable placeholder */
5916
- UUID: "uuid",
5917
- /** Replaces ISO 8601 date/datetime strings with a stable placeholder */
5918
- ISO_DATE: "iso-date",
5919
- /** Replaces MongoDB ObjectId strings with a stable placeholder */
5920
- OBJECT_ID: "objectId",
5921
- /** Replaces JWT tokens with a stable placeholder */
5922
- JWT: "jwt"
5923
- };
5924
-
5925
5925
  // src/mcp/fixtures/mcpFixture.ts
5926
5926
  var DEFAULT_CALL_TIMEOUT_MS = 3e4;
5927
5927
  function withCallTimeout(promise, ms, opName) {
@@ -6967,6 +6967,7 @@ function parseStreamJson(stdout) {
6967
6967
  const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
6968
6968
  const toolCalls = [];
6969
6969
  const textParts = [];
6970
+ let usage;
6970
6971
  const conversationHistory = [];
6971
6972
  for (const line of lines) {
6972
6973
  let event;
@@ -6999,16 +7000,28 @@ function parseStreamJson(stdout) {
6999
7000
  }
7000
7001
  }
7001
7002
  }
7002
- if (event.type === "result" && typeof event.result === "string") {
7003
- if (textParts.length === 0) {
7003
+ if (event.type === "result") {
7004
+ if (typeof event.result === "string" && textParts.length === 0) {
7004
7005
  textParts.push(event.result);
7005
7006
  }
7007
+ if (event.usage) {
7008
+ usage = {
7009
+ inputTokens: event.usage.input_tokens ?? 0,
7010
+ outputTokens: event.usage.output_tokens ?? 0,
7011
+ totalCostUsd: event.total_cost_usd ?? 0,
7012
+ durationMs: event.duration_ms ?? 0,
7013
+ durationApiMs: event.duration_api_ms,
7014
+ cacheReadInputTokens: event.usage.cache_read_input_tokens,
7015
+ cacheCreationInputTokens: event.usage.cache_creation_input_tokens
7016
+ };
7017
+ }
7006
7018
  }
7007
7019
  if (event.type === "result" && event.is_error === true) {
7008
7020
  return {
7009
7021
  success: false,
7010
7022
  toolCalls,
7011
- error: typeof event.result === "string" ? event.result : "CLI host reported an error"
7023
+ error: typeof event.result === "string" ? event.result : "CLI host reported an error",
7024
+ usage
7012
7025
  };
7013
7026
  }
7014
7027
  }
@@ -7020,7 +7033,8 @@ function parseStreamJson(stdout) {
7020
7033
  success: true,
7021
7034
  toolCalls,
7022
7035
  response: response || void 0,
7023
- conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0
7036
+ conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0,
7037
+ usage
7024
7038
  };
7025
7039
  }
7026
7040
  function createJsonParser(paths) {
@@ -7266,6 +7280,216 @@ function buildBaselinePassMap(baseline) {
7266
7280
  }
7267
7281
  return map;
7268
7282
  }
7283
+ var KIND_DIRS = {
7284
+ "eval-runner-result": "eval-runs",
7285
+ "reporter-run": "reporter-runs",
7286
+ "eval-run-comparison": "comparisons/eval-runs",
7287
+ "server-comparison": "comparisons/servers"
7288
+ };
7289
+ function createEvalResultStore(config) {
7290
+ if (config.provider === "file") {
7291
+ return new FileEvalResultStore(config);
7292
+ }
7293
+ return new GCSEvalResultStore(config);
7294
+ }
7295
+ function resolveEvalResultStore(store) {
7296
+ return isEvalResultStore(store) ? store : createEvalResultStore(store);
7297
+ }
7298
+ function isEvalResultStore(value) {
7299
+ return typeof value === "object" && value !== null && "saveArtifact" in value && "loadArtifact" in value && "loadLatestArtifact" in value && "listArtifacts" in value;
7300
+ }
7301
+ function createStoredEvalArtifact(options) {
7302
+ const createdAt = options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
7303
+ return {
7304
+ schemaVersion: 1,
7305
+ kind: options.kind,
7306
+ id: options.id ?? createDefaultArtifactId(createdAt),
7307
+ createdAt,
7308
+ metadata: {
7309
+ ...defaultEnvironmentMetadata(),
7310
+ ...options.metadata ?? {}
7311
+ },
7312
+ data: options.data
7313
+ };
7314
+ }
7315
+ function createDefaultArtifactId(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
7316
+ const safeTimestamp = timestamp.replace(/[:.]/g, "-");
7317
+ const runNumber = process.env.GITHUB_RUN_NUMBER;
7318
+ const sha = process.env.GITHUB_SHA?.slice(0, 12);
7319
+ const suffix = runNumber ?? sha;
7320
+ return suffix ? `${safeTimestamp}-${suffix}` : safeTimestamp;
7321
+ }
7322
+ function defaultEnvironmentMetadata() {
7323
+ return {
7324
+ ...process.env.GITHUB_SHA !== void 0 && {
7325
+ gitHash: process.env.GITHUB_SHA
7326
+ },
7327
+ ...process.env.GITHUB_REF_NAME !== void 0 && {
7328
+ branch: process.env.GITHUB_REF_NAME
7329
+ },
7330
+ ...process.env.GITHUB_RUN_NUMBER !== void 0 && {
7331
+ runNumber: process.env.GITHUB_RUN_NUMBER
7332
+ },
7333
+ ...process.env.GITHUB_EVENT_NAME !== void 0 && {
7334
+ trigger: process.env.GITHUB_EVENT_NAME
7335
+ }
7336
+ };
7337
+ }
7338
+ var FileEvalResultStore = class {
7339
+ dir;
7340
+ constructor(config) {
7341
+ this.dir = config.dir;
7342
+ }
7343
+ async saveArtifact(artifact) {
7344
+ const artifactDir = join(this.dir, KIND_DIRS[artifact.kind]);
7345
+ await mkdir(artifactDir, { recursive: true });
7346
+ const serialized = JSON.stringify(artifact, null, 2);
7347
+ await writeFile(
7348
+ join(artifactDir, `${artifact.id}.json`),
7349
+ serialized,
7350
+ "utf8"
7351
+ );
7352
+ await writeFile(join(artifactDir, "latest.json"), serialized, "utf8");
7353
+ }
7354
+ async loadArtifact(kind, id) {
7355
+ const raw = await readFile(
7356
+ join(this.dir, KIND_DIRS[kind], `${id}.json`),
7357
+ "utf8"
7358
+ );
7359
+ return JSON.parse(raw);
7360
+ }
7361
+ async loadLatestArtifact(kind) {
7362
+ try {
7363
+ const raw = await readFile(
7364
+ join(this.dir, KIND_DIRS[kind], "latest.json"),
7365
+ "utf8"
7366
+ );
7367
+ return JSON.parse(raw);
7368
+ } catch (error) {
7369
+ if (isMissingFileError(error)) {
7370
+ return null;
7371
+ }
7372
+ throw error;
7373
+ }
7374
+ }
7375
+ async listArtifacts(kind, options = {}) {
7376
+ let files;
7377
+ try {
7378
+ files = await readdir(join(this.dir, KIND_DIRS[kind]));
7379
+ } catch (error) {
7380
+ if (isMissingFileError(error)) {
7381
+ return [];
7382
+ }
7383
+ throw error;
7384
+ }
7385
+ const summaries = await Promise.all(
7386
+ files.filter((f) => f.endsWith(".json") && f !== "latest.json").map(async (file) => {
7387
+ const raw = await readFile(
7388
+ join(this.dir, KIND_DIRS[kind], file),
7389
+ "utf8"
7390
+ );
7391
+ return toSummary(JSON.parse(raw));
7392
+ })
7393
+ );
7394
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7395
+ }
7396
+ };
7397
+ var GCSEvalResultStore = class {
7398
+ bucketName;
7399
+ prefix;
7400
+ storage;
7401
+ constructor(config) {
7402
+ this.bucketName = config.bucket;
7403
+ this.prefix = trimSlashes(config.prefix ?? "");
7404
+ }
7405
+ async saveArtifact(artifact) {
7406
+ const bucket = await this.getBucket();
7407
+ const serialized = JSON.stringify(artifact, null, 2);
7408
+ await bucket.file(
7409
+ this.objectPath(
7410
+ artifact.kind,
7411
+ `${encodeURIComponent(artifact.id)}.json`
7412
+ )
7413
+ ).save(serialized, {
7414
+ contentType: "application/json",
7415
+ resumable: false,
7416
+ validation: false
7417
+ });
7418
+ await bucket.file(this.objectPath(artifact.kind, "latest.json")).save(serialized, {
7419
+ contentType: "application/json",
7420
+ resumable: false,
7421
+ validation: false
7422
+ });
7423
+ }
7424
+ async loadArtifact(kind, id) {
7425
+ const bucket = await this.getBucket();
7426
+ const file = bucket.file(
7427
+ this.objectPath(kind, `${encodeURIComponent(id)}.json`)
7428
+ );
7429
+ const [buffer] = await file.download();
7430
+ return JSON.parse(buffer.toString("utf8"));
7431
+ }
7432
+ async loadLatestArtifact(kind) {
7433
+ const bucket = await this.getBucket();
7434
+ const file = bucket.file(this.objectPath(kind, "latest.json"));
7435
+ const [exists] = await file.exists();
7436
+ if (!exists) return null;
7437
+ const [buffer] = await file.download();
7438
+ return JSON.parse(buffer.toString("utf8"));
7439
+ }
7440
+ async listArtifacts(kind, options = {}) {
7441
+ const bucket = await this.getBucket();
7442
+ const [files] = await bucket.getFiles({
7443
+ prefix: this.objectPath(kind, "")
7444
+ });
7445
+ const summaries = await Promise.all(
7446
+ files.filter(
7447
+ (f) => f.name.endsWith(".json") && !f.name.endsWith("/latest.json")
7448
+ ).map(async (file) => {
7449
+ const [buffer] = await file.download();
7450
+ return toSummary(
7451
+ JSON.parse(buffer.toString("utf8"))
7452
+ );
7453
+ })
7454
+ );
7455
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7456
+ }
7457
+ async getBucket() {
7458
+ if (!this.storage) {
7459
+ const moduleName = "@google-cloud/storage";
7460
+ let Storage;
7461
+ try {
7462
+ const mod = await import(moduleName);
7463
+ Storage = mod.Storage;
7464
+ } catch (error) {
7465
+ throw new Error(
7466
+ `GCS result storage requires the optional \`@google-cloud/storage\` package. Install it and authenticate with Application Default Credentials via GOOGLE_APPLICATION_CREDENTIALS.
7467
+ Original error: ${error instanceof Error ? error.message : String(error)}`
7468
+ );
7469
+ }
7470
+ this.storage = new Storage();
7471
+ }
7472
+ return this.storage.bucket(this.bucketName);
7473
+ }
7474
+ objectPath(kind, filename) {
7475
+ const parts = [this.prefix, KIND_DIRS[kind], filename].filter(Boolean);
7476
+ return parts.join("/");
7477
+ }
7478
+ };
7479
+ function toSummary(artifact) {
7480
+ return {
7481
+ kind: artifact.kind,
7482
+ id: artifact.id,
7483
+ createdAt: artifact.createdAt,
7484
+ metadata: artifact.metadata
7485
+ };
7486
+ }
7487
+ function trimSlashes(value) {
7488
+ return value.replace(/^\/+|\/+$/g, "");
7489
+ }
7490
+ function isMissingFileError(error) {
7491
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
7492
+ }
7269
7493
  var execFileAsync = promisify(execFile);
7270
7494
  async function execFileNoThrow(file, args) {
7271
7495
  try {
@@ -7312,6 +7536,41 @@ function sumUsage(a, b) {
7312
7536
  }
7313
7537
 
7314
7538
  // src/evals/evalRunner.ts
7539
+ function createToolOverrideMCP(mcp, variant) {
7540
+ return {
7541
+ ...mcp,
7542
+ async listTools() {
7543
+ const tools = await mcp.listTools();
7544
+ const knownToolNames = new Set(tools.map((tool2) => tool2.name));
7545
+ const unknownToolNames = Object.keys(variant.tools).filter(
7546
+ (name15) => !knownToolNames.has(name15)
7547
+ );
7548
+ if (unknownToolNames.length > 0) {
7549
+ throw new Error(
7550
+ `[mcp-server-tester] toolOverrides variant "${variant.id}" references unknown tool(s): ` + unknownToolNames.join(", ")
7551
+ );
7552
+ }
7553
+ return tools.map((tool2) => {
7554
+ const override = variant.tools[tool2.name];
7555
+ if (!override) {
7556
+ return tool2;
7557
+ }
7558
+ return {
7559
+ ...tool2,
7560
+ ...override.description !== void 0 && {
7561
+ description: override.description
7562
+ },
7563
+ ...override.inputSchema !== void 0 && {
7564
+ inputSchema: override.inputSchema
7565
+ }
7566
+ };
7567
+ });
7568
+ },
7569
+ async callTool(name15, args) {
7570
+ return mcp.callTool(name15, args);
7571
+ }
7572
+ };
7573
+ }
7315
7574
  async function executeToolCall(evalCase, mcp) {
7316
7575
  const mode = evalCase.mode || "direct";
7317
7576
  try {
@@ -7496,9 +7755,12 @@ async function runExpectBlockValidations(expectBlock, response, config) {
7496
7755
  }
7497
7756
  return { expectations: results, toolPrecision, toolRecall };
7498
7757
  }
7499
- function buildRequest(evalCase) {
7758
+ function buildRequest(evalCase, toolOverrideVariantId) {
7500
7759
  const request = {};
7501
7760
  if (evalCase.description) request.description = evalCase.description;
7761
+ if (toolOverrideVariantId !== void 0) {
7762
+ request.toolOverrideVariantId = toolOverrideVariantId;
7763
+ }
7502
7764
  if (evalCase.mode === "mcp_host") {
7503
7765
  if (evalCase.scenario) request.scenario = evalCase.scenario;
7504
7766
  if (evalCase.mcpHostConfig) {
@@ -7563,7 +7825,7 @@ async function runSingleIteration(evalCase, context, options) {
7563
7825
  toolName: evalCase.scenario != null ? "mcp_host" : evalCase.toolName ?? "unknown",
7564
7826
  source: "eval",
7565
7827
  pass: didCasePass(error, expectationResults),
7566
- request: buildRequest(evalCase),
7828
+ request: buildRequest(evalCase, options.toolOverrideVariantId),
7567
7829
  response,
7568
7830
  error,
7569
7831
  expectations: expectationResults,
@@ -7642,7 +7904,8 @@ async function runEvalCase(evalCase, context, options = {}) {
7642
7904
  authType: context.mcp.authType,
7643
7905
  project: context.mcp.project,
7644
7906
  durationMs: 0,
7645
- tags: evalCase.tags
7907
+ tags: evalCase.tags,
7908
+ request: buildRequest(evalCase, options.toolOverrideVariantId)
7646
7909
  };
7647
7910
  const totalHostUsage = iterationResults.reduce(
7648
7911
  (acc, r) => sumUsage(acc, r.hostUsage),
@@ -7701,11 +7964,15 @@ async function runEvalDataset(options, context) {
7701
7964
  filterTags,
7702
7965
  saveResultsTo,
7703
7966
  omitResponsesFromBaseline = true,
7967
+ redactStoredResponses,
7968
+ resultStore,
7704
7969
  baselineResultsFrom,
7970
+ toolOverrides,
7705
7971
  mcpHostModel,
7706
7972
  judgeModel
7707
7973
  } = options;
7708
7974
  const startTime = Date.now();
7975
+ const effectiveContext = toolOverrides ? { ...context, mcp: createToolOverrideMCP(context.mcp, toolOverrides) } : context;
7709
7976
  const allSchemas = {
7710
7977
  ...dataset.schemas,
7711
7978
  ...schemas
@@ -7737,9 +8004,10 @@ async function runEvalDataset(options, context) {
7737
8004
  }
7738
8005
  }
7739
8006
  const effectiveCase = withIterations.judgeReps === void 0 && defaultJudgeReps !== void 0 ? { ...withIterations, judgeReps: defaultJudgeReps } : withIterations;
7740
- const result2 = await runEvalCase(effectiveCase, context, {
8007
+ const result2 = await runEvalCase(effectiveCase, effectiveContext, {
7741
8008
  datasetName: dataset.name,
7742
- schemas: allSchemas
8009
+ schemas: allSchemas,
8010
+ toolOverrideVariantId: toolOverrides?.id
7743
8011
  });
7744
8012
  if (onCaseComplete) {
7745
8013
  await onCaseComplete(result2);
@@ -7764,6 +8032,9 @@ async function runEvalDataset(options, context) {
7764
8032
  gitHash,
7765
8033
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7766
8034
  packageVersion: package_default.version,
8035
+ ...toolOverrides !== void 0 && {
8036
+ toolOverrideVariantId: toolOverrides.id
8037
+ },
7767
8038
  ...mcpHostModel !== void 0 && { mcpHostModel },
7768
8039
  ...judgeModel !== void 0 && { judgeModel }
7769
8040
  };
@@ -7782,7 +8053,7 @@ async function runEvalDataset(options, context) {
7782
8053
  };
7783
8054
  if (baselineResultsFrom) {
7784
8055
  try {
7785
- const baseline = await loadBaseline(baselineResultsFrom);
8056
+ const baseline = typeof baselineResultsFrom === "string" ? await loadBaseline(baselineResultsFrom) : await loadStoredBaseline(baselineResultsFrom, resultStore);
7786
8057
  const baselinePassRate = baseline.total > 0 ? baseline.passed / baseline.total : 0;
7787
8058
  const baselineMap = buildBaselinePassMap(baseline);
7788
8059
  const currentCaseIds = result.caseResults.map((cr) => cr.id);
@@ -7810,7 +8081,7 @@ async function runEvalDataset(options, context) {
7810
8081
  result.deltaPassRate = result.total > 0 ? result.passed / result.total - baselinePassRate : 0;
7811
8082
  } catch (err) {
7812
8083
  console.warn(
7813
- `[mcp-server-tester] Could not load baseline from ${baselineResultsFrom}: ${err instanceof Error ? err.message : String(err)}`
8084
+ `[mcp-server-tester] Could not load baseline from ${formatBaselineRef(baselineResultsFrom)}: ${err instanceof Error ? err.message : String(err)}`
7814
8085
  );
7815
8086
  }
7816
8087
  }
@@ -7825,9 +8096,26 @@ async function runEvalDataset(options, context) {
7825
8096
  result.datasetToolF1 = avgPrec + avgRecall > 0 ? 2 * avgPrec * avgRecall / (avgPrec + avgRecall) : 0;
7826
8097
  }
7827
8098
  if (saveResultsTo) {
7828
- await saveBaseline(result, saveResultsTo, {
7829
- omitResponses: omitResponsesFromBaseline
7830
- });
8099
+ if (typeof saveResultsTo === "string") {
8100
+ await saveBaseline(result, saveResultsTo, {
8101
+ omitResponses: omitResponsesFromBaseline
8102
+ });
8103
+ } else {
8104
+ await saveStoredEvalResult(result, saveResultsTo, {
8105
+ resultStore,
8106
+ omitResponses: redactStoredResponses ?? true,
8107
+ metadata: {
8108
+ datasetName: dataset.name,
8109
+ ...toolOverrides?.id !== void 0 && {
8110
+ toolOverrideVariantId: toolOverrides.id
8111
+ },
8112
+ ...mcpHostModel !== void 0 && { mcpHostModel },
8113
+ ...judgeModel !== void 0 && { judgeModel },
8114
+ ...gitHash !== void 0 && { gitHash },
8115
+ packageVersion: package_default.version
8116
+ }
8117
+ });
8118
+ }
7831
8119
  }
7832
8120
  if (context.testInfo) {
7833
8121
  await context.testInfo.attach("mcp-test-results", {
@@ -7841,6 +8129,50 @@ async function runEvalDataset(options, context) {
7841
8129
  }
7842
8130
  return result;
7843
8131
  }
8132
+ async function loadStoredBaseline(baselineResultsFrom, resultStore) {
8133
+ if (!resultStore) {
8134
+ throw new Error("resultStore is required for store-backed baselines");
8135
+ }
8136
+ const store = resolveEvalResultStore(resultStore);
8137
+ const artifact = baselineResultsFrom.ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8138
+ "eval-runner-result",
8139
+ baselineResultsFrom.ref.id
8140
+ );
8141
+ if (!artifact) {
8142
+ throw new Error("No latest eval run artifact found");
8143
+ }
8144
+ return artifact.data;
8145
+ }
8146
+ async function saveStoredEvalResult(result, saveResultsTo, options) {
8147
+ if (!options.resultStore) {
8148
+ throw new Error("resultStore is required for store-backed saves");
8149
+ }
8150
+ const store = resolveEvalResultStore(options.resultStore);
8151
+ const data = options.omitResponses ? omitResponsesFromResult(result) : result;
8152
+ const id = saveResultsTo.ref && saveResultsTo.ref !== "latest" ? saveResultsTo.ref.id : void 0;
8153
+ await store.saveArtifact(
8154
+ createStoredEvalArtifact({
8155
+ kind: "eval-runner-result",
8156
+ id,
8157
+ data,
8158
+ metadata: options.metadata
8159
+ })
8160
+ );
8161
+ }
8162
+ function omitResponsesFromResult(result) {
8163
+ return {
8164
+ ...result,
8165
+ caseResults: result.caseResults.map(
8166
+ ({ response: _response, ...rest }) => rest
8167
+ )
8168
+ };
8169
+ }
8170
+ function formatBaselineRef(baselineResultsFrom) {
8171
+ if (typeof baselineResultsFrom === "string") {
8172
+ return baselineResultsFrom;
8173
+ }
8174
+ return baselineResultsFrom.ref === "latest" ? "resultStore latest" : `resultStore ${baselineResultsFrom.ref.id}`;
8175
+ }
7844
8176
 
7845
8177
  // src/evals/serverComparison.ts
7846
8178
  async function runServerComparison(options, contextA, contextB) {
@@ -7882,7 +8214,7 @@ async function runServerComparison(options, contextA, contextB) {
7882
8214
  }
7883
8215
  const total = cases.length;
7884
8216
  const decidedCases = aWins + bWins + ties;
7885
- return {
8217
+ const comparison = {
7886
8218
  dataset: options.dataset.name,
7887
8219
  total,
7888
8220
  aWins,
@@ -7899,6 +8231,326 @@ async function runServerComparison(options, contextA, contextB) {
7899
8231
  serverBResult: resultB,
7900
8232
  durationMs: Date.now() - startTime
7901
8233
  };
8234
+ if (options.comparisonStore) {
8235
+ await saveServerComparison({
8236
+ store: options.comparisonStore,
8237
+ comparison,
8238
+ id: options.comparisonId,
8239
+ metadata: {
8240
+ datasetName: options.dataset.name,
8241
+ ...options.comparisonMetadata ?? {}
8242
+ },
8243
+ redactStoredResponses: options.redactStoredResponses
8244
+ });
8245
+ }
8246
+ return comparison;
8247
+ }
8248
+ async function saveServerComparison(options) {
8249
+ const store = resolveEvalResultStore(options.store);
8250
+ const data = options.redactStoredResponses ? redactResponses(options.comparison) : options.comparison;
8251
+ const artifact = createStoredEvalArtifact({
8252
+ kind: "server-comparison",
8253
+ id: options.id,
8254
+ data,
8255
+ metadata: {
8256
+ datasetName: options.comparison.dataset,
8257
+ ...options.metadata ?? {}
8258
+ }
8259
+ });
8260
+ await store.saveArtifact(artifact);
8261
+ return artifact;
8262
+ }
8263
+ function redactResponses(value) {
8264
+ return JSON.parse(
8265
+ JSON.stringify(
8266
+ value,
8267
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8268
+ )
8269
+ );
8270
+ }
8271
+
8272
+ // src/evals/evalRunComparison.ts
8273
+ function compareEvalRuns(options) {
8274
+ const { baseline, candidate, labels } = options;
8275
+ const candidateMap = new Map(
8276
+ candidate.caseResults.map((result) => [result.id, result])
8277
+ );
8278
+ const cases = [];
8279
+ const seenIds = /* @__PURE__ */ new Set();
8280
+ for (const baselineCase of baseline.caseResults) {
8281
+ seenIds.add(baselineCase.id);
8282
+ const candidateCase = candidateMap.get(baselineCase.id);
8283
+ if (!candidateCase) {
8284
+ cases.push({
8285
+ id: baselineCase.id,
8286
+ outcome: "MISSING_FROM_CANDIDATE",
8287
+ baseline: baselineCase
8288
+ });
8289
+ continue;
8290
+ }
8291
+ cases.push({
8292
+ id: baselineCase.id,
8293
+ outcome: compareCaseOutcome(baselineCase.pass, candidateCase.pass),
8294
+ baseline: baselineCase,
8295
+ candidate: candidateCase
8296
+ });
8297
+ }
8298
+ for (const candidateCase of candidate.caseResults) {
8299
+ if (seenIds.has(candidateCase.id)) {
8300
+ continue;
8301
+ }
8302
+ cases.push({
8303
+ id: candidateCase.id,
8304
+ outcome: "MISSING_FROM_BASELINE",
8305
+ candidate: candidateCase
8306
+ });
8307
+ }
8308
+ const baselinePassRate = passRate(baseline);
8309
+ const candidatePassRate = passRate(candidate);
8310
+ return {
8311
+ baselineLabel: labels?.baseline ?? "baseline",
8312
+ candidateLabel: labels?.candidate ?? candidate.metadata?.toolOverrideVariantId ?? "candidate",
8313
+ baselinePassRate,
8314
+ candidatePassRate,
8315
+ deltaPassRate: candidatePassRate - baselinePassRate,
8316
+ ...metricDelta(
8317
+ "ToolPrecision",
8318
+ baseline.datasetToolPrecision,
8319
+ candidate.datasetToolPrecision
8320
+ ),
8321
+ ...metricDelta(
8322
+ "ToolRecall",
8323
+ baseline.datasetToolRecall,
8324
+ candidate.datasetToolRecall
8325
+ ),
8326
+ ...metricDelta("ToolF1", baseline.datasetToolF1, candidate.datasetToolF1),
8327
+ cases,
8328
+ improvedCases: cases.filter((c) => c.outcome === "IMPROVED"),
8329
+ regressedCases: cases.filter((c) => c.outcome === "REGRESSED"),
8330
+ unchangedPasses: cases.filter((c) => c.outcome === "UNCHANGED_PASS"),
8331
+ unchangedFailures: cases.filter((c) => c.outcome === "UNCHANGED_FAIL"),
8332
+ missingFromBaseline: cases.filter(
8333
+ (c) => c.outcome === "MISSING_FROM_BASELINE"
8334
+ ),
8335
+ missingFromCandidate: cases.filter(
8336
+ (c) => c.outcome === "MISSING_FROM_CANDIDATE"
8337
+ )
8338
+ };
8339
+ }
8340
+ async function loadStoredEvalRunnerResult(storeLike, ref) {
8341
+ const store = resolveEvalResultStore(storeLike);
8342
+ const artifact = ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8343
+ "eval-runner-result",
8344
+ ref.id
8345
+ );
8346
+ if (!artifact) {
8347
+ throw new Error("No latest eval run artifact found");
8348
+ }
8349
+ return artifact;
8350
+ }
8351
+ async function saveEvalRunComparison(options) {
8352
+ const store = resolveEvalResultStore(options.store);
8353
+ const data = options.redactStoredResponses ? redactResponses2(options.comparison) : options.comparison;
8354
+ const artifact = createStoredEvalArtifact({
8355
+ kind: "eval-run-comparison",
8356
+ id: options.id,
8357
+ data,
8358
+ metadata: {
8359
+ labels: {
8360
+ baseline: options.comparison.baselineLabel,
8361
+ candidate: options.comparison.candidateLabel
8362
+ },
8363
+ ...options.metadata ?? {}
8364
+ }
8365
+ });
8366
+ await store.saveArtifact(artifact);
8367
+ return artifact;
8368
+ }
8369
+ function compareCaseOutcome(baselinePass, candidatePass) {
8370
+ if (!baselinePass && candidatePass) return "IMPROVED";
8371
+ if (baselinePass && !candidatePass) return "REGRESSED";
8372
+ return baselinePass ? "UNCHANGED_PASS" : "UNCHANGED_FAIL";
8373
+ }
8374
+ function passRate(result) {
8375
+ return result.total > 0 ? result.passed / result.total : 0;
8376
+ }
8377
+ function metricDelta(name15, baselineValue, candidateValue) {
8378
+ const result = {};
8379
+ if (baselineValue !== void 0) {
8380
+ result[`baseline${name15}`] = baselineValue;
8381
+ }
8382
+ if (candidateValue !== void 0) {
8383
+ result[`candidate${name15}`] = candidateValue;
8384
+ }
8385
+ if (baselineValue !== void 0 && candidateValue !== void 0) {
8386
+ result[`delta${name15}`] = candidateValue - baselineValue;
8387
+ }
8388
+ return result;
8389
+ }
8390
+ function redactResponses2(value) {
8391
+ return JSON.parse(
8392
+ JSON.stringify(
8393
+ value,
8394
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8395
+ )
8396
+ );
8397
+ }
8398
+
8399
+ // src/evals/variantExperiment.ts
8400
+ async function runVariantExperiment(options, context) {
8401
+ const metric = options.metric ?? "passRate";
8402
+ const maxRounds = options.maxRounds ?? 1;
8403
+ const minImprovement = options.minImprovement ?? 0;
8404
+ const allowRegressions = options.allowRegressions ?? false;
8405
+ const baseline = await runEvalDataset(
8406
+ buildRunOptions(options, void 0),
8407
+ context
8408
+ );
8409
+ const baselineValue = readMetric(baseline, metric);
8410
+ if (baselineValue === void 0) {
8411
+ throw new Error(
8412
+ `Metric '${metric}' is unavailable: the dataset produced no tool precision/recall data. Add mcp_host cases with toolsTriggered expectations, or use metric 'passRate'.`
8413
+ );
8414
+ }
8415
+ const rounds = [];
8416
+ let bestSoFar;
8417
+ let bestAttempted;
8418
+ let reason = "max-rounds";
8419
+ for (let round = 0; round < maxRounds; round++) {
8420
+ const variants = await gatherVariants(options, {
8421
+ round,
8422
+ baseline,
8423
+ metric,
8424
+ history: rounds,
8425
+ bestSoFar
8426
+ });
8427
+ if (variants.length === 0) {
8428
+ reason = round === 0 ? "no-variants" : "no-improvement";
8429
+ break;
8430
+ }
8431
+ const candidates = [];
8432
+ for (const variant of variants) {
8433
+ const candidate = await scoreVariant(
8434
+ options,
8435
+ context,
8436
+ baseline,
8437
+ baselineValue,
8438
+ metric,
8439
+ allowRegressions,
8440
+ variant
8441
+ );
8442
+ candidates.push(candidate);
8443
+ bestAttempted = pickBetter(bestAttempted, candidate, true);
8444
+ }
8445
+ const roundBest = candidates.reduce(
8446
+ (best, candidate) => pickBetter(best, candidate, false),
8447
+ void 0
8448
+ );
8449
+ rounds.push({ round, candidates, best: roundBest });
8450
+ if (roundBest) {
8451
+ const improvement = roundBest.metricValue - (bestSoFar?.metricValue ?? baselineValue);
8452
+ bestSoFar = pickBetter(bestSoFar, roundBest, false);
8453
+ if (improvement < minImprovement) {
8454
+ reason = "no-improvement";
8455
+ break;
8456
+ }
8457
+ }
8458
+ }
8459
+ const winner = bestSoFar;
8460
+ const proposalSource = winner ?? bestAttempted;
8461
+ const proposal = proposalSource ? buildProposal(metric, baselineValue, proposalSource, winner !== void 0) : void 0;
8462
+ return {
8463
+ metric,
8464
+ baseline,
8465
+ rounds,
8466
+ winner,
8467
+ proposal,
8468
+ converged: true,
8469
+ reason
8470
+ };
8471
+ }
8472
+ async function gatherVariants(options, context) {
8473
+ if (context.round === 0 && options.variants && options.variants.length > 0) {
8474
+ return options.variants;
8475
+ }
8476
+ if (options.proposeVariants) {
8477
+ return options.proposeVariants(context);
8478
+ }
8479
+ return [];
8480
+ }
8481
+ async function scoreVariant(options, context, baseline, baselineValue, metric, allowRegressions, variant) {
8482
+ const result = await runEvalDataset(
8483
+ buildRunOptions(options, variant),
8484
+ context
8485
+ );
8486
+ const comparison = compareEvalRuns({
8487
+ baseline,
8488
+ candidate: result,
8489
+ labels: { candidate: variant.id }
8490
+ });
8491
+ const metricValue = readMetric(result, metric) ?? baselineValue;
8492
+ const disqualified = !allowRegressions && comparison.regressedCases.length > 0;
8493
+ return {
8494
+ variant,
8495
+ result,
8496
+ comparison,
8497
+ metricValue,
8498
+ metricDelta: metricValue - baselineValue,
8499
+ disqualified
8500
+ };
8501
+ }
8502
+ function pickBetter(incumbent, challenger, includeDisqualified) {
8503
+ if (!includeDisqualified && challenger.disqualified) {
8504
+ return incumbent;
8505
+ }
8506
+ if (!incumbent) {
8507
+ return challenger;
8508
+ }
8509
+ return challenger.metricValue > incumbent.metricValue ? challenger : incumbent;
8510
+ }
8511
+ function buildProposal(metric, baselineValue, source, isWinner) {
8512
+ let recommendation;
8513
+ if (isWinner) {
8514
+ recommendation = source.metricDelta > 0 ? "apply" : "inconclusive";
8515
+ } else {
8516
+ recommendation = source.disqualified ? "reject" : "inconclusive";
8517
+ }
8518
+ return {
8519
+ variantId: source.variant.id,
8520
+ metric,
8521
+ baselineValue,
8522
+ candidateValue: source.metricValue,
8523
+ delta: source.metricDelta,
8524
+ toolChanges: source.variant.tools,
8525
+ improvedCaseIds: source.comparison.improvedCases.map((c) => c.id),
8526
+ regressedCaseIds: source.comparison.regressedCases.map((c) => c.id),
8527
+ recommendation
8528
+ };
8529
+ }
8530
+ function readMetric(result, metric) {
8531
+ switch (metric) {
8532
+ case "passRate":
8533
+ return result.total > 0 ? result.passed / result.total : 0;
8534
+ case "toolF1":
8535
+ return result.datasetToolF1;
8536
+ case "toolPrecision":
8537
+ return result.datasetToolPrecision;
8538
+ case "toolRecall":
8539
+ return result.datasetToolRecall;
8540
+ }
8541
+ }
8542
+ function buildRunOptions(options, toolOverrides) {
8543
+ return {
8544
+ dataset: options.dataset,
8545
+ toolOverrides,
8546
+ defaultLlmIterations: options.defaultLlmIterations,
8547
+ defaultJudgeReps: options.defaultJudgeReps,
8548
+ concurrency: options.concurrency,
8549
+ filterTags: options.filterTags,
8550
+ schemas: options.schemas,
8551
+ mcpHostModel: options.mcpHostModel,
8552
+ judgeModel: options.judgeModel
8553
+ };
7902
8554
  }
7903
8555
 
7904
8556
  // src/spec/conformanceChecks.ts
@@ -8072,6 +8724,6 @@ function formatCapabilities(capabilities) {
8072
8724
  return parts.length > 0 ? parts.join(", ") : "none declared";
8073
8725
  }
8074
8726
 
8075
- export { BUILT_IN_RUBRICS, CLIOAuthClient, DiscoveryError, ENV_VAR_NAMES, EvalCaseSchema, EvalDatasetSchema, MCPConfigSchema, MCP_PROTOCOL_VERSION, PlaywrightOAuthClientProvider, SnapshotSanitizers, clearJudgeRegistry, closeMCPClient, createJudge, createMCPClientForConfig, createMCPFixture, createTokenAuthHeaders, discoverAuthorizationServer, discoverProtectedResource, expect, extractText, getMissingDependencyMessage, getRegisteredJudge, getResponseSizeBytes, hasValidTokens, injectTokens, isBuiltInRubric, isHttpConfig, isProviderAvailable, isStdioConfig, isTokenExpired, isTokenExpiringSoon, loadBaseline, loadEvalDataset, loadEvalDatasetFromObject, loadTokens, loadTokensFromEnv, test2 as mcpAuthTest, normalizeToolResponse, normalizeWhitespace, performClientCredentialsFlow, performOAuthSetup, performOAuthSetupIfNeeded, refreshAccessToken, registerJudge, resolveRubric, runConformanceChecks, runEvalCase, runEvalDataset, runServerComparison, saveBaseline, simulateMCPHost, test, validateAccessToken, validateError, validateEvalCase, validateEvalDataset, validateJudge, validateMCPConfig, validatePattern, validateResponse, validateSchema, validateSize, validateText, validateToolCallCount, validateToolCalls };
8727
+ export { BUILT_IN_RUBRICS, CLIOAuthClient, DiscoveryError, ENV_VAR_NAMES, EvalCaseSchema, EvalDatasetSchema, FileEvalResultStore, GCSEvalResultStore, MCPConfigSchema, MCP_PROTOCOL_VERSION, PlaywrightOAuthClientProvider, SnapshotSanitizers, clearJudgeRegistry, closeMCPClient, compareEvalRuns, createDefaultArtifactId, createEvalResultStore, createJudge, createMCPClientForConfig, createMCPFixture, createStoredEvalArtifact, createTokenAuthHeaders, defaultEnvironmentMetadata, discoverAuthorizationServer, discoverProtectedResource, expect, extractText, getMissingDependencyMessage, getRegisteredJudge, getResponseSizeBytes, hasValidTokens, injectTokens, isBuiltInRubric, isEvalResultStore, isHttpConfig, isProviderAvailable, isStdioConfig, isTokenExpired, isTokenExpiringSoon, loadBaseline, loadEvalDataset, loadEvalDatasetFromObject, loadStoredEvalRunnerResult, loadTokens, loadTokensFromEnv, test2 as mcpAuthTest, normalizeToolResponse, normalizeWhitespace, performClientCredentialsFlow, performOAuthSetup, performOAuthSetupIfNeeded, refreshAccessToken, registerJudge, resolveEvalResultStore, resolveRubric, runConformanceChecks, runEvalCase, runEvalDataset, runServerComparison, runVariantExperiment, saveBaseline, saveEvalRunComparison, saveServerComparison, simulateMCPHost, test, validateAccessToken, validateError, validateEvalCase, validateEvalDataset, validateJudge, validateMCPConfig, validatePattern, validateResponse, validateSchema, validateSize, validateText, validateToolCallCount, validateToolCalls };
8076
8728
  //# sourceMappingURL=index.js.map
8077
8729
  //# sourceMappingURL=index.js.map