@gleanwork/mcp-server-tester 1.1.1 → 2.0.0-beta.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.
@@ -216,6 +216,77 @@ type RubricSpec = BuiltInRubric | {
216
216
  /** Valid LLM judge provider kinds. */
217
217
  type ProviderKind = 'anthropic' | 'vertex-anthropic' | 'anthropic-agent-sdk' | 'openai' | 'google';
218
218
 
219
+ /**
220
+ * High-level API for interacting with MCP servers in tests
221
+ *
222
+ * This interface wraps the raw MCP Client with test-friendly methods
223
+ */
224
+ interface MCPFixtureApi {
225
+ /**
226
+ * The underlying MCP client (for advanced usage)
227
+ */
228
+ client: Client;
229
+ /**
230
+ * Authentication type used for this test session
231
+ */
232
+ authType: AuthType;
233
+ /**
234
+ * Playwright project name for this test session
235
+ */
236
+ project?: string;
237
+ /**
238
+ * Lists all available tools from the MCP server
239
+ *
240
+ * @returns Array of tool definitions
241
+ */
242
+ listTools(): Promise<Array<Tool>>;
243
+ /**
244
+ * Calls a tool on the MCP server
245
+ *
246
+ * @param name - Tool name
247
+ * @param args - Tool arguments
248
+ * @returns Tool call result
249
+ */
250
+ callTool<TArgs extends Record<string, unknown> = Record<string, unknown>>(name: string, args: TArgs): Promise<CallToolResult>;
251
+ /**
252
+ * Gets information about the connected server
253
+ */
254
+ getServerInfo(): {
255
+ name?: string;
256
+ version?: string;
257
+ } | null;
258
+ }
259
+
260
+ /**
261
+ * Canonical type definitions for @gleanwork/mcp-server-tester
262
+ *
263
+ * This module is the single source of truth for shared types.
264
+ * All other modules should import from here rather than defining their own.
265
+ *
266
+ * @packageDocumentation
267
+ */
268
+ /**
269
+ * Authentication type for MCP connections
270
+ *
271
+ * - 'oauth': Interactive OAuth 2.1 with PKCE (browser-based authentication)
272
+ * - 'api-token': Static API token (e.g., from a dashboard or environment variable)
273
+ * - 'none': No authentication
274
+ */
275
+ type AuthType = 'oauth' | 'api-token' | 'none';
276
+
277
+ interface HostEvent {
278
+ kind: 'tool_call' | 'skill' | 'command' | 'subagent';
279
+ source: 'mcp' | 'host';
280
+ name: string;
281
+ server?: string;
282
+ arguments?: Record<string, unknown>;
283
+ output?: string;
284
+ /** Explicit tool-result error status, absent when not observed. */
285
+ isError?: boolean;
286
+ rawName?: string;
287
+ id?: string;
288
+ }
289
+
219
290
  /**
220
291
  * Tool call validators for mcp_host simulation results.
221
292
  *
@@ -226,6 +297,9 @@ type ProviderKind = 'anthropic' | 'vertex-anthropic' | 'anthropic-agent-sdk' | '
226
297
  interface ToolCallExpectation {
227
298
  calls: Array<{
228
299
  name: string;
300
+ kind?: HostEvent['kind'];
301
+ source?: HostEvent['source'];
302
+ server?: string;
229
303
  arguments?: Record<string, unknown>;
230
304
  required?: boolean;
231
305
  }>;
@@ -248,6 +322,8 @@ interface ToolCallCountOptions {
248
322
  * Options for the LLM judge matcher
249
323
  */
250
324
  interface JudgeMatcherOptions {
325
+ /** Plugin options, parsed by the registered judge's schema. */
326
+ options?: Record<string, unknown>;
251
327
  /** Reference response to compare against */
252
328
  reference?: unknown;
253
329
  /** Score threshold for passing (default: 0.7) */
@@ -261,7 +337,7 @@ interface JudgeMatcherOptions {
261
337
  /**
262
338
  * Name of a registered custom judge executor.
263
339
  * When set, the named judge handles the entire evaluation pipeline
264
- * and its `pass` result is authoritative.
340
+ * and its score is compared with passingThreshold.
265
341
  */
266
342
  judge?: string;
267
343
  }
@@ -615,64 +691,6 @@ declare const expect: playwright_test.Expect<{
615
691
  toHaveToolCallCount: typeof toHaveToolCallCount;
616
692
  }>;
617
693
 
618
- /**
619
- * Canonical type definitions for @gleanwork/mcp-server-tester
620
- *
621
- * This module is the single source of truth for shared types.
622
- * All other modules should import from here rather than defining their own.
623
- *
624
- * @packageDocumentation
625
- */
626
- /**
627
- * Authentication type for MCP connections
628
- *
629
- * - 'oauth': Interactive OAuth 2.1 with PKCE (browser-based authentication)
630
- * - 'api-token': Static API token (e.g., from a dashboard or environment variable)
631
- * - 'none': No authentication
632
- */
633
- type AuthType = 'oauth' | 'api-token' | 'none';
634
-
635
- /**
636
- * High-level API for interacting with MCP servers in tests
637
- *
638
- * This interface wraps the raw MCP Client with test-friendly methods
639
- */
640
- interface MCPFixtureApi {
641
- /**
642
- * The underlying MCP client (for advanced usage)
643
- */
644
- client: Client;
645
- /**
646
- * Authentication type used for this test session
647
- */
648
- authType: AuthType;
649
- /**
650
- * Playwright project name for this test session
651
- */
652
- project?: string;
653
- /**
654
- * Lists all available tools from the MCP server
655
- *
656
- * @returns Array of tool definitions
657
- */
658
- listTools(): Promise<Array<Tool>>;
659
- /**
660
- * Calls a tool on the MCP server
661
- *
662
- * @param name - Tool name
663
- * @param args - Tool arguments
664
- * @returns Tool call result
665
- */
666
- callTool<TArgs extends Record<string, unknown> = Record<string, unknown>>(name: string, args: TArgs): Promise<CallToolResult>;
667
- /**
668
- * Gets information about the connected server
669
- */
670
- getServerInfo(): {
671
- name?: string;
672
- version?: string;
673
- } | null;
674
- }
675
-
676
694
  /**
677
695
  * Internal fixture state for passing auth type between fixtures
678
696
  */
@@ -101,6 +101,9 @@ function extractText(response) {
101
101
  if (typeof r.text === "string") {
102
102
  return r.text;
103
103
  }
104
+ if (typeof r.response === "string") {
105
+ return r.response;
106
+ }
104
107
  return JSON.stringify(r);
105
108
  }
106
109
  if (typeof response === "number" || typeof response === "boolean" || typeof response === "bigint") {
@@ -416,7 +419,7 @@ function toRegExp(pattern, caseInsensitive) {
416
419
  if (caseInsensitive && !pattern.flags.includes("i")) {
417
420
  return new RegExp(pattern.source, pattern.flags + "i");
418
421
  }
419
- return pattern;
422
+ return new RegExp(pattern.source, pattern.flags);
420
423
  }
421
424
  const flags = caseInsensitive ? "i" : "";
422
425
  return new RegExp(pattern, flags);
@@ -1176,21 +1179,109 @@ function createJudge(config = {}) {
1176
1179
  );
1177
1180
  }
1178
1181
  }
1182
+ var REGISTRY_STATE_KEY = /* @__PURE__ */ Symbol.for(
1183
+ "mcp-server-tester.framework-registry-state"
1184
+ );
1185
+ var globalRegistry = globalThis;
1186
+ var existingRegistryState = globalRegistry[REGISTRY_STATE_KEY];
1187
+ var registryState = existingRegistryState ?? {
1188
+ datasets: /* @__PURE__ */ new Map(),
1189
+ hosts: /* @__PURE__ */ new Map(),
1190
+ judges: /* @__PURE__ */ new Map(),
1191
+ metrics: /* @__PURE__ */ new Map(),
1192
+ resultStores: /* @__PURE__ */ new Map()
1193
+ };
1194
+ if (!existingRegistryState) globalRegistry[REGISTRY_STATE_KEY] = registryState;
1195
+ function createRegistry(kind, implementations) {
1196
+ const typedImplementations = implementations;
1197
+ return {
1198
+ register(implementation) {
1199
+ const existing = typedImplementations.get(implementation.name);
1200
+ if (existing && existing !== implementation) {
1201
+ throw new Error(
1202
+ `${kind} "${implementation.name}" is already registered.`
1203
+ );
1204
+ }
1205
+ typedImplementations.set(implementation.name, implementation);
1206
+ },
1207
+ get(name) {
1208
+ const implementation = typedImplementations.get(name);
1209
+ if (!implementation) {
1210
+ const available = [...typedImplementations.keys()].sort().join(", ");
1211
+ throw new Error(
1212
+ `${kind} "${name}" is not registered.${available ? ` Available: ${available}.` : ""}`
1213
+ );
1214
+ }
1215
+ return implementation;
1216
+ },
1217
+ list() {
1218
+ return [...typedImplementations.values()].sort(
1219
+ (a, b) => a.name.localeCompare(b.name)
1220
+ );
1221
+ },
1222
+ clear() {
1223
+ typedImplementations.clear();
1224
+ }
1225
+ };
1226
+ }
1227
+ createRegistry(
1228
+ "Dataset source",
1229
+ registryState.datasets
1230
+ );
1231
+ createRegistry("Host", registryState.hosts);
1232
+ var judges = createRegistry("Judge", registryState.judges);
1233
+ createRegistry(
1234
+ "Metric",
1235
+ registryState.metrics
1236
+ );
1237
+ createRegistry(
1238
+ "Result store",
1239
+ registryState.resultStores
1240
+ );
1241
+ var getJudge = (name) => judges.get(name);
1242
+ z.object({
1243
+ iterations: z.number().int().positive().optional(),
1244
+ maxCases: z.number().int().positive().optional(),
1245
+ concurrency: z.number().int().positive().optional(),
1246
+ filterTags: z.array(z.string().min(1)).optional()
1247
+ }).strict();
1179
1248
 
1180
1249
  // src/judge/judgeRegistry.ts
1181
- var registry = /* @__PURE__ */ new Map();
1182
1250
  function getRegisteredJudge(name) {
1183
- const executor = registry.get(name);
1184
- if (!executor) {
1185
- const available = registry.size > 0 ? ` Available judges: ${[...registry.keys()].join(", ")}` : " No judges are registered.";
1186
- throw new Error(
1187
- `Judge "${name}" is not registered.${available} Register it with registerJudge() before tests run.`
1188
- );
1251
+ try {
1252
+ const judge = getJudge(name);
1253
+ return judge.evaluate;
1254
+ } catch (error) {
1255
+ if (error instanceof Error && !error.message.includes("Available:")) {
1256
+ throw new Error(`${error.message} No judges are registered.`);
1257
+ }
1258
+ throw error;
1189
1259
  }
1190
- return executor;
1260
+ }
1261
+ function getRegisteredJudgeOptions(name, options) {
1262
+ const parsed = getJudge(name).schema.parse(options);
1263
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1264
+ throw new Error(`Judge "${name}" schema must return an options object.`);
1265
+ }
1266
+ return parsed;
1191
1267
  }
1192
1268
 
1193
1269
  // src/assertions/validators/judge.ts
1270
+ var judgeFrameworkOptionKeys = /* @__PURE__ */ new Set([
1271
+ "options",
1272
+ "judge",
1273
+ "rubric",
1274
+ "reference",
1275
+ "threshold",
1276
+ "reps",
1277
+ "provider",
1278
+ "model",
1279
+ "apiKeyEnvVar",
1280
+ "maxTokens",
1281
+ "temperature",
1282
+ "maxBudgetUsd",
1283
+ "maxToolOutputSize"
1284
+ ]);
1194
1285
  function computeStdDev(scores, mean) {
1195
1286
  if (scores.length <= 1) return 0;
1196
1287
  const variance = scores.reduce((sum, s) => sum + (s - mean) ** 2, 0) / scores.length;
@@ -1214,12 +1305,25 @@ async function validateJudge(response, config) {
1214
1305
  if (judgeName !== void 0) {
1215
1306
  try {
1216
1307
  const executor = getRegisteredJudge(judgeName);
1217
- const judgeResult = await executor(response, reference ?? void 0);
1308
+ const options = getRegisteredJudgeOptions(
1309
+ judgeName,
1310
+ config.options ?? Object.fromEntries(
1311
+ Object.entries(config).filter(
1312
+ ([key]) => !judgeFrameworkOptionKeys.has(key)
1313
+ )
1314
+ )
1315
+ );
1316
+ const judgeResult = await executor(
1317
+ response,
1318
+ reference ?? void 0,
1319
+ options
1320
+ );
1218
1321
  const score = judgeResult.score;
1219
1322
  const passed = score >= threshold;
1220
1323
  return {
1221
1324
  pass: passed,
1222
- message: passed ? `Custom judge "${judgeName}" passed with score ${score.toFixed(2)}` : `Custom judge "${judgeName}" failed with score ${score.toFixed(2)} (threshold: ${threshold}). ${judgeResult.reasoning ?? ""}`
1325
+ message: passed ? `Custom judge "${judgeName}" passed with score ${score.toFixed(2)}` : `Custom judge "${judgeName}" failed with score ${score.toFixed(2)} (threshold: ${threshold}). ${judgeResult.reasoning ?? ""}`,
1326
+ details: { score, reasoning: judgeResult.reasoning }
1223
1327
  };
1224
1328
  } catch (err) {
1225
1329
  return {
@@ -1309,7 +1413,8 @@ async function runSingleJudge(received, rubric, options) {
1309
1413
  reps,
1310
1414
  provider,
1311
1415
  model,
1312
- judge
1416
+ judge,
1417
+ options: judgeOptions
1313
1418
  } = options;
1314
1419
  const validation = await validateJudge(received, {
1315
1420
  ...rubric !== void 0 && { rubric },
@@ -1318,7 +1423,8 @@ async function runSingleJudge(received, rubric, options) {
1318
1423
  ...reps !== void 0 && { reps },
1319
1424
  ...provider !== void 0 && { provider },
1320
1425
  ...model !== void 0 && { model },
1321
- ...judge !== void 0 && { judge }
1426
+ ...judge !== void 0 && { judge },
1427
+ ...judgeOptions !== void 0 && { options: judgeOptions }
1322
1428
  });
1323
1429
  return { pass: validation.pass, message: validation.message };
1324
1430
  }
@@ -1491,10 +1597,22 @@ function partialMatch(actual, expected) {
1491
1597
  return JSON.stringify(actualVal) === JSON.stringify(v);
1492
1598
  });
1493
1599
  }
1600
+ function matchesIdentity(call, expected) {
1601
+ return (call.name === expected.name || call.server !== void 0 && `${call.server}.${call.name}` === expected.name) && (expected.kind === void 0 || (call.kind ?? "tool_call") === expected.kind) && (expected.source === void 0 || call.source === expected.source) && (expected.server === void 0 || call.server === expected.server);
1602
+ }
1603
+ function unverifiedEvidence(response) {
1604
+ if (response.evidence === void 0 || response.evidence === "structured")
1605
+ return void 0;
1606
+ return {
1607
+ pass: false,
1608
+ message: `Host evidence is ${response.evidence}; structured tool evidence is required.`,
1609
+ details: { evidence: response.evidence }
1610
+ };
1611
+ }
1494
1612
  function findMatchingCall(actual, expected, startIndex = 0) {
1495
1613
  for (let i = startIndex; i < actual.length; i++) {
1496
1614
  const call = actual[i];
1497
- if (call.name !== expected.name) continue;
1615
+ if (!matchesIdentity(call, expected)) continue;
1498
1616
  if (expected.arguments !== void 0 && !partialMatch(call.arguments ?? {}, expected.arguments)) {
1499
1617
  continue;
1500
1618
  }
@@ -1506,18 +1624,22 @@ function validateToolCalls(response, expectation) {
1506
1624
  if (!isSimulationResult(response)) {
1507
1625
  return {
1508
1626
  pass: false,
1509
- message: "toolsTriggered expectation requires mcp_host mode \u2014 response must be an MCPHostSimulationResult"
1627
+ message: "toolsTriggered expectation requires a host simulation response with structured tool calls"
1510
1628
  };
1511
1629
  }
1512
- const actual = response.toolCalls;
1630
+ const unverified = unverifiedEvidence(response);
1631
+ if (unverified) return unverified;
1632
+ const actual = response.events ?? response.toolCalls;
1513
1633
  const requiredCalls = expectation.calls.filter((c) => c.required !== false);
1514
1634
  const calledRequiredCount = requiredCalls.filter(
1515
1635
  (expected) => findMatchingCall(actual, expected) !== -1
1516
1636
  ).length;
1517
1637
  const recall = requiredCalls.length > 0 ? calledRequiredCount / requiredCalls.length : 1;
1518
1638
  const allowedNames = new Set(expectation.calls.map((c) => c.name));
1519
- const precision = actual.length > 0 ? actual.filter((c) => allowedNames.has(c.name)).length / actual.length : 1;
1520
- const metrics = { precision, recall };
1639
+ const precision = actual.length > 0 ? actual.filter(
1640
+ (call) => expectation.calls.some((expected) => matchesIdentity(call, expected))
1641
+ ).length / actual.length : 1;
1642
+ const metrics2 = { precision, recall };
1521
1643
  const order = expectation.order ?? "any";
1522
1644
  if (order === "strict") {
1523
1645
  let searchFrom = 0;
@@ -1532,7 +1654,7 @@ function validateToolCalls(response, expectation) {
1532
1654
  actual: actual.map((c) => c.name),
1533
1655
  expected: expected.name
1534
1656
  },
1535
- metrics
1657
+ metrics: metrics2
1536
1658
  };
1537
1659
  }
1538
1660
  } else {
@@ -1552,13 +1674,15 @@ function validateToolCalls(response, expectation) {
1552
1674
  actual: actual.map((c) => c.name),
1553
1675
  expected: expected.name
1554
1676
  },
1555
- metrics
1677
+ metrics: metrics2
1556
1678
  };
1557
1679
  }
1558
1680
  }
1559
1681
  }
1560
1682
  if (expectation.exclusive === true) {
1561
- const unexpected = actual.filter((c) => !allowedNames.has(c.name));
1683
+ const unexpected = actual.filter(
1684
+ (call) => !expectation.calls.some((expected) => matchesIdentity(call, expected))
1685
+ );
1562
1686
  if (unexpected.length > 0) {
1563
1687
  const names = unexpected.map((c) => `'${c.name}'`).join(", ");
1564
1688
  return {
@@ -1568,20 +1692,24 @@ function validateToolCalls(response, expectation) {
1568
1692
  actual: actual.map((c) => c.name),
1569
1693
  unexpected: unexpected.map((c) => c.name)
1570
1694
  },
1571
- metrics
1695
+ metrics: metrics2
1572
1696
  };
1573
1697
  }
1574
1698
  }
1575
- return { pass: true, message: "All tool call expectations met", metrics };
1699
+ return { pass: true, message: "All tool call expectations met", metrics: metrics2 };
1576
1700
  }
1577
1701
  function validateToolCallCount(response, options) {
1578
1702
  if (!isSimulationResult(response)) {
1579
1703
  return {
1580
1704
  pass: false,
1581
- message: "toolCallCount expectation requires mcp_host mode \u2014 response must be an MCPHostSimulationResult"
1705
+ message: "toolCallCount expectation requires a host simulation response with structured tool calls"
1582
1706
  };
1583
1707
  }
1584
- const count = response.toolCalls.length;
1708
+ const unverified = unverifiedEvidence(response);
1709
+ if (unverified) return unverified;
1710
+ const count = (response.events ?? response.toolCalls).filter(
1711
+ (call) => (call.kind ?? "tool_call") === "tool_call"
1712
+ ).length;
1585
1713
  const { min, max, exact } = options;
1586
1714
  if (exact !== void 0 && count !== exact) {
1587
1715
  return {
@@ -1665,14 +1793,19 @@ var MCPClientCredentialsConfigSchema = z.object({
1665
1793
  });
1666
1794
  var MCPAuthConfigSchema = z.object({
1667
1795
  accessToken: z.string().optional(),
1796
+ accessTokenEnv: z.string().min(1).optional(),
1668
1797
  oauth: MCPOAuthConfigSchema.optional(),
1669
1798
  clientCredentials: MCPClientCredentialsConfigSchema.optional()
1670
1799
  }).refine(
1800
+ (data) => !(data.accessToken && data.accessTokenEnv),
1801
+ "Cannot specify both accessToken and accessTokenEnv"
1802
+ ).refine(
1671
1803
  (data) => !(data.accessToken && data.oauth),
1672
1804
  "Cannot specify both accessToken and oauth configuration"
1673
1805
  );
1674
1806
  var StdioConfigSchema = z.object({
1675
1807
  transport: z.literal("stdio"),
1808
+ label: z.string().min(1).optional(),
1676
1809
  command: z.string().min(1, "command is required for stdio transport"),
1677
1810
  args: z.array(z.string()).optional(),
1678
1811
  cwd: z.string().optional(),
@@ -1688,6 +1821,7 @@ function isLocalhost(hostname) {
1688
1821
  }
1689
1822
  var HttpConfigSchema = z.object({
1690
1823
  transport: z.literal("http"),
1824
+ label: z.string().min(1).optional(),
1691
1825
  serverUrl: z.string().url("serverUrl must be a valid URL").refine((url) => {
1692
1826
  let parsed;
1693
1827
  try {
@@ -1740,7 +1874,7 @@ var debugHttp = createDebug(`${NAMESPACE}:http`);
1740
1874
 
1741
1875
  // package.json
1742
1876
  var package_default = {
1743
- version: "1.1.1"};
1877
+ version: "2.0.0-beta.1"};
1744
1878
  var debug = createDebug("mcp-server-tester:oauth-flow");
1745
1879
  async function generatePKCE() {
1746
1880
  const codeVerifier = oauth.generateRandomCodeVerifier();
@@ -2090,7 +2224,7 @@ async function createMCPClientForConfig(config, options) {
2090
2224
  debugHttp("Attempting transport: streamableHttp");
2091
2225
  const streamableTransport = new StreamableHTTPClientTransport(url, {
2092
2226
  requestInit,
2093
- authProvider: options?.authProvider
2227
+ ...options?.authProvider ? { authProvider: options.authProvider } : {}
2094
2228
  });
2095
2229
  await client.connect(streamableTransport, connectOptions);
2096
2230
  debugClient("Connected via Streamable HTTP");
@@ -2104,7 +2238,7 @@ async function createMCPClientForConfig(config, options) {
2104
2238
  debugHttp("Attempting transport: sse");
2105
2239
  const sseTransport = new SSEClientTransport(url, {
2106
2240
  requestInit,
2107
- authProvider: options?.authProvider
2241
+ ...options?.authProvider ? { authProvider: options.authProvider } : {}
2108
2242
  });
2109
2243
  await client.connect(sseTransport, connectOptions);
2110
2244
  debugClient("Connected via SSE");