@gleanwork/mcp-server-tester 1.0.1 → 1.1.1

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.1"};
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) {
@@ -6838,8 +6838,8 @@ Hint: run \`getMissingDependencyMessage('${provider}')\` or check docs/mcp-host.
6838
6838
  Hint: check your API key environment variable (e.g. ANTHROPIC_API_KEY, GOOGLE_APPLICATION_CREDENTIALS).`;
6839
6839
  }
6840
6840
  if (raw.includes("404") || raw.includes("Not Found") || raw.toLowerCase().includes("model") && raw.toLowerCase().includes("not found")) {
6841
- return `MCP host simulation failed: model not found.
6842
- Hint: check the model name format for your provider. For vertex-anthropic use 'claude-3-5-haiku@20241022' (with @).`;
6841
+ return `MCP host simulation failed: ${raw}
6842
+ Hint: a 404 usually means the model id is wrong or retired, or a base-URL override (e.g. ANTHROPIC_BASE_URL / OPENAI_BASE_URL pointing at a gateway) is routing requests somewhere that doesn't serve this model. Verify the model id and that no unexpected *_BASE_URL is set.`;
6843
6843
  }
6844
6844
  if (raw.includes("ENOTFOUND") || raw.includes("fetch failed") || raw.includes("ECONNREFUSED")) {
6845
6845
  return `MCP host simulation failed: network error.
@@ -6937,12 +6937,18 @@ function createVercelOrchestrator() {
6937
6937
  tools[toolName] = {
6938
6938
  description: mcpTool.description ?? "",
6939
6939
  inputSchema: jsonSchema2(rawSchema),
6940
- execute: async (args) => {
6940
+ execute: async (args, opts) => {
6941
6941
  const mcpStart = Date.now();
6942
6942
  const result2 = await mcp.callTool(toolName, args);
6943
6943
  mcpDurationMs += Date.now() - mcpStart;
6944
- allToolCalls.push({ name: toolName, arguments: args });
6945
- return extractText(result2);
6944
+ const output = extractText(result2);
6945
+ allToolCalls.push({
6946
+ id: opts?.toolCallId,
6947
+ name: toolName,
6948
+ arguments: args,
6949
+ output
6950
+ });
6951
+ return output;
6946
6952
  }
6947
6953
  };
6948
6954
  }
@@ -6964,10 +6970,17 @@ function createVercelOrchestrator() {
6964
6970
  totalCostUsd: 0,
6965
6971
  durationMs: llmDurationMs
6966
6972
  } : void 0;
6967
- const conversationHistory = (result.steps ?? []).map((step) => ({
6968
- role: step.toolCalls?.length > 0 ? "tool" : "assistant",
6969
- content: step.toolCalls?.length > 0 ? JSON.stringify(step.toolResults) : step.text ?? ""
6970
- }));
6973
+ const conversationHistory = (result.steps ?? []).flatMap((step) => {
6974
+ if (step.toolCalls?.length > 0) {
6975
+ return step.toolCalls.map(
6976
+ (tc) => ({
6977
+ role: "tool",
6978
+ toolCallId: tc.toolCallId
6979
+ })
6980
+ );
6981
+ }
6982
+ return step.text ? [{ role: "assistant", content: step.text }] : [];
6983
+ });
6971
6984
  return {
6972
6985
  success: true,
6973
6986
  toolCalls: allToolCalls,
@@ -6994,6 +7007,7 @@ function parseStreamJson(stdout) {
6994
7007
  const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
6995
7008
  const toolCalls = [];
6996
7009
  const textParts = [];
7010
+ let usage;
6997
7011
  const conversationHistory = [];
6998
7012
  for (const line of lines) {
6999
7013
  let event;
@@ -7022,20 +7036,38 @@ function parseStreamJson(stdout) {
7022
7036
  for (const block of event.message.content) {
7023
7037
  if (block.type === "tool_result") {
7024
7038
  const content = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
7025
- conversationHistory.push({ role: "tool", content });
7039
+ const call = block.tool_use_id ? toolCalls.find((tc) => tc.id === block.tool_use_id) : void 0;
7040
+ if (call) {
7041
+ call.output = content;
7042
+ conversationHistory.push({ role: "tool", toolCallId: call.id });
7043
+ } else {
7044
+ conversationHistory.push({ role: "tool", content });
7045
+ }
7026
7046
  }
7027
7047
  }
7028
7048
  }
7029
- if (event.type === "result" && typeof event.result === "string") {
7030
- if (textParts.length === 0) {
7049
+ if (event.type === "result") {
7050
+ if (typeof event.result === "string" && textParts.length === 0) {
7031
7051
  textParts.push(event.result);
7032
7052
  }
7053
+ if (event.usage) {
7054
+ usage = {
7055
+ inputTokens: event.usage.input_tokens ?? 0,
7056
+ outputTokens: event.usage.output_tokens ?? 0,
7057
+ totalCostUsd: event.total_cost_usd ?? 0,
7058
+ durationMs: event.duration_ms ?? 0,
7059
+ durationApiMs: event.duration_api_ms,
7060
+ cacheReadInputTokens: event.usage.cache_read_input_tokens,
7061
+ cacheCreationInputTokens: event.usage.cache_creation_input_tokens
7062
+ };
7063
+ }
7033
7064
  }
7034
7065
  if (event.type === "result" && event.is_error === true) {
7035
7066
  return {
7036
7067
  success: false,
7037
7068
  toolCalls,
7038
- error: typeof event.result === "string" ? event.result : "CLI host reported an error"
7069
+ error: typeof event.result === "string" ? event.result : "CLI host reported an error",
7070
+ usage
7039
7071
  };
7040
7072
  }
7041
7073
  }
@@ -7047,7 +7079,8 @@ function parseStreamJson(stdout) {
7047
7079
  success: true,
7048
7080
  toolCalls,
7049
7081
  response: response || void 0,
7050
- conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0
7082
+ conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0,
7083
+ usage
7051
7084
  };
7052
7085
  }
7053
7086
  function createJsonParser(paths) {
@@ -7293,6 +7326,216 @@ function buildBaselinePassMap(baseline) {
7293
7326
  }
7294
7327
  return map;
7295
7328
  }
7329
+ var KIND_DIRS = {
7330
+ "eval-runner-result": "eval-runs",
7331
+ "reporter-run": "reporter-runs",
7332
+ "eval-run-comparison": "comparisons/eval-runs",
7333
+ "server-comparison": "comparisons/servers"
7334
+ };
7335
+ function createEvalResultStore(config) {
7336
+ if (config.provider === "file") {
7337
+ return new FileEvalResultStore(config);
7338
+ }
7339
+ return new GCSEvalResultStore(config);
7340
+ }
7341
+ function resolveEvalResultStore(store) {
7342
+ return isEvalResultStore(store) ? store : createEvalResultStore(store);
7343
+ }
7344
+ function isEvalResultStore(value) {
7345
+ return typeof value === "object" && value !== null && "saveArtifact" in value && "loadArtifact" in value && "loadLatestArtifact" in value && "listArtifacts" in value;
7346
+ }
7347
+ function createStoredEvalArtifact(options) {
7348
+ const createdAt = options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
7349
+ return {
7350
+ schemaVersion: 1,
7351
+ kind: options.kind,
7352
+ id: options.id ?? createDefaultArtifactId(createdAt),
7353
+ createdAt,
7354
+ metadata: {
7355
+ ...defaultEnvironmentMetadata(),
7356
+ ...options.metadata ?? {}
7357
+ },
7358
+ data: options.data
7359
+ };
7360
+ }
7361
+ function createDefaultArtifactId(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
7362
+ const safeTimestamp = timestamp.replace(/[:.]/g, "-");
7363
+ const runNumber = process.env.GITHUB_RUN_NUMBER;
7364
+ const sha = process.env.GITHUB_SHA?.slice(0, 12);
7365
+ const suffix = runNumber ?? sha;
7366
+ return suffix ? `${safeTimestamp}-${suffix}` : safeTimestamp;
7367
+ }
7368
+ function defaultEnvironmentMetadata() {
7369
+ return {
7370
+ ...process.env.GITHUB_SHA !== void 0 && {
7371
+ gitHash: process.env.GITHUB_SHA
7372
+ },
7373
+ ...process.env.GITHUB_REF_NAME !== void 0 && {
7374
+ branch: process.env.GITHUB_REF_NAME
7375
+ },
7376
+ ...process.env.GITHUB_RUN_NUMBER !== void 0 && {
7377
+ runNumber: process.env.GITHUB_RUN_NUMBER
7378
+ },
7379
+ ...process.env.GITHUB_EVENT_NAME !== void 0 && {
7380
+ trigger: process.env.GITHUB_EVENT_NAME
7381
+ }
7382
+ };
7383
+ }
7384
+ var FileEvalResultStore = class {
7385
+ dir;
7386
+ constructor(config) {
7387
+ this.dir = config.dir;
7388
+ }
7389
+ async saveArtifact(artifact) {
7390
+ const artifactDir = path2.join(this.dir, KIND_DIRS[artifact.kind]);
7391
+ await fs$1.mkdir(artifactDir, { recursive: true });
7392
+ const serialized = JSON.stringify(artifact, null, 2);
7393
+ await fs$1.writeFile(
7394
+ path2.join(artifactDir, `${artifact.id}.json`),
7395
+ serialized,
7396
+ "utf8"
7397
+ );
7398
+ await fs$1.writeFile(path2.join(artifactDir, "latest.json"), serialized, "utf8");
7399
+ }
7400
+ async loadArtifact(kind, id) {
7401
+ const raw = await fs$1.readFile(
7402
+ path2.join(this.dir, KIND_DIRS[kind], `${id}.json`),
7403
+ "utf8"
7404
+ );
7405
+ return JSON.parse(raw);
7406
+ }
7407
+ async loadLatestArtifact(kind) {
7408
+ try {
7409
+ const raw = await fs$1.readFile(
7410
+ path2.join(this.dir, KIND_DIRS[kind], "latest.json"),
7411
+ "utf8"
7412
+ );
7413
+ return JSON.parse(raw);
7414
+ } catch (error) {
7415
+ if (isMissingFileError(error)) {
7416
+ return null;
7417
+ }
7418
+ throw error;
7419
+ }
7420
+ }
7421
+ async listArtifacts(kind, options = {}) {
7422
+ let files;
7423
+ try {
7424
+ files = await fs$1.readdir(path2.join(this.dir, KIND_DIRS[kind]));
7425
+ } catch (error) {
7426
+ if (isMissingFileError(error)) {
7427
+ return [];
7428
+ }
7429
+ throw error;
7430
+ }
7431
+ const summaries = await Promise.all(
7432
+ files.filter((f) => f.endsWith(".json") && f !== "latest.json").map(async (file) => {
7433
+ const raw = await fs$1.readFile(
7434
+ path2.join(this.dir, KIND_DIRS[kind], file),
7435
+ "utf8"
7436
+ );
7437
+ return toSummary(JSON.parse(raw));
7438
+ })
7439
+ );
7440
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7441
+ }
7442
+ };
7443
+ var GCSEvalResultStore = class {
7444
+ bucketName;
7445
+ prefix;
7446
+ storage;
7447
+ constructor(config) {
7448
+ this.bucketName = config.bucket;
7449
+ this.prefix = trimSlashes(config.prefix ?? "");
7450
+ }
7451
+ async saveArtifact(artifact) {
7452
+ const bucket = await this.getBucket();
7453
+ const serialized = JSON.stringify(artifact, null, 2);
7454
+ await bucket.file(
7455
+ this.objectPath(
7456
+ artifact.kind,
7457
+ `${encodeURIComponent(artifact.id)}.json`
7458
+ )
7459
+ ).save(serialized, {
7460
+ contentType: "application/json",
7461
+ resumable: false,
7462
+ validation: false
7463
+ });
7464
+ await bucket.file(this.objectPath(artifact.kind, "latest.json")).save(serialized, {
7465
+ contentType: "application/json",
7466
+ resumable: false,
7467
+ validation: false
7468
+ });
7469
+ }
7470
+ async loadArtifact(kind, id) {
7471
+ const bucket = await this.getBucket();
7472
+ const file = bucket.file(
7473
+ this.objectPath(kind, `${encodeURIComponent(id)}.json`)
7474
+ );
7475
+ const [buffer] = await file.download();
7476
+ return JSON.parse(buffer.toString("utf8"));
7477
+ }
7478
+ async loadLatestArtifact(kind) {
7479
+ const bucket = await this.getBucket();
7480
+ const file = bucket.file(this.objectPath(kind, "latest.json"));
7481
+ const [exists] = await file.exists();
7482
+ if (!exists) return null;
7483
+ const [buffer] = await file.download();
7484
+ return JSON.parse(buffer.toString("utf8"));
7485
+ }
7486
+ async listArtifacts(kind, options = {}) {
7487
+ const bucket = await this.getBucket();
7488
+ const [files] = await bucket.getFiles({
7489
+ prefix: this.objectPath(kind, "")
7490
+ });
7491
+ const summaries = await Promise.all(
7492
+ files.filter(
7493
+ (f) => f.name.endsWith(".json") && !f.name.endsWith("/latest.json")
7494
+ ).map(async (file) => {
7495
+ const [buffer] = await file.download();
7496
+ return toSummary(
7497
+ JSON.parse(buffer.toString("utf8"))
7498
+ );
7499
+ })
7500
+ );
7501
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7502
+ }
7503
+ async getBucket() {
7504
+ if (!this.storage) {
7505
+ const moduleName = "@google-cloud/storage";
7506
+ let Storage;
7507
+ try {
7508
+ const mod = await import(moduleName);
7509
+ Storage = mod.Storage;
7510
+ } catch (error) {
7511
+ throw new Error(
7512
+ `GCS result storage requires the optional \`@google-cloud/storage\` package. Install it and authenticate with Application Default Credentials via GOOGLE_APPLICATION_CREDENTIALS.
7513
+ Original error: ${error instanceof Error ? error.message : String(error)}`
7514
+ );
7515
+ }
7516
+ this.storage = new Storage();
7517
+ }
7518
+ return this.storage.bucket(this.bucketName);
7519
+ }
7520
+ objectPath(kind, filename) {
7521
+ const parts = [this.prefix, KIND_DIRS[kind], filename].filter(Boolean);
7522
+ return parts.join("/");
7523
+ }
7524
+ };
7525
+ function toSummary(artifact) {
7526
+ return {
7527
+ kind: artifact.kind,
7528
+ id: artifact.id,
7529
+ createdAt: artifact.createdAt,
7530
+ metadata: artifact.metadata
7531
+ };
7532
+ }
7533
+ function trimSlashes(value) {
7534
+ return value.replace(/^\/+|\/+$/g, "");
7535
+ }
7536
+ function isMissingFileError(error) {
7537
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
7538
+ }
7296
7539
  var execFileAsync = util.promisify(child_process.execFile);
7297
7540
  async function execFileNoThrow(file, args) {
7298
7541
  try {
@@ -7339,6 +7582,41 @@ function sumUsage(a, b) {
7339
7582
  }
7340
7583
 
7341
7584
  // src/evals/evalRunner.ts
7585
+ function createToolOverrideMCP(mcp, variant) {
7586
+ return {
7587
+ ...mcp,
7588
+ async listTools() {
7589
+ const tools = await mcp.listTools();
7590
+ const knownToolNames = new Set(tools.map((tool2) => tool2.name));
7591
+ const unknownToolNames = Object.keys(variant.tools).filter(
7592
+ (name15) => !knownToolNames.has(name15)
7593
+ );
7594
+ if (unknownToolNames.length > 0) {
7595
+ throw new Error(
7596
+ `[mcp-server-tester] toolOverrides variant "${variant.id}" references unknown tool(s): ` + unknownToolNames.join(", ")
7597
+ );
7598
+ }
7599
+ return tools.map((tool2) => {
7600
+ const override = variant.tools[tool2.name];
7601
+ if (!override) {
7602
+ return tool2;
7603
+ }
7604
+ return {
7605
+ ...tool2,
7606
+ ...override.description !== void 0 && {
7607
+ description: override.description
7608
+ },
7609
+ ...override.inputSchema !== void 0 && {
7610
+ inputSchema: override.inputSchema
7611
+ }
7612
+ };
7613
+ });
7614
+ },
7615
+ async callTool(name15, args) {
7616
+ return mcp.callTool(name15, args);
7617
+ }
7618
+ };
7619
+ }
7342
7620
  async function executeToolCall(evalCase, mcp) {
7343
7621
  const mode = evalCase.mode || "direct";
7344
7622
  try {
@@ -7523,9 +7801,12 @@ async function runExpectBlockValidations(expectBlock, response, config) {
7523
7801
  }
7524
7802
  return { expectations: results, toolPrecision, toolRecall };
7525
7803
  }
7526
- function buildRequest(evalCase) {
7804
+ function buildRequest(evalCase, toolOverrideVariantId) {
7527
7805
  const request = {};
7528
7806
  if (evalCase.description) request.description = evalCase.description;
7807
+ if (toolOverrideVariantId !== void 0) {
7808
+ request.toolOverrideVariantId = toolOverrideVariantId;
7809
+ }
7529
7810
  if (evalCase.mode === "mcp_host") {
7530
7811
  if (evalCase.scenario) request.scenario = evalCase.scenario;
7531
7812
  if (evalCase.mcpHostConfig) {
@@ -7590,7 +7871,7 @@ async function runSingleIteration(evalCase, context, options) {
7590
7871
  toolName: evalCase.scenario != null ? "mcp_host" : evalCase.toolName ?? "unknown",
7591
7872
  source: "eval",
7592
7873
  pass: didCasePass(error, expectationResults),
7593
- request: buildRequest(evalCase),
7874
+ request: buildRequest(evalCase, options.toolOverrideVariantId),
7594
7875
  response,
7595
7876
  error,
7596
7877
  expectations: expectationResults,
@@ -7669,7 +7950,8 @@ async function runEvalCase(evalCase, context, options = {}) {
7669
7950
  authType: context.mcp.authType,
7670
7951
  project: context.mcp.project,
7671
7952
  durationMs: 0,
7672
- tags: evalCase.tags
7953
+ tags: evalCase.tags,
7954
+ request: buildRequest(evalCase, options.toolOverrideVariantId)
7673
7955
  };
7674
7956
  const totalHostUsage = iterationResults.reduce(
7675
7957
  (acc, r) => sumUsage(acc, r.hostUsage),
@@ -7716,6 +7998,7 @@ async function getGitHash() {
7716
7998
  const result = await execFileNoThrow("git", ["rev-parse", "HEAD"]);
7717
7999
  return result.status === 0 ? result.stdout.trim() : void 0;
7718
8000
  }
8001
+ var warnedNoTestInfo = false;
7719
8002
  async function runEvalDataset(options, context) {
7720
8003
  const {
7721
8004
  dataset,
@@ -7728,11 +8011,15 @@ async function runEvalDataset(options, context) {
7728
8011
  filterTags,
7729
8012
  saveResultsTo,
7730
8013
  omitResponsesFromBaseline = true,
8014
+ redactStoredResponses,
8015
+ resultStore,
7731
8016
  baselineResultsFrom,
8017
+ toolOverrides,
7732
8018
  mcpHostModel,
7733
8019
  judgeModel
7734
8020
  } = options;
7735
8021
  const startTime = Date.now();
8022
+ const effectiveContext = toolOverrides ? { ...context, mcp: createToolOverrideMCP(context.mcp, toolOverrides) } : context;
7736
8023
  const allSchemas = {
7737
8024
  ...dataset.schemas,
7738
8025
  ...schemas
@@ -7764,9 +8051,10 @@ async function runEvalDataset(options, context) {
7764
8051
  }
7765
8052
  }
7766
8053
  const effectiveCase = withIterations.judgeReps === void 0 && defaultJudgeReps !== void 0 ? { ...withIterations, judgeReps: defaultJudgeReps } : withIterations;
7767
- const result2 = await runEvalCase(effectiveCase, context, {
8054
+ const result2 = await runEvalCase(effectiveCase, effectiveContext, {
7768
8055
  datasetName: dataset.name,
7769
- schemas: allSchemas
8056
+ schemas: allSchemas,
8057
+ toolOverrideVariantId: toolOverrides?.id
7770
8058
  });
7771
8059
  if (onCaseComplete) {
7772
8060
  await onCaseComplete(result2);
@@ -7791,6 +8079,9 @@ async function runEvalDataset(options, context) {
7791
8079
  gitHash,
7792
8080
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7793
8081
  packageVersion: package_default.version,
8082
+ ...toolOverrides !== void 0 && {
8083
+ toolOverrideVariantId: toolOverrides.id
8084
+ },
7794
8085
  ...mcpHostModel !== void 0 && { mcpHostModel },
7795
8086
  ...judgeModel !== void 0 && { judgeModel }
7796
8087
  };
@@ -7809,7 +8100,7 @@ async function runEvalDataset(options, context) {
7809
8100
  };
7810
8101
  if (baselineResultsFrom) {
7811
8102
  try {
7812
- const baseline = await loadBaseline(baselineResultsFrom);
8103
+ const baseline = typeof baselineResultsFrom === "string" ? await loadBaseline(baselineResultsFrom) : await loadStoredBaseline(baselineResultsFrom, resultStore);
7813
8104
  const baselinePassRate = baseline.total > 0 ? baseline.passed / baseline.total : 0;
7814
8105
  const baselineMap = buildBaselinePassMap(baseline);
7815
8106
  const currentCaseIds = result.caseResults.map((cr) => cr.id);
@@ -7837,7 +8128,7 @@ async function runEvalDataset(options, context) {
7837
8128
  result.deltaPassRate = result.total > 0 ? result.passed / result.total - baselinePassRate : 0;
7838
8129
  } catch (err) {
7839
8130
  console.warn(
7840
- `[mcp-server-tester] Could not load baseline from ${baselineResultsFrom}: ${err instanceof Error ? err.message : String(err)}`
8131
+ `[mcp-server-tester] Could not load baseline from ${formatBaselineRef(baselineResultsFrom)}: ${err instanceof Error ? err.message : String(err)}`
7841
8132
  );
7842
8133
  }
7843
8134
  }
@@ -7852,22 +8143,84 @@ async function runEvalDataset(options, context) {
7852
8143
  result.datasetToolF1 = avgPrec + avgRecall > 0 ? 2 * avgPrec * avgRecall / (avgPrec + avgRecall) : 0;
7853
8144
  }
7854
8145
  if (saveResultsTo) {
7855
- await saveBaseline(result, saveResultsTo, {
7856
- omitResponses: omitResponsesFromBaseline
7857
- });
8146
+ if (typeof saveResultsTo === "string") {
8147
+ await saveBaseline(result, saveResultsTo, {
8148
+ omitResponses: omitResponsesFromBaseline
8149
+ });
8150
+ } else {
8151
+ await saveStoredEvalResult(result, saveResultsTo, {
8152
+ resultStore,
8153
+ omitResponses: redactStoredResponses ?? true,
8154
+ metadata: {
8155
+ datasetName: dataset.name,
8156
+ ...toolOverrides?.id !== void 0 && {
8157
+ toolOverrideVariantId: toolOverrides.id
8158
+ },
8159
+ ...mcpHostModel !== void 0 && { mcpHostModel },
8160
+ ...judgeModel !== void 0 && { judgeModel },
8161
+ ...gitHash !== void 0 && { gitHash },
8162
+ packageVersion: package_default.version
8163
+ }
8164
+ });
8165
+ }
7858
8166
  }
7859
8167
  if (context.testInfo) {
7860
8168
  await context.testInfo.attach("mcp-test-results", {
7861
8169
  contentType: "application/json",
7862
8170
  body: Buffer.from(JSON.stringify({ caseResults }))
7863
8171
  });
7864
- } else if (caseResults.length > 0) {
8172
+ } else if (caseResults.length > 0 && !warnedNoTestInfo) {
8173
+ warnedNoTestInfo = true;
7865
8174
  console.warn(
7866
8175
  "[mcp-server-tester] runEvalDataset: testInfo not provided \u2014 results will not appear in the MCP reporter.\nTo enable reporting, pass testInfo from the Playwright test function:\n await runEvalDataset({ dataset }, { mcp, testInfo });"
7867
8176
  );
7868
8177
  }
7869
8178
  return result;
7870
8179
  }
8180
+ async function loadStoredBaseline(baselineResultsFrom, resultStore) {
8181
+ if (!resultStore) {
8182
+ throw new Error("resultStore is required for store-backed baselines");
8183
+ }
8184
+ const store = resolveEvalResultStore(resultStore);
8185
+ const artifact = baselineResultsFrom.ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8186
+ "eval-runner-result",
8187
+ baselineResultsFrom.ref.id
8188
+ );
8189
+ if (!artifact) {
8190
+ throw new Error("No latest eval run artifact found");
8191
+ }
8192
+ return artifact.data;
8193
+ }
8194
+ async function saveStoredEvalResult(result, saveResultsTo, options) {
8195
+ if (!options.resultStore) {
8196
+ throw new Error("resultStore is required for store-backed saves");
8197
+ }
8198
+ const store = resolveEvalResultStore(options.resultStore);
8199
+ const data = options.omitResponses ? omitResponsesFromResult(result) : result;
8200
+ const id = saveResultsTo.ref && saveResultsTo.ref !== "latest" ? saveResultsTo.ref.id : void 0;
8201
+ await store.saveArtifact(
8202
+ createStoredEvalArtifact({
8203
+ kind: "eval-runner-result",
8204
+ id,
8205
+ data,
8206
+ metadata: options.metadata
8207
+ })
8208
+ );
8209
+ }
8210
+ function omitResponsesFromResult(result) {
8211
+ return {
8212
+ ...result,
8213
+ caseResults: result.caseResults.map(
8214
+ ({ response: _response, ...rest }) => rest
8215
+ )
8216
+ };
8217
+ }
8218
+ function formatBaselineRef(baselineResultsFrom) {
8219
+ if (typeof baselineResultsFrom === "string") {
8220
+ return baselineResultsFrom;
8221
+ }
8222
+ return baselineResultsFrom.ref === "latest" ? "resultStore latest" : `resultStore ${baselineResultsFrom.ref.id}`;
8223
+ }
7871
8224
 
7872
8225
  // src/evals/serverComparison.ts
7873
8226
  async function runServerComparison(options, contextA, contextB) {
@@ -7909,7 +8262,7 @@ async function runServerComparison(options, contextA, contextB) {
7909
8262
  }
7910
8263
  const total = cases.length;
7911
8264
  const decidedCases = aWins + bWins + ties;
7912
- return {
8265
+ const comparison = {
7913
8266
  dataset: options.dataset.name,
7914
8267
  total,
7915
8268
  aWins,
@@ -7926,6 +8279,366 @@ async function runServerComparison(options, contextA, contextB) {
7926
8279
  serverBResult: resultB,
7927
8280
  durationMs: Date.now() - startTime
7928
8281
  };
8282
+ if (options.comparisonStore) {
8283
+ await saveServerComparison({
8284
+ store: options.comparisonStore,
8285
+ comparison,
8286
+ id: options.comparisonId,
8287
+ metadata: {
8288
+ datasetName: options.dataset.name,
8289
+ ...options.comparisonMetadata ?? {}
8290
+ },
8291
+ redactStoredResponses: options.redactStoredResponses
8292
+ });
8293
+ }
8294
+ return comparison;
8295
+ }
8296
+ async function saveServerComparison(options) {
8297
+ const store = resolveEvalResultStore(options.store);
8298
+ const data = options.redactStoredResponses ? redactResponses(options.comparison) : options.comparison;
8299
+ const artifact = createStoredEvalArtifact({
8300
+ kind: "server-comparison",
8301
+ id: options.id,
8302
+ data,
8303
+ metadata: {
8304
+ datasetName: options.comparison.dataset,
8305
+ ...options.metadata ?? {}
8306
+ }
8307
+ });
8308
+ await store.saveArtifact(artifact);
8309
+ return artifact;
8310
+ }
8311
+ function redactResponses(value) {
8312
+ return JSON.parse(
8313
+ JSON.stringify(
8314
+ value,
8315
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8316
+ )
8317
+ );
8318
+ }
8319
+
8320
+ // src/evals/evalRunComparison.ts
8321
+ function compareEvalRuns(options) {
8322
+ const { baseline, candidate, labels } = options;
8323
+ const candidateMap = new Map(
8324
+ candidate.caseResults.map((result) => [result.id, result])
8325
+ );
8326
+ const cases = [];
8327
+ const seenIds = /* @__PURE__ */ new Set();
8328
+ for (const baselineCase of baseline.caseResults) {
8329
+ seenIds.add(baselineCase.id);
8330
+ const candidateCase = candidateMap.get(baselineCase.id);
8331
+ if (!candidateCase) {
8332
+ cases.push({
8333
+ id: baselineCase.id,
8334
+ outcome: "MISSING_FROM_CANDIDATE",
8335
+ baseline: baselineCase
8336
+ });
8337
+ continue;
8338
+ }
8339
+ cases.push({
8340
+ id: baselineCase.id,
8341
+ outcome: compareCaseOutcome(baselineCase.pass, candidateCase.pass),
8342
+ baseline: baselineCase,
8343
+ candidate: candidateCase
8344
+ });
8345
+ }
8346
+ for (const candidateCase of candidate.caseResults) {
8347
+ if (seenIds.has(candidateCase.id)) {
8348
+ continue;
8349
+ }
8350
+ cases.push({
8351
+ id: candidateCase.id,
8352
+ outcome: "MISSING_FROM_BASELINE",
8353
+ candidate: candidateCase
8354
+ });
8355
+ }
8356
+ const baselinePassRate = passRate(baseline);
8357
+ const candidatePassRate = passRate(candidate);
8358
+ return {
8359
+ baselineLabel: labels?.baseline ?? "baseline",
8360
+ candidateLabel: labels?.candidate ?? candidate.metadata?.toolOverrideVariantId ?? "candidate",
8361
+ baselinePassRate,
8362
+ candidatePassRate,
8363
+ deltaPassRate: candidatePassRate - baselinePassRate,
8364
+ ...metricDelta(
8365
+ "ToolPrecision",
8366
+ baseline.datasetToolPrecision,
8367
+ candidate.datasetToolPrecision
8368
+ ),
8369
+ ...metricDelta(
8370
+ "ToolRecall",
8371
+ baseline.datasetToolRecall,
8372
+ candidate.datasetToolRecall
8373
+ ),
8374
+ ...metricDelta("ToolF1", baseline.datasetToolF1, candidate.datasetToolF1),
8375
+ cases,
8376
+ improvedCases: cases.filter((c) => c.outcome === "IMPROVED"),
8377
+ regressedCases: cases.filter((c) => c.outcome === "REGRESSED"),
8378
+ unchangedPasses: cases.filter((c) => c.outcome === "UNCHANGED_PASS"),
8379
+ unchangedFailures: cases.filter((c) => c.outcome === "UNCHANGED_FAIL"),
8380
+ missingFromBaseline: cases.filter(
8381
+ (c) => c.outcome === "MISSING_FROM_BASELINE"
8382
+ ),
8383
+ missingFromCandidate: cases.filter(
8384
+ (c) => c.outcome === "MISSING_FROM_CANDIDATE"
8385
+ )
8386
+ };
8387
+ }
8388
+ async function loadStoredEvalRunnerResult(storeLike, ref) {
8389
+ const store = resolveEvalResultStore(storeLike);
8390
+ const artifact = ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8391
+ "eval-runner-result",
8392
+ ref.id
8393
+ );
8394
+ if (!artifact) {
8395
+ throw new Error("No latest eval run artifact found");
8396
+ }
8397
+ return artifact;
8398
+ }
8399
+ async function saveEvalRunComparison(options) {
8400
+ const store = resolveEvalResultStore(options.store);
8401
+ const data = options.redactStoredResponses ? redactResponses2(options.comparison) : options.comparison;
8402
+ const artifact = createStoredEvalArtifact({
8403
+ kind: "eval-run-comparison",
8404
+ id: options.id,
8405
+ data,
8406
+ metadata: {
8407
+ labels: {
8408
+ baseline: options.comparison.baselineLabel,
8409
+ candidate: options.comparison.candidateLabel
8410
+ },
8411
+ ...options.metadata ?? {}
8412
+ }
8413
+ });
8414
+ await store.saveArtifact(artifact);
8415
+ return artifact;
8416
+ }
8417
+ function compareCaseOutcome(baselinePass, candidatePass) {
8418
+ if (!baselinePass && candidatePass) return "IMPROVED";
8419
+ if (baselinePass && !candidatePass) return "REGRESSED";
8420
+ return baselinePass ? "UNCHANGED_PASS" : "UNCHANGED_FAIL";
8421
+ }
8422
+ function passRate(result) {
8423
+ return result.total > 0 ? result.passed / result.total : 0;
8424
+ }
8425
+ function metricDelta(name15, baselineValue, candidateValue) {
8426
+ const result = {};
8427
+ if (baselineValue !== void 0) {
8428
+ result[`baseline${name15}`] = baselineValue;
8429
+ }
8430
+ if (candidateValue !== void 0) {
8431
+ result[`candidate${name15}`] = candidateValue;
8432
+ }
8433
+ if (baselineValue !== void 0 && candidateValue !== void 0) {
8434
+ result[`delta${name15}`] = candidateValue - baselineValue;
8435
+ }
8436
+ return result;
8437
+ }
8438
+ function redactResponses2(value) {
8439
+ return JSON.parse(
8440
+ JSON.stringify(
8441
+ value,
8442
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8443
+ )
8444
+ );
8445
+ }
8446
+
8447
+ // src/evals/variantExperiment.ts
8448
+ async function runVariantExperiment(options, context) {
8449
+ const metric = options.metric ?? "passRate";
8450
+ const maxRounds = options.maxRounds ?? 1;
8451
+ const minImprovement = options.minImprovement ?? 0;
8452
+ const allowRegressions = options.allowRegressions ?? false;
8453
+ const internalContext = {
8454
+ mcp: context.mcp,
8455
+ expect: context.expect
8456
+ };
8457
+ const baseline = await runEvalDataset(
8458
+ buildRunOptions(options, void 0),
8459
+ internalContext
8460
+ );
8461
+ const baselineValue = readMetric(baseline, metric);
8462
+ if (baselineValue === void 0) {
8463
+ throw new Error(
8464
+ `Metric '${metric}' is unavailable: the dataset produced no tool precision/recall data. Add mcp_host cases with toolsTriggered expectations, or use metric 'passRate'.`
8465
+ );
8466
+ }
8467
+ const rounds = [];
8468
+ let bestSoFar;
8469
+ let bestAttempted;
8470
+ let reason = "max-rounds";
8471
+ for (let round = 0; round < maxRounds; round++) {
8472
+ const variants = await gatherVariants(options, {
8473
+ round,
8474
+ baseline,
8475
+ metric,
8476
+ history: rounds,
8477
+ bestSoFar
8478
+ });
8479
+ if (variants.length === 0) {
8480
+ reason = round === 0 ? "no-variants" : "no-improvement";
8481
+ break;
8482
+ }
8483
+ const candidates = [];
8484
+ for (const variant of variants) {
8485
+ const candidate = await scoreVariant(
8486
+ options,
8487
+ internalContext,
8488
+ baseline,
8489
+ baselineValue,
8490
+ metric,
8491
+ allowRegressions,
8492
+ variant
8493
+ );
8494
+ candidates.push(candidate);
8495
+ bestAttempted = pickBetter(bestAttempted, candidate, true);
8496
+ }
8497
+ const roundBest = candidates.reduce(
8498
+ (best, candidate) => pickBetter(best, candidate, false),
8499
+ void 0
8500
+ );
8501
+ rounds.push({ round, candidates, best: roundBest });
8502
+ if (roundBest) {
8503
+ const improvement = roundBest.metricValue - (bestSoFar?.metricValue ?? baselineValue);
8504
+ bestSoFar = pickBetter(bestSoFar, roundBest, false);
8505
+ if (improvement < minImprovement) {
8506
+ reason = "no-improvement";
8507
+ break;
8508
+ }
8509
+ }
8510
+ }
8511
+ const winner = bestSoFar;
8512
+ const proposalSource = winner ?? bestAttempted;
8513
+ const proposal = proposalSource ? buildProposal(metric, baselineValue, proposalSource, winner !== void 0) : void 0;
8514
+ const result = {
8515
+ metric,
8516
+ baseline,
8517
+ rounds,
8518
+ winner,
8519
+ proposal,
8520
+ converged: true,
8521
+ reason
8522
+ };
8523
+ if (context.testInfo) {
8524
+ const surfaceRun = winner?.result ?? bestAttempted?.result ?? baseline;
8525
+ await context.testInfo.attach("mcp-test-results", {
8526
+ contentType: "application/json",
8527
+ body: Buffer.from(
8528
+ JSON.stringify({ caseResults: surfaceRun.caseResults })
8529
+ )
8530
+ });
8531
+ await context.testInfo.attach("mcp-variant-experiment", {
8532
+ contentType: "application/json",
8533
+ body: Buffer.from(
8534
+ JSON.stringify(buildExperimentData(result, baselineValue))
8535
+ )
8536
+ });
8537
+ }
8538
+ return result;
8539
+ }
8540
+ function buildExperimentData(result, baselineValue) {
8541
+ return {
8542
+ metric: result.metric,
8543
+ baselineValue,
8544
+ bestValue: result.winner?.metricValue ?? result.proposal?.candidateValue ?? baselineValue,
8545
+ rounds: result.rounds.map((round) => {
8546
+ const best = round.best ?? round.candidates[0];
8547
+ return {
8548
+ round: round.round,
8549
+ variantId: best?.variant.id ?? "(none)",
8550
+ metricValue: best?.metricValue ?? baselineValue,
8551
+ metricDelta: best?.metricDelta ?? 0,
8552
+ disqualified: best?.disqualified ?? false
8553
+ };
8554
+ }),
8555
+ winnerVariantId: result.winner?.variant.id,
8556
+ recommendation: result.proposal?.recommendation,
8557
+ reason: result.reason
8558
+ };
8559
+ }
8560
+ async function gatherVariants(options, context) {
8561
+ if (context.round === 0 && options.variants && options.variants.length > 0) {
8562
+ return options.variants;
8563
+ }
8564
+ if (options.proposeVariants) {
8565
+ return options.proposeVariants(context);
8566
+ }
8567
+ return [];
8568
+ }
8569
+ async function scoreVariant(options, context, baseline, baselineValue, metric, allowRegressions, variant) {
8570
+ const result = await runEvalDataset(
8571
+ buildRunOptions(options, variant),
8572
+ context
8573
+ );
8574
+ const comparison = compareEvalRuns({
8575
+ baseline,
8576
+ candidate: result,
8577
+ labels: { candidate: variant.id }
8578
+ });
8579
+ const metricValue = readMetric(result, metric) ?? baselineValue;
8580
+ const disqualified = !allowRegressions && comparison.regressedCases.length > 0;
8581
+ return {
8582
+ variant,
8583
+ result,
8584
+ comparison,
8585
+ metricValue,
8586
+ metricDelta: metricValue - baselineValue,
8587
+ disqualified
8588
+ };
8589
+ }
8590
+ function pickBetter(incumbent, challenger, includeDisqualified) {
8591
+ if (!includeDisqualified && challenger.disqualified) {
8592
+ return incumbent;
8593
+ }
8594
+ if (!incumbent) {
8595
+ return challenger;
8596
+ }
8597
+ return challenger.metricValue > incumbent.metricValue ? challenger : incumbent;
8598
+ }
8599
+ function buildProposal(metric, baselineValue, source, isWinner) {
8600
+ let recommendation;
8601
+ if (isWinner) {
8602
+ recommendation = source.metricDelta > 0 ? "apply" : "inconclusive";
8603
+ } else {
8604
+ recommendation = source.disqualified ? "reject" : "inconclusive";
8605
+ }
8606
+ return {
8607
+ variantId: source.variant.id,
8608
+ metric,
8609
+ baselineValue,
8610
+ candidateValue: source.metricValue,
8611
+ delta: source.metricDelta,
8612
+ toolChanges: source.variant.tools,
8613
+ improvedCaseIds: source.comparison.improvedCases.map((c) => c.id),
8614
+ regressedCaseIds: source.comparison.regressedCases.map((c) => c.id),
8615
+ recommendation
8616
+ };
8617
+ }
8618
+ function readMetric(result, metric) {
8619
+ switch (metric) {
8620
+ case "passRate":
8621
+ return result.total > 0 ? result.passed / result.total : 0;
8622
+ case "toolF1":
8623
+ return result.datasetToolF1;
8624
+ case "toolPrecision":
8625
+ return result.datasetToolPrecision;
8626
+ case "toolRecall":
8627
+ return result.datasetToolRecall;
8628
+ }
8629
+ }
8630
+ function buildRunOptions(options, toolOverrides) {
8631
+ return {
8632
+ dataset: options.dataset,
8633
+ toolOverrides,
8634
+ defaultLlmIterations: options.defaultLlmIterations,
8635
+ defaultJudgeReps: options.defaultJudgeReps,
8636
+ concurrency: options.concurrency,
8637
+ filterTags: options.filterTags,
8638
+ schemas: options.schemas,
8639
+ mcpHostModel: options.mcpHostModel,
8640
+ judgeModel: options.judgeModel
8641
+ };
7929
8642
  }
7930
8643
 
7931
8644
  // src/spec/conformanceChecks.ts
@@ -8105,15 +8818,22 @@ exports.DiscoveryError = DiscoveryError;
8105
8818
  exports.ENV_VAR_NAMES = ENV_VAR_NAMES;
8106
8819
  exports.EvalCaseSchema = EvalCaseSchema;
8107
8820
  exports.EvalDatasetSchema = EvalDatasetSchema;
8821
+ exports.FileEvalResultStore = FileEvalResultStore;
8822
+ exports.GCSEvalResultStore = GCSEvalResultStore;
8108
8823
  exports.MCPConfigSchema = MCPConfigSchema;
8109
8824
  exports.MCP_PROTOCOL_VERSION = MCP_PROTOCOL_VERSION;
8110
8825
  exports.SnapshotSanitizers = SnapshotSanitizers;
8111
8826
  exports.clearJudgeRegistry = clearJudgeRegistry;
8112
8827
  exports.closeMCPClient = closeMCPClient;
8828
+ exports.compareEvalRuns = compareEvalRuns;
8829
+ exports.createDefaultArtifactId = createDefaultArtifactId;
8830
+ exports.createEvalResultStore = createEvalResultStore;
8113
8831
  exports.createJudge = createJudge;
8114
8832
  exports.createMCPClientForConfig = createMCPClientForConfig;
8115
8833
  exports.createMCPFixture = createMCPFixture;
8834
+ exports.createStoredEvalArtifact = createStoredEvalArtifact;
8116
8835
  exports.createTokenAuthHeaders = createTokenAuthHeaders;
8836
+ exports.defaultEnvironmentMetadata = defaultEnvironmentMetadata;
8117
8837
  exports.discoverAuthorizationServer = discoverAuthorizationServer;
8118
8838
  exports.discoverProtectedResource = discoverProtectedResource;
8119
8839
  exports.expect = expect;
@@ -8124,6 +8844,7 @@ exports.getResponseSizeBytes = getResponseSizeBytes;
8124
8844
  exports.hasValidTokens = hasValidTokens;
8125
8845
  exports.injectTokens = injectTokens;
8126
8846
  exports.isBuiltInRubric = isBuiltInRubric;
8847
+ exports.isEvalResultStore = isEvalResultStore;
8127
8848
  exports.isHttpConfig = isHttpConfig;
8128
8849
  exports.isProviderAvailable = isProviderAvailable;
8129
8850
  exports.isStdioConfig = isStdioConfig;
@@ -8132,6 +8853,7 @@ exports.isTokenExpiringSoon = isTokenExpiringSoon;
8132
8853
  exports.loadBaseline = loadBaseline;
8133
8854
  exports.loadEvalDataset = loadEvalDataset;
8134
8855
  exports.loadEvalDatasetFromObject = loadEvalDatasetFromObject;
8856
+ exports.loadStoredEvalRunnerResult = loadStoredEvalRunnerResult;
8135
8857
  exports.loadTokens = loadTokens;
8136
8858
  exports.loadTokensFromEnv = loadTokensFromEnv;
8137
8859
  exports.mcpAuthTest = test2;
@@ -8142,12 +8864,16 @@ exports.performOAuthSetup = performOAuthSetup;
8142
8864
  exports.performOAuthSetupIfNeeded = performOAuthSetupIfNeeded;
8143
8865
  exports.refreshAccessToken = refreshAccessToken;
8144
8866
  exports.registerJudge = registerJudge;
8867
+ exports.resolveEvalResultStore = resolveEvalResultStore;
8145
8868
  exports.resolveRubric = resolveRubric;
8146
8869
  exports.runConformanceChecks = runConformanceChecks;
8147
8870
  exports.runEvalCase = runEvalCase;
8148
8871
  exports.runEvalDataset = runEvalDataset;
8149
8872
  exports.runServerComparison = runServerComparison;
8873
+ exports.runVariantExperiment = runVariantExperiment;
8150
8874
  exports.saveBaseline = saveBaseline;
8875
+ exports.saveEvalRunComparison = saveEvalRunComparison;
8876
+ exports.saveServerComparison = saveServerComparison;
8151
8877
  exports.simulateMCPHost = simulateMCPHost;
8152
8878
  exports.test = test;
8153
8879
  exports.validateAccessToken = validateAccessToken;