@frontmcp/testing 1.8.0 → 1.8.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/esm/index.mjs CHANGED
@@ -343,6 +343,7 @@ var interceptors = {
343
343
 
344
344
  // libs/testing/src/transport/streamable-http.transport.ts
345
345
  var DEFAULT_TIMEOUT = 3e4;
346
+ var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
346
347
  var StreamableHttpTransport = class {
347
348
  config;
348
349
  state = "disconnected";
@@ -354,6 +355,10 @@ var StreamableHttpTransport = class {
354
355
  interceptors;
355
356
  publicMode;
356
357
  elicitationHandler;
358
+ notificationHandler;
359
+ notificationStream;
360
+ notificationProtocolVersion = "";
361
+ notificationStreamReopenTimer;
357
362
  constructor(config) {
358
363
  this.config = {
359
364
  baseUrl: config.baseUrl.replace(/\/$/, ""),
@@ -370,6 +375,7 @@ var StreamableHttpTransport = class {
370
375
  this.interceptors = config.interceptors;
371
376
  this.publicMode = config.publicMode ?? false;
372
377
  this.elicitationHandler = config.elicitationHandler;
378
+ this.notificationHandler = config.notificationHandler;
373
379
  }
374
380
  async connect() {
375
381
  this.state = "connecting";
@@ -589,7 +595,12 @@ var StreamableHttpTransport = class {
589
595
  };
590
596
  }
591
597
  }
598
+ async openNotificationStream(protocolVersion) {
599
+ this.notificationProtocolVersion = protocolVersion;
600
+ await this.connectNotificationStream();
601
+ }
592
602
  async close() {
603
+ this.closeNotificationStream();
593
604
  this.state = "disconnected";
594
605
  this.sessionId = void 0;
595
606
  this.log("StreamableHTTP transport closed");
@@ -628,6 +639,7 @@ var StreamableHttpTransport = class {
628
639
  return { ...this.lastRequestHeaders };
629
640
  }
630
641
  async simulateDisconnect() {
642
+ this.closeNotificationStream();
631
643
  this.state = "disconnected";
632
644
  this.sessionId = void 0;
633
645
  }
@@ -646,6 +658,72 @@ var StreamableHttpTransport = class {
646
658
  // ═══════════════════════════════════════════════════════════════════
647
659
  // PRIVATE HELPERS
648
660
  // ═══════════════════════════════════════════════════════════════════
661
+ closeNotificationStream() {
662
+ clearTimeout(this.notificationStreamReopenTimer);
663
+ this.notificationStream?.abort();
664
+ this.notificationStream = void 0;
665
+ }
666
+ /** Opens the stream without holding up the caller past the request timeout; close() aborts it at any point. */
667
+ async connectNotificationStream() {
668
+ if (!this.sessionId || this.notificationStream) return;
669
+ const controller = new AbortController();
670
+ this.notificationStream = controller;
671
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
672
+ const established = fetch(this.mcpUrl(), {
673
+ method: "GET",
674
+ headers: { ...headers, Accept: "text/event-stream", "MCP-Protocol-Version": this.notificationProtocolVersion },
675
+ signal: controller.signal
676
+ }).then(
677
+ async (response) => {
678
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
679
+ if (!response.ok || !response.body || !isEventStream) {
680
+ await response.body?.cancel();
681
+ this.log(`Server offers no notification stream (HTTP ${response.status})`);
682
+ this.releaseNotificationStream(controller);
683
+ return;
684
+ }
685
+ void this.readNotificationStream(controller, response.body.getReader());
686
+ },
687
+ (error) => {
688
+ this.log("Failed to open the notification stream:", error);
689
+ this.releaseNotificationStream(controller);
690
+ }
691
+ );
692
+ let establishTimer;
693
+ const establishDeadline = new Promise((resolve) => {
694
+ establishTimer = setTimeout(resolve, this.config.timeout);
695
+ });
696
+ await Promise.race([established, establishDeadline]);
697
+ clearTimeout(establishTimer);
698
+ }
699
+ releaseNotificationStream(controller) {
700
+ if (this.notificationStream === controller) this.notificationStream = void 0;
701
+ }
702
+ async readNotificationStream(controller, reader) {
703
+ const decoder = new TextDecoder();
704
+ let buffer = "";
705
+ try {
706
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
707
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
708
+ buffer = remainder;
709
+ for (const eventText of completeEvents) {
710
+ for (const event of this.parseSSEEvents(eventText, void 0).events) {
711
+ await this.handleSSEEvent(event);
712
+ }
713
+ }
714
+ }
715
+ } catch (error) {
716
+ this.log("Notification stream ended:", error);
717
+ } finally {
718
+ reader.releaseLock();
719
+ }
720
+ if (controller.signal.aborted || this.notificationStream !== controller) return;
721
+ this.releaseNotificationStream(controller);
722
+ this.notificationStreamReopenTimer = setTimeout(
723
+ () => void this.connectNotificationStream(),
724
+ NOTIFICATION_STREAM_REOPEN_DELAY_MS
725
+ );
726
+ }
649
727
  /**
650
728
  * Handle SSE response with elicitation support.
651
729
  *
@@ -783,6 +861,13 @@ var StreamableHttpTransport = class {
783
861
  response: { jsonrpc: "2.0", id: null, result: void 0 }
784
862
  };
785
863
  }
864
+ if ("method" in parsed && parsed.id === void 0) {
865
+ this.notificationHandler?.(parsed);
866
+ return {
867
+ isFinal: false,
868
+ response: { jsonrpc: "2.0", id: null, result: void 0 }
869
+ };
870
+ }
786
871
  if ("result" in parsed || "error" in parsed) {
787
872
  return { isFinal: true, response: parsed };
788
873
  }
@@ -953,6 +1038,13 @@ var StreamableHttpTransport = class {
953
1038
  };
954
1039
  }
955
1040
  };
1041
+ function splitSSEEvents(buffer) {
1042
+ const endsWithCarriageReturn = buffer.endsWith("\r");
1043
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1044
+ const completeEvents = normalized.split("\n\n");
1045
+ const remainder = completeEvents.pop() ?? "";
1046
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
1047
+ }
956
1048
 
957
1049
  // libs/testing/src/platform/platform-client-info.ts
958
1050
  var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
@@ -1108,7 +1200,8 @@ var McpTestClientBuilder = class {
1108
1200
  return this;
1109
1201
  }
1110
1202
  /**
1111
- * Set the MCP protocol version to request
1203
+ * Set the MCP protocol version to request (default '2025-06-18').
1204
+ * '2026-07-28' has no initialize handshake, so `build()` throws for it.
1112
1205
  */
1113
1206
  withProtocolVersion(version) {
1114
1207
  this.config.protocolVersion = version;
@@ -1204,6 +1297,7 @@ var McpTestClientBuilder = class {
1204
1297
  // libs/testing/src/client/mcp-test-client.ts
1205
1298
  var DEFAULT_TIMEOUT2 = 3e4;
1206
1299
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1300
+ var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1207
1301
  var DEFAULT_CLIENT_INFO = {
1208
1302
  name: "@frontmcp/testing",
1209
1303
  version: "0.4.0"
@@ -1240,6 +1334,8 @@ var McpTestClient = class {
1240
1334
  _traces = [];
1241
1335
  _notifications = [];
1242
1336
  _progressUpdates = [];
1337
+ collectingProgress = false;
1338
+ progressTokenCounter = 0;
1243
1339
  // Interceptor chain
1244
1340
  _interceptors;
1245
1341
  // Elicitation handler for server→client elicit requests
@@ -1248,6 +1344,11 @@ var McpTestClient = class {
1248
1344
  // CONSTRUCTOR & FACTORY
1249
1345
  // ═══════════════════════════════════════════════════════════════════
1250
1346
  constructor(config) {
1347
+ if (config.protocolVersion && UNSUPPORTED_PROTOCOL_VERSIONS.includes(config.protocolVersion)) {
1348
+ throw new Error(
1349
+ `McpTestClient does not support protocol version ${config.protocolVersion}: it has no initialize handshake. Use ${DEFAULT_PROTOCOL_VERSION} or earlier.`
1350
+ );
1351
+ }
1251
1352
  this.config = {
1252
1353
  baseUrl: config.baseUrl,
1253
1354
  entryPath: config.entryPath,
@@ -1333,6 +1434,7 @@ var McpTestClient = class {
1333
1434
  jsonrpc: "2.0",
1334
1435
  method: "notifications/initialized"
1335
1436
  });
1437
+ await this.transport.openNotificationStream?.(this.initResult.protocolVersion);
1336
1438
  this.log("info", `Connected to ${this.initResult.serverInfo?.name ?? "MCP Server"}`);
1337
1439
  return this.initResult;
1338
1440
  }
@@ -1443,8 +1545,8 @@ var McpTestClient = class {
1443
1545
  /**
1444
1546
  * Call a tool by name with arguments
1445
1547
  */
1446
- call: async (name, args) => {
1447
- const response = await this.callTool(name, args);
1548
+ call: async (name, args, options = {}) => {
1549
+ const response = await this.callTool(name, args, this.progressTokenFor(options));
1448
1550
  return this.wrapToolResult(response);
1449
1551
  }
1450
1552
  };
@@ -1586,6 +1688,7 @@ var McpTestClient = class {
1586
1688
  * Collect progress notifications specifically
1587
1689
  */
1588
1690
  collectProgress: () => {
1691
+ this.collectingProgress = true;
1589
1692
  return new ProgressCollector(this._progressUpdates);
1590
1693
  },
1591
1694
  /**
@@ -1891,12 +1994,29 @@ var McpTestClient = class {
1891
1994
  async listTools() {
1892
1995
  return this.request("tools/list", {});
1893
1996
  }
1894
- async callTool(name, args) {
1997
+ async callTool(name, args, progressToken) {
1895
1998
  return this.request("tools/call", {
1896
1999
  name,
1897
- arguments: args ?? {}
2000
+ arguments: args ?? {},
2001
+ ...progressToken !== void 0 && { _meta: { progressToken } }
1898
2002
  });
1899
2003
  }
2004
+ progressTokenFor(options) {
2005
+ if (options.progressToken !== void 0) return options.progressToken;
2006
+ return this.collectingProgress ? `progress-${++this.progressTokenCounter}` : void 0;
2007
+ }
2008
+ recordNotification = (notification) => {
2009
+ const timestamp = /* @__PURE__ */ new Date();
2010
+ this._notifications.push({ method: notification.method, params: notification.params, timestamp });
2011
+ if (notification.method !== "notifications/progress") return;
2012
+ const { progress, total, progressToken } = notification.params ?? {};
2013
+ this._progressUpdates.push({
2014
+ progress: Number(progress),
2015
+ total: typeof total === "number" ? total : void 0,
2016
+ progressToken: typeof progressToken === "string" || typeof progressToken === "number" ? progressToken : void 0,
2017
+ timestamp
2018
+ });
2019
+ };
1900
2020
  async listResources() {
1901
2021
  return this.request("resources/list", {});
1902
2022
  }
@@ -1943,7 +2063,8 @@ var McpTestClient = class {
1943
2063
  debug: this.config.debug,
1944
2064
  interceptors: this._interceptors,
1945
2065
  clientInfo: this.config.clientInfo,
1946
- elicitationHandler: this._elicitationHandler
2066
+ elicitationHandler: this._elicitationHandler,
2067
+ notificationHandler: this.recordNotification
1947
2068
  });
1948
2069
  case "sse":
1949
2070
  throw new Error("SSE transport not yet implemented");
@@ -5486,18 +5607,17 @@ var toBeError = function(received, expectedCode) {
5486
5607
  message: () => `Expected a result wrapper object with isError property`
5487
5608
  };
5488
5609
  }
5489
- let pass = result.isError;
5490
- if (pass && expectedCode !== void 0) {
5491
- pass = result.error?.code === expectedCode;
5492
- }
5610
+ const receivedCode = typeof expectedCode === "string" ? result.raw?._meta?.["code"] : result.error?.code;
5611
+ const codeMatches = expectedCode === void 0 || receivedCode === expectedCode;
5612
+ const pass = result.isError && codeMatches;
5493
5613
  return {
5494
5614
  pass,
5495
5615
  message: () => {
5496
5616
  if (!result.isError) {
5497
5617
  return "Expected result to be an error, but it was successful";
5498
5618
  }
5499
- if (expectedCode !== void 0 && result.error?.code !== expectedCode) {
5500
- return `Expected error code ${expectedCode}, but got ${result.error?.code}`;
5619
+ if (!codeMatches) {
5620
+ return `Expected error code ${expectedCode}, but got ${String(receivedCode)}`;
5501
5621
  }
5502
5622
  return "Expected result not to be an error";
5503
5623
  }
@@ -363,18 +363,17 @@ var toBeError = function(received, expectedCode) {
363
363
  message: () => `Expected a result wrapper object with isError property`
364
364
  };
365
365
  }
366
- let pass = result.isError;
367
- if (pass && expectedCode !== void 0) {
368
- pass = result.error?.code === expectedCode;
369
- }
366
+ const receivedCode = typeof expectedCode === "string" ? result.raw?._meta?.["code"] : result.error?.code;
367
+ const codeMatches = expectedCode === void 0 || receivedCode === expectedCode;
368
+ const pass = result.isError && codeMatches;
370
369
  return {
371
370
  pass,
372
371
  message: () => {
373
372
  if (!result.isError) {
374
373
  return "Expected result to be an error, but it was successful";
375
374
  }
376
- if (expectedCode !== void 0 && result.error?.code !== expectedCode) {
377
- return `Expected error code ${expectedCode}, but got ${result.error?.code}`;
375
+ if (!codeMatches) {
376
+ return `Expected error code ${expectedCode}, but got ${String(receivedCode)}`;
378
377
  }
379
378
  return "Expected result not to be an error";
380
379
  }
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/testing",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "E2E testing framework for FrontMCP servers - MCP client, auth mocks, Playwright integration",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",
@@ -87,9 +87,9 @@
87
87
  "./esm": null
88
88
  },
89
89
  "peerDependencies": {
90
- "@frontmcp/lazy-zod": "1.8.0",
91
- "@frontmcp/sdk": "1.8.0",
92
- "@frontmcp/ui": "1.8.0",
90
+ "@frontmcp/lazy-zod": "1.8.1",
91
+ "@frontmcp/sdk": "1.8.1",
92
+ "@frontmcp/ui": "1.8.1",
93
93
  "@jest/globals": "^29.0.0 || ^30.0.0",
94
94
  "@playwright/test": "^1.40.0",
95
95
  "jest": "^29.0.0 || ^30.0.0"
@@ -112,8 +112,8 @@
112
112
  "node": ">=24.0.0"
113
113
  },
114
114
  "dependencies": {
115
- "@frontmcp/protocol": "1.8.0",
116
- "@frontmcp/utils": "1.8.0",
115
+ "@frontmcp/protocol": "1.8.1",
116
+ "@frontmcp/utils": "1.8.1",
117
117
  "jose": "^6.0.11",
118
118
  "tslib": "^2.3.0"
119
119
  },
@@ -1046,6 +1046,7 @@ function sleep3(ms) {
1046
1046
 
1047
1047
  // libs/testing/src/transport/streamable-http.transport.ts
1048
1048
  var DEFAULT_TIMEOUT = 3e4;
1049
+ var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
1049
1050
  var StreamableHttpTransport = class {
1050
1051
  config;
1051
1052
  state = "disconnected";
@@ -1057,6 +1058,10 @@ var StreamableHttpTransport = class {
1057
1058
  interceptors;
1058
1059
  publicMode;
1059
1060
  elicitationHandler;
1061
+ notificationHandler;
1062
+ notificationStream;
1063
+ notificationProtocolVersion = "";
1064
+ notificationStreamReopenTimer;
1060
1065
  constructor(config) {
1061
1066
  this.config = {
1062
1067
  baseUrl: config.baseUrl.replace(/\/$/, ""),
@@ -1073,6 +1078,7 @@ var StreamableHttpTransport = class {
1073
1078
  this.interceptors = config.interceptors;
1074
1079
  this.publicMode = config.publicMode ?? false;
1075
1080
  this.elicitationHandler = config.elicitationHandler;
1081
+ this.notificationHandler = config.notificationHandler;
1076
1082
  }
1077
1083
  async connect() {
1078
1084
  this.state = "connecting";
@@ -1292,7 +1298,12 @@ var StreamableHttpTransport = class {
1292
1298
  };
1293
1299
  }
1294
1300
  }
1301
+ async openNotificationStream(protocolVersion) {
1302
+ this.notificationProtocolVersion = protocolVersion;
1303
+ await this.connectNotificationStream();
1304
+ }
1295
1305
  async close() {
1306
+ this.closeNotificationStream();
1296
1307
  this.state = "disconnected";
1297
1308
  this.sessionId = void 0;
1298
1309
  this.log("StreamableHTTP transport closed");
@@ -1331,6 +1342,7 @@ var StreamableHttpTransport = class {
1331
1342
  return { ...this.lastRequestHeaders };
1332
1343
  }
1333
1344
  async simulateDisconnect() {
1345
+ this.closeNotificationStream();
1334
1346
  this.state = "disconnected";
1335
1347
  this.sessionId = void 0;
1336
1348
  }
@@ -1349,6 +1361,72 @@ var StreamableHttpTransport = class {
1349
1361
  // ═══════════════════════════════════════════════════════════════════
1350
1362
  // PRIVATE HELPERS
1351
1363
  // ═══════════════════════════════════════════════════════════════════
1364
+ closeNotificationStream() {
1365
+ clearTimeout(this.notificationStreamReopenTimer);
1366
+ this.notificationStream?.abort();
1367
+ this.notificationStream = void 0;
1368
+ }
1369
+ /** Opens the stream without holding up the caller past the request timeout; close() aborts it at any point. */
1370
+ async connectNotificationStream() {
1371
+ if (!this.sessionId || this.notificationStream) return;
1372
+ const controller = new AbortController();
1373
+ this.notificationStream = controller;
1374
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
1375
+ const established = fetch(this.mcpUrl(), {
1376
+ method: "GET",
1377
+ headers: { ...headers, Accept: "text/event-stream", "MCP-Protocol-Version": this.notificationProtocolVersion },
1378
+ signal: controller.signal
1379
+ }).then(
1380
+ async (response) => {
1381
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
1382
+ if (!response.ok || !response.body || !isEventStream) {
1383
+ await response.body?.cancel();
1384
+ this.log(`Server offers no notification stream (HTTP ${response.status})`);
1385
+ this.releaseNotificationStream(controller);
1386
+ return;
1387
+ }
1388
+ void this.readNotificationStream(controller, response.body.getReader());
1389
+ },
1390
+ (error) => {
1391
+ this.log("Failed to open the notification stream:", error);
1392
+ this.releaseNotificationStream(controller);
1393
+ }
1394
+ );
1395
+ let establishTimer;
1396
+ const establishDeadline = new Promise((resolve) => {
1397
+ establishTimer = setTimeout(resolve, this.config.timeout);
1398
+ });
1399
+ await Promise.race([established, establishDeadline]);
1400
+ clearTimeout(establishTimer);
1401
+ }
1402
+ releaseNotificationStream(controller) {
1403
+ if (this.notificationStream === controller) this.notificationStream = void 0;
1404
+ }
1405
+ async readNotificationStream(controller, reader) {
1406
+ const decoder = new TextDecoder();
1407
+ let buffer = "";
1408
+ try {
1409
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
1410
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1411
+ buffer = remainder;
1412
+ for (const eventText of completeEvents) {
1413
+ for (const event of this.parseSSEEvents(eventText, void 0).events) {
1414
+ await this.handleSSEEvent(event);
1415
+ }
1416
+ }
1417
+ }
1418
+ } catch (error) {
1419
+ this.log("Notification stream ended:", error);
1420
+ } finally {
1421
+ reader.releaseLock();
1422
+ }
1423
+ if (controller.signal.aborted || this.notificationStream !== controller) return;
1424
+ this.releaseNotificationStream(controller);
1425
+ this.notificationStreamReopenTimer = setTimeout(
1426
+ () => void this.connectNotificationStream(),
1427
+ NOTIFICATION_STREAM_REOPEN_DELAY_MS
1428
+ );
1429
+ }
1352
1430
  /**
1353
1431
  * Handle SSE response with elicitation support.
1354
1432
  *
@@ -1486,6 +1564,13 @@ var StreamableHttpTransport = class {
1486
1564
  response: { jsonrpc: "2.0", id: null, result: void 0 }
1487
1565
  };
1488
1566
  }
1567
+ if ("method" in parsed && parsed.id === void 0) {
1568
+ this.notificationHandler?.(parsed);
1569
+ return {
1570
+ isFinal: false,
1571
+ response: { jsonrpc: "2.0", id: null, result: void 0 }
1572
+ };
1573
+ }
1489
1574
  if ("result" in parsed || "error" in parsed) {
1490
1575
  return { isFinal: true, response: parsed };
1491
1576
  }
@@ -1656,6 +1741,13 @@ var StreamableHttpTransport = class {
1656
1741
  };
1657
1742
  }
1658
1743
  };
1744
+ function splitSSEEvents(buffer) {
1745
+ const endsWithCarriageReturn = buffer.endsWith("\r");
1746
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1747
+ const completeEvents = normalized.split("\n\n");
1748
+ const remainder = completeEvents.pop() ?? "";
1749
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
1750
+ }
1659
1751
 
1660
1752
  // libs/testing/src/platform/platform-client-info.ts
1661
1753
  var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
@@ -1792,7 +1884,8 @@ var McpTestClientBuilder = class {
1792
1884
  return this;
1793
1885
  }
1794
1886
  /**
1795
- * Set the MCP protocol version to request
1887
+ * Set the MCP protocol version to request (default '2025-06-18').
1888
+ * '2026-07-28' has no initialize handshake, so `build()` throws for it.
1796
1889
  */
1797
1890
  withProtocolVersion(version) {
1798
1891
  this.config.protocolVersion = version;
@@ -1888,6 +1981,7 @@ var McpTestClientBuilder = class {
1888
1981
  // libs/testing/src/client/mcp-test-client.ts
1889
1982
  var DEFAULT_TIMEOUT2 = 3e4;
1890
1983
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1984
+ var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1891
1985
  var DEFAULT_CLIENT_INFO = {
1892
1986
  name: "@frontmcp/testing",
1893
1987
  version: "0.4.0"
@@ -1924,6 +2018,8 @@ var McpTestClient = class {
1924
2018
  _traces = [];
1925
2019
  _notifications = [];
1926
2020
  _progressUpdates = [];
2021
+ collectingProgress = false;
2022
+ progressTokenCounter = 0;
1927
2023
  // Interceptor chain
1928
2024
  _interceptors;
1929
2025
  // Elicitation handler for server→client elicit requests
@@ -1932,6 +2028,11 @@ var McpTestClient = class {
1932
2028
  // CONSTRUCTOR & FACTORY
1933
2029
  // ═══════════════════════════════════════════════════════════════════
1934
2030
  constructor(config) {
2031
+ if (config.protocolVersion && UNSUPPORTED_PROTOCOL_VERSIONS.includes(config.protocolVersion)) {
2032
+ throw new Error(
2033
+ `McpTestClient does not support protocol version ${config.protocolVersion}: it has no initialize handshake. Use ${DEFAULT_PROTOCOL_VERSION} or earlier.`
2034
+ );
2035
+ }
1935
2036
  this.config = {
1936
2037
  baseUrl: config.baseUrl,
1937
2038
  entryPath: config.entryPath,
@@ -2017,6 +2118,7 @@ var McpTestClient = class {
2017
2118
  jsonrpc: "2.0",
2018
2119
  method: "notifications/initialized"
2019
2120
  });
2121
+ await this.transport.openNotificationStream?.(this.initResult.protocolVersion);
2020
2122
  this.log("info", `Connected to ${this.initResult.serverInfo?.name ?? "MCP Server"}`);
2021
2123
  return this.initResult;
2022
2124
  }
@@ -2127,8 +2229,8 @@ var McpTestClient = class {
2127
2229
  /**
2128
2230
  * Call a tool by name with arguments
2129
2231
  */
2130
- call: async (name, args) => {
2131
- const response = await this.callTool(name, args);
2232
+ call: async (name, args, options = {}) => {
2233
+ const response = await this.callTool(name, args, this.progressTokenFor(options));
2132
2234
  return this.wrapToolResult(response);
2133
2235
  }
2134
2236
  };
@@ -2270,6 +2372,7 @@ var McpTestClient = class {
2270
2372
  * Collect progress notifications specifically
2271
2373
  */
2272
2374
  collectProgress: () => {
2375
+ this.collectingProgress = true;
2273
2376
  return new ProgressCollector(this._progressUpdates);
2274
2377
  },
2275
2378
  /**
@@ -2575,12 +2678,29 @@ var McpTestClient = class {
2575
2678
  async listTools() {
2576
2679
  return this.request("tools/list", {});
2577
2680
  }
2578
- async callTool(name, args) {
2681
+ async callTool(name, args, progressToken) {
2579
2682
  return this.request("tools/call", {
2580
2683
  name,
2581
- arguments: args ?? {}
2684
+ arguments: args ?? {},
2685
+ ...progressToken !== void 0 && { _meta: { progressToken } }
2582
2686
  });
2583
2687
  }
2688
+ progressTokenFor(options) {
2689
+ if (options.progressToken !== void 0) return options.progressToken;
2690
+ return this.collectingProgress ? `progress-${++this.progressTokenCounter}` : void 0;
2691
+ }
2692
+ recordNotification = (notification) => {
2693
+ const timestamp = /* @__PURE__ */ new Date();
2694
+ this._notifications.push({ method: notification.method, params: notification.params, timestamp });
2695
+ if (notification.method !== "notifications/progress") return;
2696
+ const { progress, total, progressToken } = notification.params ?? {};
2697
+ this._progressUpdates.push({
2698
+ progress: Number(progress),
2699
+ total: typeof total === "number" ? total : void 0,
2700
+ progressToken: typeof progressToken === "string" || typeof progressToken === "number" ? progressToken : void 0,
2701
+ timestamp
2702
+ });
2703
+ };
2584
2704
  async listResources() {
2585
2705
  return this.request("resources/list", {});
2586
2706
  }
@@ -2627,7 +2747,8 @@ var McpTestClient = class {
2627
2747
  debug: this.config.debug,
2628
2748
  interceptors: this._interceptors,
2629
2749
  clientInfo: this.config.clientInfo,
2630
- elicitationHandler: this._elicitationHandler
2750
+ elicitationHandler: this._elicitationHandler,
2751
+ notificationHandler: this.recordNotification
2631
2752
  });
2632
2753
  case "sse":
2633
2754
  throw new Error("SSE transport not yet implemented");
package/esm/setup.mjs CHANGED
@@ -397,18 +397,17 @@ var init_mcp_matchers = __esm({
397
397
  message: () => `Expected a result wrapper object with isError property`
398
398
  };
399
399
  }
400
- let pass = result.isError;
401
- if (pass && expectedCode !== void 0) {
402
- pass = result.error?.code === expectedCode;
403
- }
400
+ const receivedCode = typeof expectedCode === "string" ? result.raw?._meta?.["code"] : result.error?.code;
401
+ const codeMatches = expectedCode === void 0 || receivedCode === expectedCode;
402
+ const pass = result.isError && codeMatches;
404
403
  return {
405
404
  pass,
406
405
  message: () => {
407
406
  if (!result.isError) {
408
407
  return "Expected result to be an error, but it was successful";
409
408
  }
410
- if (expectedCode !== void 0 && result.error?.code !== expectedCode) {
411
- return `Expected error code ${expectedCode}, but got ${result.error?.code}`;
409
+ if (!codeMatches) {
410
+ return `Expected error code ${expectedCode}, but got ${String(receivedCode)}`;
412
411
  }
413
412
  return "Expected result not to be an error";
414
413
  }