@gleanwork/mcp-server-tester 1.0.1-beta.0 → 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-beta.0"};
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) {
@@ -7280,6 +7280,216 @@ function buildBaselinePassMap(baseline) {
7280
7280
  }
7281
7281
  return map;
7282
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
+ }
7283
7493
  var execFileAsync = promisify(execFile);
7284
7494
  async function execFileNoThrow(file, args) {
7285
7495
  try {
@@ -7326,6 +7536,41 @@ function sumUsage(a, b) {
7326
7536
  }
7327
7537
 
7328
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
+ }
7329
7574
  async function executeToolCall(evalCase, mcp) {
7330
7575
  const mode = evalCase.mode || "direct";
7331
7576
  try {
@@ -7510,9 +7755,12 @@ async function runExpectBlockValidations(expectBlock, response, config) {
7510
7755
  }
7511
7756
  return { expectations: results, toolPrecision, toolRecall };
7512
7757
  }
7513
- function buildRequest(evalCase) {
7758
+ function buildRequest(evalCase, toolOverrideVariantId) {
7514
7759
  const request = {};
7515
7760
  if (evalCase.description) request.description = evalCase.description;
7761
+ if (toolOverrideVariantId !== void 0) {
7762
+ request.toolOverrideVariantId = toolOverrideVariantId;
7763
+ }
7516
7764
  if (evalCase.mode === "mcp_host") {
7517
7765
  if (evalCase.scenario) request.scenario = evalCase.scenario;
7518
7766
  if (evalCase.mcpHostConfig) {
@@ -7577,7 +7825,7 @@ async function runSingleIteration(evalCase, context, options) {
7577
7825
  toolName: evalCase.scenario != null ? "mcp_host" : evalCase.toolName ?? "unknown",
7578
7826
  source: "eval",
7579
7827
  pass: didCasePass(error, expectationResults),
7580
- request: buildRequest(evalCase),
7828
+ request: buildRequest(evalCase, options.toolOverrideVariantId),
7581
7829
  response,
7582
7830
  error,
7583
7831
  expectations: expectationResults,
@@ -7656,7 +7904,8 @@ async function runEvalCase(evalCase, context, options = {}) {
7656
7904
  authType: context.mcp.authType,
7657
7905
  project: context.mcp.project,
7658
7906
  durationMs: 0,
7659
- tags: evalCase.tags
7907
+ tags: evalCase.tags,
7908
+ request: buildRequest(evalCase, options.toolOverrideVariantId)
7660
7909
  };
7661
7910
  const totalHostUsage = iterationResults.reduce(
7662
7911
  (acc, r) => sumUsage(acc, r.hostUsage),
@@ -7715,11 +7964,15 @@ async function runEvalDataset(options, context) {
7715
7964
  filterTags,
7716
7965
  saveResultsTo,
7717
7966
  omitResponsesFromBaseline = true,
7967
+ redactStoredResponses,
7968
+ resultStore,
7718
7969
  baselineResultsFrom,
7970
+ toolOverrides,
7719
7971
  mcpHostModel,
7720
7972
  judgeModel
7721
7973
  } = options;
7722
7974
  const startTime = Date.now();
7975
+ const effectiveContext = toolOverrides ? { ...context, mcp: createToolOverrideMCP(context.mcp, toolOverrides) } : context;
7723
7976
  const allSchemas = {
7724
7977
  ...dataset.schemas,
7725
7978
  ...schemas
@@ -7751,9 +8004,10 @@ async function runEvalDataset(options, context) {
7751
8004
  }
7752
8005
  }
7753
8006
  const effectiveCase = withIterations.judgeReps === void 0 && defaultJudgeReps !== void 0 ? { ...withIterations, judgeReps: defaultJudgeReps } : withIterations;
7754
- const result2 = await runEvalCase(effectiveCase, context, {
8007
+ const result2 = await runEvalCase(effectiveCase, effectiveContext, {
7755
8008
  datasetName: dataset.name,
7756
- schemas: allSchemas
8009
+ schemas: allSchemas,
8010
+ toolOverrideVariantId: toolOverrides?.id
7757
8011
  });
7758
8012
  if (onCaseComplete) {
7759
8013
  await onCaseComplete(result2);
@@ -7778,6 +8032,9 @@ async function runEvalDataset(options, context) {
7778
8032
  gitHash,
7779
8033
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7780
8034
  packageVersion: package_default.version,
8035
+ ...toolOverrides !== void 0 && {
8036
+ toolOverrideVariantId: toolOverrides.id
8037
+ },
7781
8038
  ...mcpHostModel !== void 0 && { mcpHostModel },
7782
8039
  ...judgeModel !== void 0 && { judgeModel }
7783
8040
  };
@@ -7796,7 +8053,7 @@ async function runEvalDataset(options, context) {
7796
8053
  };
7797
8054
  if (baselineResultsFrom) {
7798
8055
  try {
7799
- const baseline = await loadBaseline(baselineResultsFrom);
8056
+ const baseline = typeof baselineResultsFrom === "string" ? await loadBaseline(baselineResultsFrom) : await loadStoredBaseline(baselineResultsFrom, resultStore);
7800
8057
  const baselinePassRate = baseline.total > 0 ? baseline.passed / baseline.total : 0;
7801
8058
  const baselineMap = buildBaselinePassMap(baseline);
7802
8059
  const currentCaseIds = result.caseResults.map((cr) => cr.id);
@@ -7824,7 +8081,7 @@ async function runEvalDataset(options, context) {
7824
8081
  result.deltaPassRate = result.total > 0 ? result.passed / result.total - baselinePassRate : 0;
7825
8082
  } catch (err) {
7826
8083
  console.warn(
7827
- `[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)}`
7828
8085
  );
7829
8086
  }
7830
8087
  }
@@ -7839,9 +8096,26 @@ async function runEvalDataset(options, context) {
7839
8096
  result.datasetToolF1 = avgPrec + avgRecall > 0 ? 2 * avgPrec * avgRecall / (avgPrec + avgRecall) : 0;
7840
8097
  }
7841
8098
  if (saveResultsTo) {
7842
- await saveBaseline(result, saveResultsTo, {
7843
- omitResponses: omitResponsesFromBaseline
7844
- });
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
+ }
7845
8119
  }
7846
8120
  if (context.testInfo) {
7847
8121
  await context.testInfo.attach("mcp-test-results", {
@@ -7855,6 +8129,50 @@ async function runEvalDataset(options, context) {
7855
8129
  }
7856
8130
  return result;
7857
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
+ }
7858
8176
 
7859
8177
  // src/evals/serverComparison.ts
7860
8178
  async function runServerComparison(options, contextA, contextB) {
@@ -7896,7 +8214,7 @@ async function runServerComparison(options, contextA, contextB) {
7896
8214
  }
7897
8215
  const total = cases.length;
7898
8216
  const decidedCases = aWins + bWins + ties;
7899
- return {
8217
+ const comparison = {
7900
8218
  dataset: options.dataset.name,
7901
8219
  total,
7902
8220
  aWins,
@@ -7913,6 +8231,326 @@ async function runServerComparison(options, contextA, contextB) {
7913
8231
  serverBResult: resultB,
7914
8232
  durationMs: Date.now() - startTime
7915
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
+ };
7916
8554
  }
7917
8555
 
7918
8556
  // src/spec/conformanceChecks.ts
@@ -8086,6 +8724,6 @@ function formatCapabilities(capabilities) {
8086
8724
  return parts.length > 0 ? parts.join(", ") : "none declared";
8087
8725
  }
8088
8726
 
8089
- 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 };
8090
8728
  //# sourceMappingURL=index.js.map
8091
8729
  //# sourceMappingURL=index.js.map