@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.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.1"};
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) {
@@ -6811,8 +6811,8 @@ Hint: run \`getMissingDependencyMessage('${provider}')\` or check docs/mcp-host.
6811
6811
  Hint: check your API key environment variable (e.g. ANTHROPIC_API_KEY, GOOGLE_APPLICATION_CREDENTIALS).`;
6812
6812
  }
6813
6813
  if (raw.includes("404") || raw.includes("Not Found") || raw.toLowerCase().includes("model") && raw.toLowerCase().includes("not found")) {
6814
- return `MCP host simulation failed: model not found.
6815
- Hint: check the model name format for your provider. For vertex-anthropic use 'claude-3-5-haiku@20241022' (with @).`;
6814
+ return `MCP host simulation failed: ${raw}
6815
+ 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.`;
6816
6816
  }
6817
6817
  if (raw.includes("ENOTFOUND") || raw.includes("fetch failed") || raw.includes("ECONNREFUSED")) {
6818
6818
  return `MCP host simulation failed: network error.
@@ -6910,12 +6910,18 @@ function createVercelOrchestrator() {
6910
6910
  tools[toolName] = {
6911
6911
  description: mcpTool.description ?? "",
6912
6912
  inputSchema: jsonSchema2(rawSchema),
6913
- execute: async (args) => {
6913
+ execute: async (args, opts) => {
6914
6914
  const mcpStart = Date.now();
6915
6915
  const result2 = await mcp.callTool(toolName, args);
6916
6916
  mcpDurationMs += Date.now() - mcpStart;
6917
- allToolCalls.push({ name: toolName, arguments: args });
6918
- return extractText(result2);
6917
+ const output = extractText(result2);
6918
+ allToolCalls.push({
6919
+ id: opts?.toolCallId,
6920
+ name: toolName,
6921
+ arguments: args,
6922
+ output
6923
+ });
6924
+ return output;
6919
6925
  }
6920
6926
  };
6921
6927
  }
@@ -6937,10 +6943,17 @@ function createVercelOrchestrator() {
6937
6943
  totalCostUsd: 0,
6938
6944
  durationMs: llmDurationMs
6939
6945
  } : void 0;
6940
- const conversationHistory = (result.steps ?? []).map((step) => ({
6941
- role: step.toolCalls?.length > 0 ? "tool" : "assistant",
6942
- content: step.toolCalls?.length > 0 ? JSON.stringify(step.toolResults) : step.text ?? ""
6943
- }));
6946
+ const conversationHistory = (result.steps ?? []).flatMap((step) => {
6947
+ if (step.toolCalls?.length > 0) {
6948
+ return step.toolCalls.map(
6949
+ (tc) => ({
6950
+ role: "tool",
6951
+ toolCallId: tc.toolCallId
6952
+ })
6953
+ );
6954
+ }
6955
+ return step.text ? [{ role: "assistant", content: step.text }] : [];
6956
+ });
6944
6957
  return {
6945
6958
  success: true,
6946
6959
  toolCalls: allToolCalls,
@@ -6967,6 +6980,7 @@ function parseStreamJson(stdout) {
6967
6980
  const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
6968
6981
  const toolCalls = [];
6969
6982
  const textParts = [];
6983
+ let usage;
6970
6984
  const conversationHistory = [];
6971
6985
  for (const line of lines) {
6972
6986
  let event;
@@ -6995,20 +7009,38 @@ function parseStreamJson(stdout) {
6995
7009
  for (const block of event.message.content) {
6996
7010
  if (block.type === "tool_result") {
6997
7011
  const content = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
6998
- conversationHistory.push({ role: "tool", content });
7012
+ const call = block.tool_use_id ? toolCalls.find((tc) => tc.id === block.tool_use_id) : void 0;
7013
+ if (call) {
7014
+ call.output = content;
7015
+ conversationHistory.push({ role: "tool", toolCallId: call.id });
7016
+ } else {
7017
+ conversationHistory.push({ role: "tool", content });
7018
+ }
6999
7019
  }
7000
7020
  }
7001
7021
  }
7002
- if (event.type === "result" && typeof event.result === "string") {
7003
- if (textParts.length === 0) {
7022
+ if (event.type === "result") {
7023
+ if (typeof event.result === "string" && textParts.length === 0) {
7004
7024
  textParts.push(event.result);
7005
7025
  }
7026
+ if (event.usage) {
7027
+ usage = {
7028
+ inputTokens: event.usage.input_tokens ?? 0,
7029
+ outputTokens: event.usage.output_tokens ?? 0,
7030
+ totalCostUsd: event.total_cost_usd ?? 0,
7031
+ durationMs: event.duration_ms ?? 0,
7032
+ durationApiMs: event.duration_api_ms,
7033
+ cacheReadInputTokens: event.usage.cache_read_input_tokens,
7034
+ cacheCreationInputTokens: event.usage.cache_creation_input_tokens
7035
+ };
7036
+ }
7006
7037
  }
7007
7038
  if (event.type === "result" && event.is_error === true) {
7008
7039
  return {
7009
7040
  success: false,
7010
7041
  toolCalls,
7011
- error: typeof event.result === "string" ? event.result : "CLI host reported an error"
7042
+ error: typeof event.result === "string" ? event.result : "CLI host reported an error",
7043
+ usage
7012
7044
  };
7013
7045
  }
7014
7046
  }
@@ -7020,7 +7052,8 @@ function parseStreamJson(stdout) {
7020
7052
  success: true,
7021
7053
  toolCalls,
7022
7054
  response: response || void 0,
7023
- conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0
7055
+ conversationHistory: conversationHistory.length > 0 ? conversationHistory : void 0,
7056
+ usage
7024
7057
  };
7025
7058
  }
7026
7059
  function createJsonParser(paths) {
@@ -7266,6 +7299,216 @@ function buildBaselinePassMap(baseline) {
7266
7299
  }
7267
7300
  return map;
7268
7301
  }
7302
+ var KIND_DIRS = {
7303
+ "eval-runner-result": "eval-runs",
7304
+ "reporter-run": "reporter-runs",
7305
+ "eval-run-comparison": "comparisons/eval-runs",
7306
+ "server-comparison": "comparisons/servers"
7307
+ };
7308
+ function createEvalResultStore(config) {
7309
+ if (config.provider === "file") {
7310
+ return new FileEvalResultStore(config);
7311
+ }
7312
+ return new GCSEvalResultStore(config);
7313
+ }
7314
+ function resolveEvalResultStore(store) {
7315
+ return isEvalResultStore(store) ? store : createEvalResultStore(store);
7316
+ }
7317
+ function isEvalResultStore(value) {
7318
+ return typeof value === "object" && value !== null && "saveArtifact" in value && "loadArtifact" in value && "loadLatestArtifact" in value && "listArtifacts" in value;
7319
+ }
7320
+ function createStoredEvalArtifact(options) {
7321
+ const createdAt = options.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
7322
+ return {
7323
+ schemaVersion: 1,
7324
+ kind: options.kind,
7325
+ id: options.id ?? createDefaultArtifactId(createdAt),
7326
+ createdAt,
7327
+ metadata: {
7328
+ ...defaultEnvironmentMetadata(),
7329
+ ...options.metadata ?? {}
7330
+ },
7331
+ data: options.data
7332
+ };
7333
+ }
7334
+ function createDefaultArtifactId(timestamp = (/* @__PURE__ */ new Date()).toISOString()) {
7335
+ const safeTimestamp = timestamp.replace(/[:.]/g, "-");
7336
+ const runNumber = process.env.GITHUB_RUN_NUMBER;
7337
+ const sha = process.env.GITHUB_SHA?.slice(0, 12);
7338
+ const suffix = runNumber ?? sha;
7339
+ return suffix ? `${safeTimestamp}-${suffix}` : safeTimestamp;
7340
+ }
7341
+ function defaultEnvironmentMetadata() {
7342
+ return {
7343
+ ...process.env.GITHUB_SHA !== void 0 && {
7344
+ gitHash: process.env.GITHUB_SHA
7345
+ },
7346
+ ...process.env.GITHUB_REF_NAME !== void 0 && {
7347
+ branch: process.env.GITHUB_REF_NAME
7348
+ },
7349
+ ...process.env.GITHUB_RUN_NUMBER !== void 0 && {
7350
+ runNumber: process.env.GITHUB_RUN_NUMBER
7351
+ },
7352
+ ...process.env.GITHUB_EVENT_NAME !== void 0 && {
7353
+ trigger: process.env.GITHUB_EVENT_NAME
7354
+ }
7355
+ };
7356
+ }
7357
+ var FileEvalResultStore = class {
7358
+ dir;
7359
+ constructor(config) {
7360
+ this.dir = config.dir;
7361
+ }
7362
+ async saveArtifact(artifact) {
7363
+ const artifactDir = join(this.dir, KIND_DIRS[artifact.kind]);
7364
+ await mkdir(artifactDir, { recursive: true });
7365
+ const serialized = JSON.stringify(artifact, null, 2);
7366
+ await writeFile(
7367
+ join(artifactDir, `${artifact.id}.json`),
7368
+ serialized,
7369
+ "utf8"
7370
+ );
7371
+ await writeFile(join(artifactDir, "latest.json"), serialized, "utf8");
7372
+ }
7373
+ async loadArtifact(kind, id) {
7374
+ const raw = await readFile(
7375
+ join(this.dir, KIND_DIRS[kind], `${id}.json`),
7376
+ "utf8"
7377
+ );
7378
+ return JSON.parse(raw);
7379
+ }
7380
+ async loadLatestArtifact(kind) {
7381
+ try {
7382
+ const raw = await readFile(
7383
+ join(this.dir, KIND_DIRS[kind], "latest.json"),
7384
+ "utf8"
7385
+ );
7386
+ return JSON.parse(raw);
7387
+ } catch (error) {
7388
+ if (isMissingFileError(error)) {
7389
+ return null;
7390
+ }
7391
+ throw error;
7392
+ }
7393
+ }
7394
+ async listArtifacts(kind, options = {}) {
7395
+ let files;
7396
+ try {
7397
+ files = await readdir(join(this.dir, KIND_DIRS[kind]));
7398
+ } catch (error) {
7399
+ if (isMissingFileError(error)) {
7400
+ return [];
7401
+ }
7402
+ throw error;
7403
+ }
7404
+ const summaries = await Promise.all(
7405
+ files.filter((f) => f.endsWith(".json") && f !== "latest.json").map(async (file) => {
7406
+ const raw = await readFile(
7407
+ join(this.dir, KIND_DIRS[kind], file),
7408
+ "utf8"
7409
+ );
7410
+ return toSummary(JSON.parse(raw));
7411
+ })
7412
+ );
7413
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7414
+ }
7415
+ };
7416
+ var GCSEvalResultStore = class {
7417
+ bucketName;
7418
+ prefix;
7419
+ storage;
7420
+ constructor(config) {
7421
+ this.bucketName = config.bucket;
7422
+ this.prefix = trimSlashes(config.prefix ?? "");
7423
+ }
7424
+ async saveArtifact(artifact) {
7425
+ const bucket = await this.getBucket();
7426
+ const serialized = JSON.stringify(artifact, null, 2);
7427
+ await bucket.file(
7428
+ this.objectPath(
7429
+ artifact.kind,
7430
+ `${encodeURIComponent(artifact.id)}.json`
7431
+ )
7432
+ ).save(serialized, {
7433
+ contentType: "application/json",
7434
+ resumable: false,
7435
+ validation: false
7436
+ });
7437
+ await bucket.file(this.objectPath(artifact.kind, "latest.json")).save(serialized, {
7438
+ contentType: "application/json",
7439
+ resumable: false,
7440
+ validation: false
7441
+ });
7442
+ }
7443
+ async loadArtifact(kind, id) {
7444
+ const bucket = await this.getBucket();
7445
+ const file = bucket.file(
7446
+ this.objectPath(kind, `${encodeURIComponent(id)}.json`)
7447
+ );
7448
+ const [buffer] = await file.download();
7449
+ return JSON.parse(buffer.toString("utf8"));
7450
+ }
7451
+ async loadLatestArtifact(kind) {
7452
+ const bucket = await this.getBucket();
7453
+ const file = bucket.file(this.objectPath(kind, "latest.json"));
7454
+ const [exists] = await file.exists();
7455
+ if (!exists) return null;
7456
+ const [buffer] = await file.download();
7457
+ return JSON.parse(buffer.toString("utf8"));
7458
+ }
7459
+ async listArtifacts(kind, options = {}) {
7460
+ const bucket = await this.getBucket();
7461
+ const [files] = await bucket.getFiles({
7462
+ prefix: this.objectPath(kind, "")
7463
+ });
7464
+ const summaries = await Promise.all(
7465
+ files.filter(
7466
+ (f) => f.name.endsWith(".json") && !f.name.endsWith("/latest.json")
7467
+ ).map(async (file) => {
7468
+ const [buffer] = await file.download();
7469
+ return toSummary(
7470
+ JSON.parse(buffer.toString("utf8"))
7471
+ );
7472
+ })
7473
+ );
7474
+ return summaries.sort((a, b) => b.createdAt.localeCompare(a.createdAt)).slice(0, options.limit);
7475
+ }
7476
+ async getBucket() {
7477
+ if (!this.storage) {
7478
+ const moduleName = "@google-cloud/storage";
7479
+ let Storage;
7480
+ try {
7481
+ const mod = await import(moduleName);
7482
+ Storage = mod.Storage;
7483
+ } catch (error) {
7484
+ throw new Error(
7485
+ `GCS result storage requires the optional \`@google-cloud/storage\` package. Install it and authenticate with Application Default Credentials via GOOGLE_APPLICATION_CREDENTIALS.
7486
+ Original error: ${error instanceof Error ? error.message : String(error)}`
7487
+ );
7488
+ }
7489
+ this.storage = new Storage();
7490
+ }
7491
+ return this.storage.bucket(this.bucketName);
7492
+ }
7493
+ objectPath(kind, filename) {
7494
+ const parts = [this.prefix, KIND_DIRS[kind], filename].filter(Boolean);
7495
+ return parts.join("/");
7496
+ }
7497
+ };
7498
+ function toSummary(artifact) {
7499
+ return {
7500
+ kind: artifact.kind,
7501
+ id: artifact.id,
7502
+ createdAt: artifact.createdAt,
7503
+ metadata: artifact.metadata
7504
+ };
7505
+ }
7506
+ function trimSlashes(value) {
7507
+ return value.replace(/^\/+|\/+$/g, "");
7508
+ }
7509
+ function isMissingFileError(error) {
7510
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
7511
+ }
7269
7512
  var execFileAsync = promisify(execFile);
7270
7513
  async function execFileNoThrow(file, args) {
7271
7514
  try {
@@ -7312,6 +7555,41 @@ function sumUsage(a, b) {
7312
7555
  }
7313
7556
 
7314
7557
  // src/evals/evalRunner.ts
7558
+ function createToolOverrideMCP(mcp, variant) {
7559
+ return {
7560
+ ...mcp,
7561
+ async listTools() {
7562
+ const tools = await mcp.listTools();
7563
+ const knownToolNames = new Set(tools.map((tool2) => tool2.name));
7564
+ const unknownToolNames = Object.keys(variant.tools).filter(
7565
+ (name15) => !knownToolNames.has(name15)
7566
+ );
7567
+ if (unknownToolNames.length > 0) {
7568
+ throw new Error(
7569
+ `[mcp-server-tester] toolOverrides variant "${variant.id}" references unknown tool(s): ` + unknownToolNames.join(", ")
7570
+ );
7571
+ }
7572
+ return tools.map((tool2) => {
7573
+ const override = variant.tools[tool2.name];
7574
+ if (!override) {
7575
+ return tool2;
7576
+ }
7577
+ return {
7578
+ ...tool2,
7579
+ ...override.description !== void 0 && {
7580
+ description: override.description
7581
+ },
7582
+ ...override.inputSchema !== void 0 && {
7583
+ inputSchema: override.inputSchema
7584
+ }
7585
+ };
7586
+ });
7587
+ },
7588
+ async callTool(name15, args) {
7589
+ return mcp.callTool(name15, args);
7590
+ }
7591
+ };
7592
+ }
7315
7593
  async function executeToolCall(evalCase, mcp) {
7316
7594
  const mode = evalCase.mode || "direct";
7317
7595
  try {
@@ -7496,9 +7774,12 @@ async function runExpectBlockValidations(expectBlock, response, config) {
7496
7774
  }
7497
7775
  return { expectations: results, toolPrecision, toolRecall };
7498
7776
  }
7499
- function buildRequest(evalCase) {
7777
+ function buildRequest(evalCase, toolOverrideVariantId) {
7500
7778
  const request = {};
7501
7779
  if (evalCase.description) request.description = evalCase.description;
7780
+ if (toolOverrideVariantId !== void 0) {
7781
+ request.toolOverrideVariantId = toolOverrideVariantId;
7782
+ }
7502
7783
  if (evalCase.mode === "mcp_host") {
7503
7784
  if (evalCase.scenario) request.scenario = evalCase.scenario;
7504
7785
  if (evalCase.mcpHostConfig) {
@@ -7563,7 +7844,7 @@ async function runSingleIteration(evalCase, context, options) {
7563
7844
  toolName: evalCase.scenario != null ? "mcp_host" : evalCase.toolName ?? "unknown",
7564
7845
  source: "eval",
7565
7846
  pass: didCasePass(error, expectationResults),
7566
- request: buildRequest(evalCase),
7847
+ request: buildRequest(evalCase, options.toolOverrideVariantId),
7567
7848
  response,
7568
7849
  error,
7569
7850
  expectations: expectationResults,
@@ -7642,7 +7923,8 @@ async function runEvalCase(evalCase, context, options = {}) {
7642
7923
  authType: context.mcp.authType,
7643
7924
  project: context.mcp.project,
7644
7925
  durationMs: 0,
7645
- tags: evalCase.tags
7926
+ tags: evalCase.tags,
7927
+ request: buildRequest(evalCase, options.toolOverrideVariantId)
7646
7928
  };
7647
7929
  const totalHostUsage = iterationResults.reduce(
7648
7930
  (acc, r) => sumUsage(acc, r.hostUsage),
@@ -7689,6 +7971,7 @@ async function getGitHash() {
7689
7971
  const result = await execFileNoThrow("git", ["rev-parse", "HEAD"]);
7690
7972
  return result.status === 0 ? result.stdout.trim() : void 0;
7691
7973
  }
7974
+ var warnedNoTestInfo = false;
7692
7975
  async function runEvalDataset(options, context) {
7693
7976
  const {
7694
7977
  dataset,
@@ -7701,11 +7984,15 @@ async function runEvalDataset(options, context) {
7701
7984
  filterTags,
7702
7985
  saveResultsTo,
7703
7986
  omitResponsesFromBaseline = true,
7987
+ redactStoredResponses,
7988
+ resultStore,
7704
7989
  baselineResultsFrom,
7990
+ toolOverrides,
7705
7991
  mcpHostModel,
7706
7992
  judgeModel
7707
7993
  } = options;
7708
7994
  const startTime = Date.now();
7995
+ const effectiveContext = toolOverrides ? { ...context, mcp: createToolOverrideMCP(context.mcp, toolOverrides) } : context;
7709
7996
  const allSchemas = {
7710
7997
  ...dataset.schemas,
7711
7998
  ...schemas
@@ -7737,9 +8024,10 @@ async function runEvalDataset(options, context) {
7737
8024
  }
7738
8025
  }
7739
8026
  const effectiveCase = withIterations.judgeReps === void 0 && defaultJudgeReps !== void 0 ? { ...withIterations, judgeReps: defaultJudgeReps } : withIterations;
7740
- const result2 = await runEvalCase(effectiveCase, context, {
8027
+ const result2 = await runEvalCase(effectiveCase, effectiveContext, {
7741
8028
  datasetName: dataset.name,
7742
- schemas: allSchemas
8029
+ schemas: allSchemas,
8030
+ toolOverrideVariantId: toolOverrides?.id
7743
8031
  });
7744
8032
  if (onCaseComplete) {
7745
8033
  await onCaseComplete(result2);
@@ -7764,6 +8052,9 @@ async function runEvalDataset(options, context) {
7764
8052
  gitHash,
7765
8053
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7766
8054
  packageVersion: package_default.version,
8055
+ ...toolOverrides !== void 0 && {
8056
+ toolOverrideVariantId: toolOverrides.id
8057
+ },
7767
8058
  ...mcpHostModel !== void 0 && { mcpHostModel },
7768
8059
  ...judgeModel !== void 0 && { judgeModel }
7769
8060
  };
@@ -7782,7 +8073,7 @@ async function runEvalDataset(options, context) {
7782
8073
  };
7783
8074
  if (baselineResultsFrom) {
7784
8075
  try {
7785
- const baseline = await loadBaseline(baselineResultsFrom);
8076
+ const baseline = typeof baselineResultsFrom === "string" ? await loadBaseline(baselineResultsFrom) : await loadStoredBaseline(baselineResultsFrom, resultStore);
7786
8077
  const baselinePassRate = baseline.total > 0 ? baseline.passed / baseline.total : 0;
7787
8078
  const baselineMap = buildBaselinePassMap(baseline);
7788
8079
  const currentCaseIds = result.caseResults.map((cr) => cr.id);
@@ -7810,7 +8101,7 @@ async function runEvalDataset(options, context) {
7810
8101
  result.deltaPassRate = result.total > 0 ? result.passed / result.total - baselinePassRate : 0;
7811
8102
  } catch (err) {
7812
8103
  console.warn(
7813
- `[mcp-server-tester] Could not load baseline from ${baselineResultsFrom}: ${err instanceof Error ? err.message : String(err)}`
8104
+ `[mcp-server-tester] Could not load baseline from ${formatBaselineRef(baselineResultsFrom)}: ${err instanceof Error ? err.message : String(err)}`
7814
8105
  );
7815
8106
  }
7816
8107
  }
@@ -7825,22 +8116,84 @@ async function runEvalDataset(options, context) {
7825
8116
  result.datasetToolF1 = avgPrec + avgRecall > 0 ? 2 * avgPrec * avgRecall / (avgPrec + avgRecall) : 0;
7826
8117
  }
7827
8118
  if (saveResultsTo) {
7828
- await saveBaseline(result, saveResultsTo, {
7829
- omitResponses: omitResponsesFromBaseline
7830
- });
8119
+ if (typeof saveResultsTo === "string") {
8120
+ await saveBaseline(result, saveResultsTo, {
8121
+ omitResponses: omitResponsesFromBaseline
8122
+ });
8123
+ } else {
8124
+ await saveStoredEvalResult(result, saveResultsTo, {
8125
+ resultStore,
8126
+ omitResponses: redactStoredResponses ?? true,
8127
+ metadata: {
8128
+ datasetName: dataset.name,
8129
+ ...toolOverrides?.id !== void 0 && {
8130
+ toolOverrideVariantId: toolOverrides.id
8131
+ },
8132
+ ...mcpHostModel !== void 0 && { mcpHostModel },
8133
+ ...judgeModel !== void 0 && { judgeModel },
8134
+ ...gitHash !== void 0 && { gitHash },
8135
+ packageVersion: package_default.version
8136
+ }
8137
+ });
8138
+ }
7831
8139
  }
7832
8140
  if (context.testInfo) {
7833
8141
  await context.testInfo.attach("mcp-test-results", {
7834
8142
  contentType: "application/json",
7835
8143
  body: Buffer.from(JSON.stringify({ caseResults }))
7836
8144
  });
7837
- } else if (caseResults.length > 0) {
8145
+ } else if (caseResults.length > 0 && !warnedNoTestInfo) {
8146
+ warnedNoTestInfo = true;
7838
8147
  console.warn(
7839
8148
  "[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 });"
7840
8149
  );
7841
8150
  }
7842
8151
  return result;
7843
8152
  }
8153
+ async function loadStoredBaseline(baselineResultsFrom, resultStore) {
8154
+ if (!resultStore) {
8155
+ throw new Error("resultStore is required for store-backed baselines");
8156
+ }
8157
+ const store = resolveEvalResultStore(resultStore);
8158
+ const artifact = baselineResultsFrom.ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8159
+ "eval-runner-result",
8160
+ baselineResultsFrom.ref.id
8161
+ );
8162
+ if (!artifact) {
8163
+ throw new Error("No latest eval run artifact found");
8164
+ }
8165
+ return artifact.data;
8166
+ }
8167
+ async function saveStoredEvalResult(result, saveResultsTo, options) {
8168
+ if (!options.resultStore) {
8169
+ throw new Error("resultStore is required for store-backed saves");
8170
+ }
8171
+ const store = resolveEvalResultStore(options.resultStore);
8172
+ const data = options.omitResponses ? omitResponsesFromResult(result) : result;
8173
+ const id = saveResultsTo.ref && saveResultsTo.ref !== "latest" ? saveResultsTo.ref.id : void 0;
8174
+ await store.saveArtifact(
8175
+ createStoredEvalArtifact({
8176
+ kind: "eval-runner-result",
8177
+ id,
8178
+ data,
8179
+ metadata: options.metadata
8180
+ })
8181
+ );
8182
+ }
8183
+ function omitResponsesFromResult(result) {
8184
+ return {
8185
+ ...result,
8186
+ caseResults: result.caseResults.map(
8187
+ ({ response: _response, ...rest }) => rest
8188
+ )
8189
+ };
8190
+ }
8191
+ function formatBaselineRef(baselineResultsFrom) {
8192
+ if (typeof baselineResultsFrom === "string") {
8193
+ return baselineResultsFrom;
8194
+ }
8195
+ return baselineResultsFrom.ref === "latest" ? "resultStore latest" : `resultStore ${baselineResultsFrom.ref.id}`;
8196
+ }
7844
8197
 
7845
8198
  // src/evals/serverComparison.ts
7846
8199
  async function runServerComparison(options, contextA, contextB) {
@@ -7882,7 +8235,7 @@ async function runServerComparison(options, contextA, contextB) {
7882
8235
  }
7883
8236
  const total = cases.length;
7884
8237
  const decidedCases = aWins + bWins + ties;
7885
- return {
8238
+ const comparison = {
7886
8239
  dataset: options.dataset.name,
7887
8240
  total,
7888
8241
  aWins,
@@ -7899,6 +8252,366 @@ async function runServerComparison(options, contextA, contextB) {
7899
8252
  serverBResult: resultB,
7900
8253
  durationMs: Date.now() - startTime
7901
8254
  };
8255
+ if (options.comparisonStore) {
8256
+ await saveServerComparison({
8257
+ store: options.comparisonStore,
8258
+ comparison,
8259
+ id: options.comparisonId,
8260
+ metadata: {
8261
+ datasetName: options.dataset.name,
8262
+ ...options.comparisonMetadata ?? {}
8263
+ },
8264
+ redactStoredResponses: options.redactStoredResponses
8265
+ });
8266
+ }
8267
+ return comparison;
8268
+ }
8269
+ async function saveServerComparison(options) {
8270
+ const store = resolveEvalResultStore(options.store);
8271
+ const data = options.redactStoredResponses ? redactResponses(options.comparison) : options.comparison;
8272
+ const artifact = createStoredEvalArtifact({
8273
+ kind: "server-comparison",
8274
+ id: options.id,
8275
+ data,
8276
+ metadata: {
8277
+ datasetName: options.comparison.dataset,
8278
+ ...options.metadata ?? {}
8279
+ }
8280
+ });
8281
+ await store.saveArtifact(artifact);
8282
+ return artifact;
8283
+ }
8284
+ function redactResponses(value) {
8285
+ return JSON.parse(
8286
+ JSON.stringify(
8287
+ value,
8288
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8289
+ )
8290
+ );
8291
+ }
8292
+
8293
+ // src/evals/evalRunComparison.ts
8294
+ function compareEvalRuns(options) {
8295
+ const { baseline, candidate, labels } = options;
8296
+ const candidateMap = new Map(
8297
+ candidate.caseResults.map((result) => [result.id, result])
8298
+ );
8299
+ const cases = [];
8300
+ const seenIds = /* @__PURE__ */ new Set();
8301
+ for (const baselineCase of baseline.caseResults) {
8302
+ seenIds.add(baselineCase.id);
8303
+ const candidateCase = candidateMap.get(baselineCase.id);
8304
+ if (!candidateCase) {
8305
+ cases.push({
8306
+ id: baselineCase.id,
8307
+ outcome: "MISSING_FROM_CANDIDATE",
8308
+ baseline: baselineCase
8309
+ });
8310
+ continue;
8311
+ }
8312
+ cases.push({
8313
+ id: baselineCase.id,
8314
+ outcome: compareCaseOutcome(baselineCase.pass, candidateCase.pass),
8315
+ baseline: baselineCase,
8316
+ candidate: candidateCase
8317
+ });
8318
+ }
8319
+ for (const candidateCase of candidate.caseResults) {
8320
+ if (seenIds.has(candidateCase.id)) {
8321
+ continue;
8322
+ }
8323
+ cases.push({
8324
+ id: candidateCase.id,
8325
+ outcome: "MISSING_FROM_BASELINE",
8326
+ candidate: candidateCase
8327
+ });
8328
+ }
8329
+ const baselinePassRate = passRate(baseline);
8330
+ const candidatePassRate = passRate(candidate);
8331
+ return {
8332
+ baselineLabel: labels?.baseline ?? "baseline",
8333
+ candidateLabel: labels?.candidate ?? candidate.metadata?.toolOverrideVariantId ?? "candidate",
8334
+ baselinePassRate,
8335
+ candidatePassRate,
8336
+ deltaPassRate: candidatePassRate - baselinePassRate,
8337
+ ...metricDelta(
8338
+ "ToolPrecision",
8339
+ baseline.datasetToolPrecision,
8340
+ candidate.datasetToolPrecision
8341
+ ),
8342
+ ...metricDelta(
8343
+ "ToolRecall",
8344
+ baseline.datasetToolRecall,
8345
+ candidate.datasetToolRecall
8346
+ ),
8347
+ ...metricDelta("ToolF1", baseline.datasetToolF1, candidate.datasetToolF1),
8348
+ cases,
8349
+ improvedCases: cases.filter((c) => c.outcome === "IMPROVED"),
8350
+ regressedCases: cases.filter((c) => c.outcome === "REGRESSED"),
8351
+ unchangedPasses: cases.filter((c) => c.outcome === "UNCHANGED_PASS"),
8352
+ unchangedFailures: cases.filter((c) => c.outcome === "UNCHANGED_FAIL"),
8353
+ missingFromBaseline: cases.filter(
8354
+ (c) => c.outcome === "MISSING_FROM_BASELINE"
8355
+ ),
8356
+ missingFromCandidate: cases.filter(
8357
+ (c) => c.outcome === "MISSING_FROM_CANDIDATE"
8358
+ )
8359
+ };
8360
+ }
8361
+ async function loadStoredEvalRunnerResult(storeLike, ref) {
8362
+ const store = resolveEvalResultStore(storeLike);
8363
+ const artifact = ref === "latest" ? await store.loadLatestArtifact("eval-runner-result") : await store.loadArtifact(
8364
+ "eval-runner-result",
8365
+ ref.id
8366
+ );
8367
+ if (!artifact) {
8368
+ throw new Error("No latest eval run artifact found");
8369
+ }
8370
+ return artifact;
8371
+ }
8372
+ async function saveEvalRunComparison(options) {
8373
+ const store = resolveEvalResultStore(options.store);
8374
+ const data = options.redactStoredResponses ? redactResponses2(options.comparison) : options.comparison;
8375
+ const artifact = createStoredEvalArtifact({
8376
+ kind: "eval-run-comparison",
8377
+ id: options.id,
8378
+ data,
8379
+ metadata: {
8380
+ labels: {
8381
+ baseline: options.comparison.baselineLabel,
8382
+ candidate: options.comparison.candidateLabel
8383
+ },
8384
+ ...options.metadata ?? {}
8385
+ }
8386
+ });
8387
+ await store.saveArtifact(artifact);
8388
+ return artifact;
8389
+ }
8390
+ function compareCaseOutcome(baselinePass, candidatePass) {
8391
+ if (!baselinePass && candidatePass) return "IMPROVED";
8392
+ if (baselinePass && !candidatePass) return "REGRESSED";
8393
+ return baselinePass ? "UNCHANGED_PASS" : "UNCHANGED_FAIL";
8394
+ }
8395
+ function passRate(result) {
8396
+ return result.total > 0 ? result.passed / result.total : 0;
8397
+ }
8398
+ function metricDelta(name15, baselineValue, candidateValue) {
8399
+ const result = {};
8400
+ if (baselineValue !== void 0) {
8401
+ result[`baseline${name15}`] = baselineValue;
8402
+ }
8403
+ if (candidateValue !== void 0) {
8404
+ result[`candidate${name15}`] = candidateValue;
8405
+ }
8406
+ if (baselineValue !== void 0 && candidateValue !== void 0) {
8407
+ result[`delta${name15}`] = candidateValue - baselineValue;
8408
+ }
8409
+ return result;
8410
+ }
8411
+ function redactResponses2(value) {
8412
+ return JSON.parse(
8413
+ JSON.stringify(
8414
+ value,
8415
+ (key, currentValue) => key === "response" ? void 0 : currentValue
8416
+ )
8417
+ );
8418
+ }
8419
+
8420
+ // src/evals/variantExperiment.ts
8421
+ async function runVariantExperiment(options, context) {
8422
+ const metric = options.metric ?? "passRate";
8423
+ const maxRounds = options.maxRounds ?? 1;
8424
+ const minImprovement = options.minImprovement ?? 0;
8425
+ const allowRegressions = options.allowRegressions ?? false;
8426
+ const internalContext = {
8427
+ mcp: context.mcp,
8428
+ expect: context.expect
8429
+ };
8430
+ const baseline = await runEvalDataset(
8431
+ buildRunOptions(options, void 0),
8432
+ internalContext
8433
+ );
8434
+ const baselineValue = readMetric(baseline, metric);
8435
+ if (baselineValue === void 0) {
8436
+ throw new Error(
8437
+ `Metric '${metric}' is unavailable: the dataset produced no tool precision/recall data. Add mcp_host cases with toolsTriggered expectations, or use metric 'passRate'.`
8438
+ );
8439
+ }
8440
+ const rounds = [];
8441
+ let bestSoFar;
8442
+ let bestAttempted;
8443
+ let reason = "max-rounds";
8444
+ for (let round = 0; round < maxRounds; round++) {
8445
+ const variants = await gatherVariants(options, {
8446
+ round,
8447
+ baseline,
8448
+ metric,
8449
+ history: rounds,
8450
+ bestSoFar
8451
+ });
8452
+ if (variants.length === 0) {
8453
+ reason = round === 0 ? "no-variants" : "no-improvement";
8454
+ break;
8455
+ }
8456
+ const candidates = [];
8457
+ for (const variant of variants) {
8458
+ const candidate = await scoreVariant(
8459
+ options,
8460
+ internalContext,
8461
+ baseline,
8462
+ baselineValue,
8463
+ metric,
8464
+ allowRegressions,
8465
+ variant
8466
+ );
8467
+ candidates.push(candidate);
8468
+ bestAttempted = pickBetter(bestAttempted, candidate, true);
8469
+ }
8470
+ const roundBest = candidates.reduce(
8471
+ (best, candidate) => pickBetter(best, candidate, false),
8472
+ void 0
8473
+ );
8474
+ rounds.push({ round, candidates, best: roundBest });
8475
+ if (roundBest) {
8476
+ const improvement = roundBest.metricValue - (bestSoFar?.metricValue ?? baselineValue);
8477
+ bestSoFar = pickBetter(bestSoFar, roundBest, false);
8478
+ if (improvement < minImprovement) {
8479
+ reason = "no-improvement";
8480
+ break;
8481
+ }
8482
+ }
8483
+ }
8484
+ const winner = bestSoFar;
8485
+ const proposalSource = winner ?? bestAttempted;
8486
+ const proposal = proposalSource ? buildProposal(metric, baselineValue, proposalSource, winner !== void 0) : void 0;
8487
+ const result = {
8488
+ metric,
8489
+ baseline,
8490
+ rounds,
8491
+ winner,
8492
+ proposal,
8493
+ converged: true,
8494
+ reason
8495
+ };
8496
+ if (context.testInfo) {
8497
+ const surfaceRun = winner?.result ?? bestAttempted?.result ?? baseline;
8498
+ await context.testInfo.attach("mcp-test-results", {
8499
+ contentType: "application/json",
8500
+ body: Buffer.from(
8501
+ JSON.stringify({ caseResults: surfaceRun.caseResults })
8502
+ )
8503
+ });
8504
+ await context.testInfo.attach("mcp-variant-experiment", {
8505
+ contentType: "application/json",
8506
+ body: Buffer.from(
8507
+ JSON.stringify(buildExperimentData(result, baselineValue))
8508
+ )
8509
+ });
8510
+ }
8511
+ return result;
8512
+ }
8513
+ function buildExperimentData(result, baselineValue) {
8514
+ return {
8515
+ metric: result.metric,
8516
+ baselineValue,
8517
+ bestValue: result.winner?.metricValue ?? result.proposal?.candidateValue ?? baselineValue,
8518
+ rounds: result.rounds.map((round) => {
8519
+ const best = round.best ?? round.candidates[0];
8520
+ return {
8521
+ round: round.round,
8522
+ variantId: best?.variant.id ?? "(none)",
8523
+ metricValue: best?.metricValue ?? baselineValue,
8524
+ metricDelta: best?.metricDelta ?? 0,
8525
+ disqualified: best?.disqualified ?? false
8526
+ };
8527
+ }),
8528
+ winnerVariantId: result.winner?.variant.id,
8529
+ recommendation: result.proposal?.recommendation,
8530
+ reason: result.reason
8531
+ };
8532
+ }
8533
+ async function gatherVariants(options, context) {
8534
+ if (context.round === 0 && options.variants && options.variants.length > 0) {
8535
+ return options.variants;
8536
+ }
8537
+ if (options.proposeVariants) {
8538
+ return options.proposeVariants(context);
8539
+ }
8540
+ return [];
8541
+ }
8542
+ async function scoreVariant(options, context, baseline, baselineValue, metric, allowRegressions, variant) {
8543
+ const result = await runEvalDataset(
8544
+ buildRunOptions(options, variant),
8545
+ context
8546
+ );
8547
+ const comparison = compareEvalRuns({
8548
+ baseline,
8549
+ candidate: result,
8550
+ labels: { candidate: variant.id }
8551
+ });
8552
+ const metricValue = readMetric(result, metric) ?? baselineValue;
8553
+ const disqualified = !allowRegressions && comparison.regressedCases.length > 0;
8554
+ return {
8555
+ variant,
8556
+ result,
8557
+ comparison,
8558
+ metricValue,
8559
+ metricDelta: metricValue - baselineValue,
8560
+ disqualified
8561
+ };
8562
+ }
8563
+ function pickBetter(incumbent, challenger, includeDisqualified) {
8564
+ if (!includeDisqualified && challenger.disqualified) {
8565
+ return incumbent;
8566
+ }
8567
+ if (!incumbent) {
8568
+ return challenger;
8569
+ }
8570
+ return challenger.metricValue > incumbent.metricValue ? challenger : incumbent;
8571
+ }
8572
+ function buildProposal(metric, baselineValue, source, isWinner) {
8573
+ let recommendation;
8574
+ if (isWinner) {
8575
+ recommendation = source.metricDelta > 0 ? "apply" : "inconclusive";
8576
+ } else {
8577
+ recommendation = source.disqualified ? "reject" : "inconclusive";
8578
+ }
8579
+ return {
8580
+ variantId: source.variant.id,
8581
+ metric,
8582
+ baselineValue,
8583
+ candidateValue: source.metricValue,
8584
+ delta: source.metricDelta,
8585
+ toolChanges: source.variant.tools,
8586
+ improvedCaseIds: source.comparison.improvedCases.map((c) => c.id),
8587
+ regressedCaseIds: source.comparison.regressedCases.map((c) => c.id),
8588
+ recommendation
8589
+ };
8590
+ }
8591
+ function readMetric(result, metric) {
8592
+ switch (metric) {
8593
+ case "passRate":
8594
+ return result.total > 0 ? result.passed / result.total : 0;
8595
+ case "toolF1":
8596
+ return result.datasetToolF1;
8597
+ case "toolPrecision":
8598
+ return result.datasetToolPrecision;
8599
+ case "toolRecall":
8600
+ return result.datasetToolRecall;
8601
+ }
8602
+ }
8603
+ function buildRunOptions(options, toolOverrides) {
8604
+ return {
8605
+ dataset: options.dataset,
8606
+ toolOverrides,
8607
+ defaultLlmIterations: options.defaultLlmIterations,
8608
+ defaultJudgeReps: options.defaultJudgeReps,
8609
+ concurrency: options.concurrency,
8610
+ filterTags: options.filterTags,
8611
+ schemas: options.schemas,
8612
+ mcpHostModel: options.mcpHostModel,
8613
+ judgeModel: options.judgeModel
8614
+ };
7902
8615
  }
7903
8616
 
7904
8617
  // src/spec/conformanceChecks.ts
@@ -8072,6 +8785,6 @@ function formatCapabilities(capabilities) {
8072
8785
  return parts.length > 0 ? parts.join(", ") : "none declared";
8073
8786
  }
8074
8787
 
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 };
8788
+ 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
8789
  //# sourceMappingURL=index.js.map
8077
8790
  //# sourceMappingURL=index.js.map