@frontmcp/testing 1.7.2 → 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.
@@ -772,236 +772,281 @@ function getMeasurementsForProject(project) {
772
772
  return getGlobalMeasurementsArray().filter((m) => m.project === project);
773
773
  }
774
774
 
775
- // libs/testing/src/platform/platform-client-info.ts
776
- var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
777
- function getPlatformClientInfo(platform) {
778
- switch (platform) {
779
- case "openai":
780
- return {
781
- name: "ChatGPT",
782
- version: "1.0"
783
- };
784
- case "ext-apps":
785
- return {
786
- name: "mcp-ext-apps",
787
- version: "1.0"
788
- };
789
- case "claude":
790
- return {
791
- name: "claude-desktop",
792
- version: "1.0"
793
- };
794
- case "cursor":
795
- return {
796
- name: "cursor",
797
- version: "1.0"
798
- };
799
- case "continue":
800
- return {
801
- name: "continue",
802
- version: "1.0"
803
- };
804
- case "cody":
805
- return {
806
- name: "cody",
807
- version: "1.0"
808
- };
809
- case "gemini":
810
- return {
811
- name: "gemini",
812
- version: "1.0"
813
- };
814
- case "generic-mcp":
815
- return {
816
- name: "generic-mcp-client",
817
- version: "1.0"
818
- };
819
- case "unknown":
820
- default:
821
- return {
822
- name: "mcp-test-client",
823
- version: "1.0"
824
- };
825
- }
826
- }
827
- function getPlatformCapabilities(platform) {
828
- const baseCapabilities = {
829
- sampling: {},
830
- // Include elicitation.form by default for testing elicitation workflows
831
- // Note: MCP SDK expects form to be an object, not boolean
832
- elicitation: {
833
- form: {}
834
- }
835
- };
836
- if (platform === "ext-apps") {
775
+ // libs/testing/src/interceptor/mock-registry.ts
776
+ var DefaultMockRegistry = class {
777
+ mocks = [];
778
+ add(mock) {
779
+ const entry = {
780
+ definition: mock,
781
+ callCount: 0,
782
+ calls: [],
783
+ remainingUses: mock.times ?? Infinity
784
+ };
785
+ this.mocks.push(entry);
837
786
  return {
838
- ...baseCapabilities,
839
- experimental: {
840
- [MCP_APPS_EXTENSION_KEY]: {
841
- mimeTypes: ["text/html;profile=mcp-app"]
787
+ remove: () => {
788
+ const index = this.mocks.indexOf(entry);
789
+ if (index !== -1) {
790
+ this.mocks.splice(index, 1);
842
791
  }
843
- }
792
+ },
793
+ callCount: () => entry.callCount,
794
+ calls: () => [...entry.calls]
844
795
  };
845
796
  }
846
- return baseCapabilities;
847
- }
848
-
849
- // libs/testing/src/client/mcp-test-client.builder.ts
850
- var McpTestClientBuilder = class {
851
- config;
852
- constructor(config) {
853
- this.config = { ...config };
797
+ clear() {
798
+ this.mocks = [];
854
799
  }
855
- /**
856
- * Set the authentication configuration
857
- */
858
- withAuth(auth) {
859
- this.config.auth = { ...this.config.auth, ...auth };
860
- return this;
800
+ getAll() {
801
+ return this.mocks.map((e) => e.definition);
802
+ }
803
+ match(request) {
804
+ for (const entry of this.mocks) {
805
+ if (entry.remainingUses <= 0) continue;
806
+ const { definition } = entry;
807
+ if (definition.method !== request.method) continue;
808
+ if (definition.params !== void 0) {
809
+ const params = request.params ?? {};
810
+ if (typeof definition.params === "function") {
811
+ if (!definition.params(params)) continue;
812
+ } else {
813
+ if (!this.paramsMatch(definition.params, params)) continue;
814
+ }
815
+ }
816
+ entry.callCount++;
817
+ entry.calls.push(request);
818
+ entry.remainingUses--;
819
+ return definition;
820
+ }
821
+ return void 0;
861
822
  }
862
823
  /**
863
- * Set the bearer token for authentication
824
+ * Check if request params match the mock params definition
864
825
  */
865
- withToken(token) {
866
- this.config.auth = { ...this.config.auth, token };
867
- return this;
826
+ paramsMatch(expected, actual) {
827
+ for (const [key, value] of Object.entries(expected)) {
828
+ if (!(key in actual)) return false;
829
+ const actualValue = actual[key];
830
+ if (Array.isArray(value)) {
831
+ if (!Array.isArray(actualValue)) return false;
832
+ if (value.length !== actualValue.length) return false;
833
+ for (let i = 0; i < value.length; i++) {
834
+ const expectedItem = value[i];
835
+ const actualItem = actualValue[i];
836
+ if (typeof expectedItem === "object" && expectedItem !== null) {
837
+ if (typeof actualItem !== "object" || actualItem === null) return false;
838
+ if (!this.paramsMatch(expectedItem, actualItem)) {
839
+ return false;
840
+ }
841
+ } else if (actualItem !== expectedItem) {
842
+ return false;
843
+ }
844
+ }
845
+ } else if (typeof value === "object" && value !== null) {
846
+ if (typeof actualValue !== "object" || actualValue === null) return false;
847
+ if (!this.paramsMatch(value, actualValue)) {
848
+ return false;
849
+ }
850
+ } else if (actualValue !== value) {
851
+ return false;
852
+ }
853
+ }
854
+ return true;
868
855
  }
856
+ };
857
+ var mockResponse = {
869
858
  /**
870
- * Add custom headers to all requests
859
+ * Create a successful JSON-RPC response
871
860
  */
872
- withHeaders(headers) {
873
- this.config.auth = {
874
- ...this.config.auth,
875
- headers: { ...this.config.auth?.headers, ...headers }
861
+ success(result, id = 1) {
862
+ return {
863
+ jsonrpc: "2.0",
864
+ id,
865
+ result
876
866
  };
877
- return this;
878
- }
867
+ },
879
868
  /**
880
- * Set the transport type
869
+ * Create an error JSON-RPC response
881
870
  */
882
- withTransport(transport) {
883
- this.config.transport = transport;
884
- return this;
885
- }
871
+ error(code, message, data, id = 1) {
872
+ return {
873
+ jsonrpc: "2.0",
874
+ id,
875
+ error: { code, message, data }
876
+ };
877
+ },
886
878
  /**
887
- * Set the request timeout in milliseconds
879
+ * Create a tool result response
888
880
  */
889
- withTimeout(timeoutMs) {
890
- this.config.timeout = timeoutMs;
891
- return this;
892
- }
881
+ toolResult(content, id = 1) {
882
+ return {
883
+ jsonrpc: "2.0",
884
+ id,
885
+ result: { content }
886
+ };
887
+ },
893
888
  /**
894
- * Enable debug logging
889
+ * Create a tools/list response
895
890
  */
896
- withDebug(enabled = true) {
897
- this.config.debug = enabled;
898
- return this;
899
- }
891
+ toolsList(tools, id = 1) {
892
+ return {
893
+ jsonrpc: "2.0",
894
+ id,
895
+ result: { tools }
896
+ };
897
+ },
900
898
  /**
901
- * Enable public mode - skip authentication entirely.
902
- * When true, no Authorization header is sent and anonymous token is not requested.
903
- * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
899
+ * Create a resources/list response
904
900
  */
905
- withPublicMode(enabled = true) {
906
- this.config.publicMode = enabled;
907
- return this;
908
- }
901
+ resourcesList(resources, id = 1) {
902
+ return {
903
+ jsonrpc: "2.0",
904
+ id,
905
+ result: { resources }
906
+ };
907
+ },
909
908
  /**
910
- * Set the MCP protocol version to request
909
+ * Create a resources/read response
911
910
  */
912
- withProtocolVersion(version) {
913
- this.config.protocolVersion = version;
914
- return this;
915
- }
911
+ resourceRead(contents, id = 1) {
912
+ return {
913
+ jsonrpc: "2.0",
914
+ id,
915
+ result: { contents }
916
+ };
917
+ },
916
918
  /**
917
- * Set the client info sent during initialization
919
+ * Common MCP errors
918
920
  */
919
- withClientInfo(info) {
920
- this.config.clientInfo = info;
921
- return this;
921
+ errors: {
922
+ methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
923
+ invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
924
+ internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
925
+ resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
926
+ toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
927
+ unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
928
+ forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
929
+ }
930
+ };
931
+
932
+ // libs/testing/src/interceptor/interceptor-chain.ts
933
+ var DefaultInterceptorChain = class {
934
+ request = [];
935
+ response = [];
936
+ mocks;
937
+ constructor() {
938
+ this.mocks = new DefaultMockRegistry();
922
939
  }
923
940
  /**
924
- * Set the platform type for testing platform-specific meta keys.
925
- * Automatically configures clientInfo and capabilities for platform detection.
926
- *
927
- * Platform-specific behavior:
928
- * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
929
- * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
930
- * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
931
- * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
932
- * - Other platforms follow similar patterns
933
- *
934
- * @example
935
- * ```typescript
936
- * const client = await McpTestClient.create({ baseUrl })
937
- * .withPlatform('openai')
938
- * .buildAndConnect();
939
- *
940
- * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
941
- * const extAppsClient = await McpTestClient.create({ baseUrl })
942
- * .withPlatform('ext-apps')
943
- * .buildAndConnect();
944
- * ```
941
+ * Add a request interceptor
945
942
  */
946
- withPlatform(platform) {
947
- this.config.platform = platform;
948
- this.config.clientInfo = getPlatformClientInfo(platform);
949
- this.config.capabilities = getPlatformCapabilities(platform);
950
- return this;
943
+ addRequestInterceptor(interceptor) {
944
+ this.request.push(interceptor);
945
+ return () => {
946
+ const index = this.request.indexOf(interceptor);
947
+ if (index !== -1) {
948
+ this.request.splice(index, 1);
949
+ }
950
+ };
951
951
  }
952
952
  /**
953
- * Set custom client capabilities for MCP initialization.
954
- * Use this for fine-grained control over capabilities sent during initialization.
955
- *
956
- * @example
957
- * ```typescript
958
- * const client = await McpTestClient.create({ baseUrl })
959
- * .withCapabilities({
960
- * sampling: {},
961
- * experimental: {
962
- * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
963
- * }
964
- * })
965
- * .buildAndConnect();
966
- * ```
953
+ * Add a response interceptor
967
954
  */
968
- withCapabilities(capabilities) {
969
- this.config.capabilities = capabilities;
970
- return this;
955
+ addResponseInterceptor(interceptor) {
956
+ this.response.push(interceptor);
957
+ return () => {
958
+ const index = this.response.indexOf(interceptor);
959
+ if (index !== -1) {
960
+ this.response.splice(index, 1);
961
+ }
962
+ };
971
963
  }
972
964
  /**
973
- * Set query parameters to append to the connection URL.
974
- * Useful for testing mode switches like `?mode=skills_only`.
975
- *
976
- * @example
977
- * ```typescript
978
- * const client = await McpTestClient.create({ baseUrl })
979
- * .withQueryParams({ mode: 'skills_only' })
980
- * .buildAndConnect();
981
- * ```
965
+ * Process a request through the interceptor chain
966
+ * Returns either:
967
+ * - { type: 'continue', request } - continue with (possibly modified) request
968
+ * - { type: 'mock', response } - return mock response immediately
969
+ * - { type: 'error', error } - throw error
982
970
  */
983
- withQueryParams(params) {
984
- this.config.queryParams = { ...this.config.queryParams, ...params };
985
- return this;
971
+ async processRequest(request, meta) {
972
+ let currentRequest = request;
973
+ const mockDef = this.mocks.match(request);
974
+ if (mockDef) {
975
+ if (mockDef.delay && mockDef.delay > 0) {
976
+ await sleep3(mockDef.delay);
977
+ }
978
+ let mockResponse2;
979
+ if (typeof mockDef.response === "function") {
980
+ mockResponse2 = await mockDef.response(request);
981
+ } else {
982
+ mockResponse2 = mockDef.response;
983
+ }
984
+ return {
985
+ type: "mock",
986
+ response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
987
+ };
988
+ }
989
+ for (const interceptor of this.request) {
990
+ const ctx = {
991
+ request: currentRequest,
992
+ meta
993
+ };
994
+ const result = await interceptor(ctx);
995
+ switch (result.action) {
996
+ case "passthrough":
997
+ break;
998
+ case "modify":
999
+ currentRequest = result.request;
1000
+ break;
1001
+ case "mock":
1002
+ return {
1003
+ type: "mock",
1004
+ response: { ...result.response, id: request.id ?? result.response.id }
1005
+ };
1006
+ case "error":
1007
+ return { type: "error", error: result.error };
1008
+ }
1009
+ }
1010
+ return { type: "continue", request: currentRequest };
986
1011
  }
987
1012
  /**
988
- * Build the McpTestClient instance (does not connect)
1013
+ * Process a response through the interceptor chain
989
1014
  */
990
- build() {
991
- return new McpTestClient(this.config);
1015
+ async processResponse(request, response, durationMs) {
1016
+ let currentResponse = response;
1017
+ for (const interceptor of this.response) {
1018
+ const ctx = {
1019
+ request,
1020
+ response: currentResponse,
1021
+ durationMs
1022
+ };
1023
+ const result = await interceptor(ctx);
1024
+ switch (result.action) {
1025
+ case "passthrough":
1026
+ break;
1027
+ case "modify":
1028
+ currentResponse = result.response;
1029
+ break;
1030
+ }
1031
+ }
1032
+ return currentResponse;
992
1033
  }
993
1034
  /**
994
- * Build the McpTestClient and connect to the server
1035
+ * Clear all interceptors and mocks
995
1036
  */
996
- async buildAndConnect() {
997
- const client = this.build();
998
- await client.connect();
999
- return client;
1037
+ clear() {
1038
+ this.request = [];
1039
+ this.response = [];
1040
+ this.mocks.clear();
1000
1041
  }
1001
1042
  };
1043
+ function sleep3(ms) {
1044
+ return new Promise((resolve) => setTimeout(resolve, ms));
1045
+ }
1002
1046
 
1003
1047
  // libs/testing/src/transport/streamable-http.transport.ts
1004
1048
  var DEFAULT_TIMEOUT = 3e4;
1049
+ var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
1005
1050
  var StreamableHttpTransport = class {
1006
1051
  config;
1007
1052
  state = "disconnected";
@@ -1013,10 +1058,15 @@ var StreamableHttpTransport = class {
1013
1058
  interceptors;
1014
1059
  publicMode;
1015
1060
  elicitationHandler;
1061
+ notificationHandler;
1062
+ notificationStream;
1063
+ notificationProtocolVersion = "";
1064
+ notificationStreamReopenTimer;
1016
1065
  constructor(config) {
1017
1066
  this.config = {
1018
1067
  baseUrl: config.baseUrl.replace(/\/$/, ""),
1019
1068
  // Remove trailing slash
1069
+ entryPath: config.entryPath,
1020
1070
  timeout: config.timeout ?? DEFAULT_TIMEOUT,
1021
1071
  auth: config.auth ?? {},
1022
1072
  publicMode: config.publicMode ?? false,
@@ -1028,6 +1078,7 @@ var StreamableHttpTransport = class {
1028
1078
  this.interceptors = config.interceptors;
1029
1079
  this.publicMode = config.publicMode ?? false;
1030
1080
  this.elicitationHandler = config.elicitationHandler;
1081
+ this.notificationHandler = config.notificationHandler;
1031
1082
  }
1032
1083
  async connect() {
1033
1084
  this.state = "connecting";
@@ -1109,7 +1160,7 @@ var StreamableHttpTransport = class {
1109
1160
  }
1110
1161
  const headers = this.buildHeaders();
1111
1162
  this.lastRequestHeaders = headers;
1112
- const url = `${this.config.baseUrl}/`;
1163
+ const url = this.mcpUrl();
1113
1164
  this.log(`POST ${url}`, message);
1114
1165
  const controller = new AbortController();
1115
1166
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -1179,7 +1230,7 @@ var StreamableHttpTransport = class {
1179
1230
  this.ensureConnected();
1180
1231
  const headers = this.buildHeaders();
1181
1232
  this.lastRequestHeaders = headers;
1182
- const url = `${this.config.baseUrl}/`;
1233
+ const url = this.mcpUrl();
1183
1234
  this.log(`POST ${url} (notification)`, message);
1184
1235
  const controller = new AbortController();
1185
1236
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -1210,7 +1261,7 @@ var StreamableHttpTransport = class {
1210
1261
  this.ensureConnected();
1211
1262
  const headers = this.buildHeaders();
1212
1263
  this.lastRequestHeaders = headers;
1213
- const url = `${this.config.baseUrl}/`;
1264
+ const url = this.mcpUrl();
1214
1265
  this.log(`POST ${url} (raw)`, data);
1215
1266
  const controller = new AbortController();
1216
1267
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -1247,7 +1298,12 @@ var StreamableHttpTransport = class {
1247
1298
  };
1248
1299
  }
1249
1300
  }
1301
+ async openNotificationStream(protocolVersion) {
1302
+ this.notificationProtocolVersion = protocolVersion;
1303
+ await this.connectNotificationStream();
1304
+ }
1250
1305
  async close() {
1306
+ this.closeNotificationStream();
1251
1307
  this.state = "disconnected";
1252
1308
  this.sessionId = void 0;
1253
1309
  this.log("StreamableHTTP transport closed");
@@ -1286,6 +1342,7 @@ var StreamableHttpTransport = class {
1286
1342
  return { ...this.lastRequestHeaders };
1287
1343
  }
1288
1344
  async simulateDisconnect() {
1345
+ this.closeNotificationStream();
1289
1346
  this.state = "disconnected";
1290
1347
  this.sessionId = void 0;
1291
1348
  }
@@ -1304,6 +1361,72 @@ var StreamableHttpTransport = class {
1304
1361
  // ═══════════════════════════════════════════════════════════════════
1305
1362
  // PRIVATE HELPERS
1306
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
+ }
1307
1430
  /**
1308
1431
  * Handle SSE response with elicitation support.
1309
1432
  *
@@ -1441,6 +1564,13 @@ var StreamableHttpTransport = class {
1441
1564
  response: { jsonrpc: "2.0", id: null, result: void 0 }
1442
1565
  };
1443
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
+ }
1444
1574
  if ("result" in parsed || "error" in parsed) {
1445
1575
  return { isFinal: true, response: parsed };
1446
1576
  }
@@ -1493,7 +1623,7 @@ var StreamableHttpTransport = class {
1493
1623
  */
1494
1624
  async sendElicitationResponse(requestId, response) {
1495
1625
  const headers = this.buildHeaders();
1496
- const url = `${this.config.baseUrl}/`;
1626
+ const url = this.mcpUrl();
1497
1627
  const rpcResponse = {
1498
1628
  jsonrpc: "2.0",
1499
1629
  id: requestId,
@@ -1513,6 +1643,18 @@ var StreamableHttpTransport = class {
1513
1643
  this.log("Failed to send elicitation response:", error);
1514
1644
  }
1515
1645
  }
1646
+ /**
1647
+ * The MCP endpoint URL. `entryPath` is where the server mounts MCP; the
1648
+ * trailing slash is kept because both hosts normalize it and the original
1649
+ * behaviour (`${baseUrl}/`) relied on it (issue #543).
1650
+ */
1651
+ mcpUrl() {
1652
+ const url = new URL(this.config.baseUrl);
1653
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
1654
+ const basePath = url.pathname.replace(/\/+$/, "");
1655
+ url.pathname = entry ? `${basePath}/${entry}` : `${basePath}/`;
1656
+ return url.toString();
1657
+ }
1516
1658
  buildHeaders() {
1517
1659
  const headers = {
1518
1660
  "Content-Type": "application/json",
@@ -1599,286 +1741,266 @@ var StreamableHttpTransport = class {
1599
1741
  };
1600
1742
  }
1601
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
+ }
1602
1751
 
1603
- // libs/testing/src/interceptor/mock-registry.ts
1604
- var DefaultMockRegistry = class {
1605
- mocks = [];
1606
- add(mock) {
1607
- const entry = {
1608
- definition: mock,
1609
- callCount: 0,
1610
- calls: [],
1611
- remainingUses: mock.times ?? Infinity
1612
- };
1613
- this.mocks.push(entry);
1752
+ // libs/testing/src/platform/platform-client-info.ts
1753
+ var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
1754
+ function getPlatformClientInfo(platform) {
1755
+ switch (platform) {
1756
+ case "openai":
1757
+ return {
1758
+ name: "ChatGPT",
1759
+ version: "1.0"
1760
+ };
1761
+ case "ext-apps":
1762
+ return {
1763
+ name: "mcp-ext-apps",
1764
+ version: "1.0"
1765
+ };
1766
+ case "claude":
1767
+ return {
1768
+ name: "claude-desktop",
1769
+ version: "1.0"
1770
+ };
1771
+ case "cursor":
1772
+ return {
1773
+ name: "cursor",
1774
+ version: "1.0"
1775
+ };
1776
+ case "continue":
1777
+ return {
1778
+ name: "continue",
1779
+ version: "1.0"
1780
+ };
1781
+ case "cody":
1782
+ return {
1783
+ name: "cody",
1784
+ version: "1.0"
1785
+ };
1786
+ case "gemini":
1787
+ return {
1788
+ name: "gemini",
1789
+ version: "1.0"
1790
+ };
1791
+ case "generic-mcp":
1792
+ return {
1793
+ name: "generic-mcp-client",
1794
+ version: "1.0"
1795
+ };
1796
+ case "unknown":
1797
+ default:
1798
+ return {
1799
+ name: "mcp-test-client",
1800
+ version: "1.0"
1801
+ };
1802
+ }
1803
+ }
1804
+ function getPlatformCapabilities(platform) {
1805
+ const baseCapabilities = {
1806
+ sampling: {},
1807
+ // Include elicitation.form by default for testing elicitation workflows
1808
+ // Note: MCP SDK expects form to be an object, not boolean
1809
+ elicitation: {
1810
+ form: {}
1811
+ }
1812
+ };
1813
+ if (platform === "ext-apps") {
1614
1814
  return {
1615
- remove: () => {
1616
- const index = this.mocks.indexOf(entry);
1617
- if (index !== -1) {
1618
- this.mocks.splice(index, 1);
1815
+ ...baseCapabilities,
1816
+ experimental: {
1817
+ [MCP_APPS_EXTENSION_KEY]: {
1818
+ mimeTypes: ["text/html;profile=mcp-app"]
1619
1819
  }
1620
- },
1621
- callCount: () => entry.callCount,
1622
- calls: () => [...entry.calls]
1820
+ }
1623
1821
  };
1624
1822
  }
1625
- clear() {
1626
- this.mocks = [];
1627
- }
1628
- getAll() {
1629
- return this.mocks.map((e) => e.definition);
1630
- }
1631
- match(request) {
1632
- for (const entry of this.mocks) {
1633
- if (entry.remainingUses <= 0) continue;
1634
- const { definition } = entry;
1635
- if (definition.method !== request.method) continue;
1636
- if (definition.params !== void 0) {
1637
- const params = request.params ?? {};
1638
- if (typeof definition.params === "function") {
1639
- if (!definition.params(params)) continue;
1640
- } else {
1641
- if (!this.paramsMatch(definition.params, params)) continue;
1642
- }
1643
- }
1644
- entry.callCount++;
1645
- entry.calls.push(request);
1646
- entry.remainingUses--;
1647
- return definition;
1648
- }
1649
- return void 0;
1823
+ return baseCapabilities;
1824
+ }
1825
+
1826
+ // libs/testing/src/client/mcp-test-client.builder.ts
1827
+ var McpTestClientBuilder = class {
1828
+ config;
1829
+ constructor(config) {
1830
+ this.config = { ...config };
1650
1831
  }
1651
1832
  /**
1652
- * Check if request params match the mock params definition
1833
+ * Set the authentication configuration
1653
1834
  */
1654
- paramsMatch(expected, actual) {
1655
- for (const [key, value] of Object.entries(expected)) {
1656
- if (!(key in actual)) return false;
1657
- const actualValue = actual[key];
1658
- if (Array.isArray(value)) {
1659
- if (!Array.isArray(actualValue)) return false;
1660
- if (value.length !== actualValue.length) return false;
1661
- for (let i = 0; i < value.length; i++) {
1662
- const expectedItem = value[i];
1663
- const actualItem = actualValue[i];
1664
- if (typeof expectedItem === "object" && expectedItem !== null) {
1665
- if (typeof actualItem !== "object" || actualItem === null) return false;
1666
- if (!this.paramsMatch(expectedItem, actualItem)) {
1667
- return false;
1668
- }
1669
- } else if (actualItem !== expectedItem) {
1670
- return false;
1671
- }
1672
- }
1673
- } else if (typeof value === "object" && value !== null) {
1674
- if (typeof actualValue !== "object" || actualValue === null) return false;
1675
- if (!this.paramsMatch(value, actualValue)) {
1676
- return false;
1677
- }
1678
- } else if (actualValue !== value) {
1679
- return false;
1680
- }
1681
- }
1682
- return true;
1835
+ withAuth(auth) {
1836
+ this.config.auth = { ...this.config.auth, ...auth };
1837
+ return this;
1683
1838
  }
1684
- };
1685
- var mockResponse = {
1686
1839
  /**
1687
- * Create a successful JSON-RPC response
1840
+ * Set the bearer token for authentication
1688
1841
  */
1689
- success(result, id = 1) {
1690
- return {
1691
- jsonrpc: "2.0",
1692
- id,
1693
- result
1694
- };
1695
- },
1842
+ withToken(token) {
1843
+ this.config.auth = { ...this.config.auth, token };
1844
+ return this;
1845
+ }
1696
1846
  /**
1697
- * Create an error JSON-RPC response
1847
+ * Add custom headers to all requests
1698
1848
  */
1699
- error(code, message, data, id = 1) {
1700
- return {
1701
- jsonrpc: "2.0",
1702
- id,
1703
- error: { code, message, data }
1849
+ withHeaders(headers) {
1850
+ this.config.auth = {
1851
+ ...this.config.auth,
1852
+ headers: { ...this.config.auth?.headers, ...headers }
1704
1853
  };
1705
- },
1854
+ return this;
1855
+ }
1706
1856
  /**
1707
- * Create a tool result response
1857
+ * Set the transport type
1708
1858
  */
1709
- toolResult(content, id = 1) {
1710
- return {
1711
- jsonrpc: "2.0",
1712
- id,
1713
- result: { content }
1714
- };
1715
- },
1859
+ withTransport(transport) {
1860
+ this.config.transport = transport;
1861
+ return this;
1862
+ }
1716
1863
  /**
1717
- * Create a tools/list response
1864
+ * Set the request timeout in milliseconds
1718
1865
  */
1719
- toolsList(tools, id = 1) {
1720
- return {
1721
- jsonrpc: "2.0",
1722
- id,
1723
- result: { tools }
1724
- };
1725
- },
1866
+ withTimeout(timeoutMs) {
1867
+ this.config.timeout = timeoutMs;
1868
+ return this;
1869
+ }
1726
1870
  /**
1727
- * Create a resources/list response
1871
+ * Enable debug logging
1728
1872
  */
1729
- resourcesList(resources, id = 1) {
1730
- return {
1731
- jsonrpc: "2.0",
1732
- id,
1733
- result: { resources }
1734
- };
1735
- },
1873
+ withDebug(enabled = true) {
1874
+ this.config.debug = enabled;
1875
+ return this;
1876
+ }
1736
1877
  /**
1737
- * Create a resources/read response
1878
+ * Enable public mode - skip authentication entirely.
1879
+ * When true, no Authorization header is sent and anonymous token is not requested.
1880
+ * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
1738
1881
  */
1739
- resourceRead(contents, id = 1) {
1740
- return {
1741
- jsonrpc: "2.0",
1742
- id,
1743
- result: { contents }
1744
- };
1745
- },
1882
+ withPublicMode(enabled = true) {
1883
+ this.config.publicMode = enabled;
1884
+ return this;
1885
+ }
1746
1886
  /**
1747
- * Common MCP errors
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.
1748
1889
  */
1749
- errors: {
1750
- methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
1751
- invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
1752
- internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
1753
- resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
1754
- toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
1755
- unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
1756
- forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
1890
+ withProtocolVersion(version) {
1891
+ this.config.protocolVersion = version;
1892
+ return this;
1757
1893
  }
1758
- };
1759
-
1760
- // libs/testing/src/interceptor/interceptor-chain.ts
1761
- var DefaultInterceptorChain = class {
1762
- request = [];
1763
- response = [];
1764
- mocks;
1765
- constructor() {
1766
- this.mocks = new DefaultMockRegistry();
1894
+ /**
1895
+ * Set the client info sent during initialization
1896
+ */
1897
+ withClientInfo(info) {
1898
+ this.config.clientInfo = info;
1899
+ return this;
1767
1900
  }
1768
1901
  /**
1769
- * Add a request interceptor
1902
+ * Set the platform type for testing platform-specific meta keys.
1903
+ * Automatically configures clientInfo and capabilities for platform detection.
1904
+ *
1905
+ * Platform-specific behavior:
1906
+ * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
1907
+ * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
1908
+ * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
1909
+ * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
1910
+ * - Other platforms follow similar patterns
1911
+ *
1912
+ * @example
1913
+ * ```typescript
1914
+ * const client = await McpTestClient.create({ baseUrl })
1915
+ * .withPlatform('openai')
1916
+ * .buildAndConnect();
1917
+ *
1918
+ * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
1919
+ * const extAppsClient = await McpTestClient.create({ baseUrl })
1920
+ * .withPlatform('ext-apps')
1921
+ * .buildAndConnect();
1922
+ * ```
1770
1923
  */
1771
- addRequestInterceptor(interceptor) {
1772
- this.request.push(interceptor);
1773
- return () => {
1774
- const index = this.request.indexOf(interceptor);
1775
- if (index !== -1) {
1776
- this.request.splice(index, 1);
1777
- }
1778
- };
1924
+ withPlatform(platform) {
1925
+ this.config.platform = platform;
1926
+ this.config.clientInfo = getPlatformClientInfo(platform);
1927
+ this.config.capabilities = getPlatformCapabilities(platform);
1928
+ return this;
1779
1929
  }
1780
1930
  /**
1781
- * Add a response interceptor
1931
+ * Set custom client capabilities for MCP initialization.
1932
+ * Use this for fine-grained control over capabilities sent during initialization.
1933
+ *
1934
+ * @example
1935
+ * ```typescript
1936
+ * const client = await McpTestClient.create({ baseUrl })
1937
+ * .withCapabilities({
1938
+ * sampling: {},
1939
+ * experimental: {
1940
+ * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
1941
+ * }
1942
+ * })
1943
+ * .buildAndConnect();
1944
+ * ```
1782
1945
  */
1783
- addResponseInterceptor(interceptor) {
1784
- this.response.push(interceptor);
1785
- return () => {
1786
- const index = this.response.indexOf(interceptor);
1787
- if (index !== -1) {
1788
- this.response.splice(index, 1);
1789
- }
1790
- };
1946
+ withCapabilities(capabilities) {
1947
+ this.config.capabilities = capabilities;
1948
+ return this;
1791
1949
  }
1792
1950
  /**
1793
- * Process a request through the interceptor chain
1794
- * Returns either:
1795
- * - { type: 'continue', request } - continue with (possibly modified) request
1796
- * - { type: 'mock', response } - return mock response immediately
1797
- * - { type: 'error', error } - throw error
1951
+ * Set query parameters to append to the connection URL.
1952
+ * Useful for testing mode switches like `?mode=skills_only`.
1953
+ *
1954
+ * @example
1955
+ * ```typescript
1956
+ * const client = await McpTestClient.create({ baseUrl })
1957
+ * .withQueryParams({ mode: 'skills_only' })
1958
+ * .buildAndConnect();
1959
+ * ```
1798
1960
  */
1799
- async processRequest(request, meta) {
1800
- let currentRequest = request;
1801
- const mockDef = this.mocks.match(request);
1802
- if (mockDef) {
1803
- if (mockDef.delay && mockDef.delay > 0) {
1804
- await sleep3(mockDef.delay);
1805
- }
1806
- let mockResponse2;
1807
- if (typeof mockDef.response === "function") {
1808
- mockResponse2 = await mockDef.response(request);
1809
- } else {
1810
- mockResponse2 = mockDef.response;
1811
- }
1812
- return {
1813
- type: "mock",
1814
- response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
1815
- };
1816
- }
1817
- for (const interceptor of this.request) {
1818
- const ctx = {
1819
- request: currentRequest,
1820
- meta
1821
- };
1822
- const result = await interceptor(ctx);
1823
- switch (result.action) {
1824
- case "passthrough":
1825
- break;
1826
- case "modify":
1827
- currentRequest = result.request;
1828
- break;
1829
- case "mock":
1830
- return {
1831
- type: "mock",
1832
- response: { ...result.response, id: request.id ?? result.response.id }
1833
- };
1834
- case "error":
1835
- return { type: "error", error: result.error };
1836
- }
1837
- }
1838
- return { type: "continue", request: currentRequest };
1961
+ withQueryParams(params) {
1962
+ this.config.queryParams = { ...this.config.queryParams, ...params };
1963
+ return this;
1839
1964
  }
1840
1965
  /**
1841
- * Process a response through the interceptor chain
1966
+ * Build the McpTestClient instance (does not connect)
1842
1967
  */
1843
- async processResponse(request, response, durationMs) {
1844
- let currentResponse = response;
1845
- for (const interceptor of this.response) {
1846
- const ctx = {
1847
- request,
1848
- response: currentResponse,
1849
- durationMs
1850
- };
1851
- const result = await interceptor(ctx);
1852
- switch (result.action) {
1853
- case "passthrough":
1854
- break;
1855
- case "modify":
1856
- currentResponse = result.response;
1857
- break;
1858
- }
1859
- }
1860
- return currentResponse;
1968
+ build() {
1969
+ return new McpTestClient(this.config);
1861
1970
  }
1862
1971
  /**
1863
- * Clear all interceptors and mocks
1972
+ * Build the McpTestClient and connect to the server
1864
1973
  */
1865
- clear() {
1866
- this.request = [];
1867
- this.response = [];
1868
- this.mocks.clear();
1974
+ async buildAndConnect() {
1975
+ const client = this.build();
1976
+ await client.connect();
1977
+ return client;
1869
1978
  }
1870
1979
  };
1871
- function sleep3(ms) {
1872
- return new Promise((resolve) => setTimeout(resolve, ms));
1873
- }
1874
1980
 
1875
1981
  // libs/testing/src/client/mcp-test-client.ts
1876
1982
  var DEFAULT_TIMEOUT2 = 3e4;
1877
1983
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1984
+ var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1878
1985
  var DEFAULT_CLIENT_INFO = {
1879
1986
  name: "@frontmcp/testing",
1880
1987
  version: "0.4.0"
1881
1988
  };
1989
+ function extractEntryPaths(data) {
1990
+ let payload = data;
1991
+ if (typeof payload === "string") {
1992
+ try {
1993
+ payload = JSON.parse(payload);
1994
+ } catch {
1995
+ return void 0;
1996
+ }
1997
+ }
1998
+ if (!payload || typeof payload !== "object") return void 0;
1999
+ const paths = payload.entryPaths;
2000
+ if (!Array.isArray(paths)) return void 0;
2001
+ const strings = paths.filter((p) => typeof p === "string");
2002
+ return strings.length > 0 ? strings : void 0;
2003
+ }
1882
2004
  var McpTestClient = class {
1883
2005
  // Platform, capabilities, and queryParams are optional - only set when needed
1884
2006
  config;
@@ -1887,6 +2009,8 @@ var McpTestClient = class {
1887
2009
  requestIdCounter = 0;
1888
2010
  _lastRequestId = 0;
1889
2011
  _sessionId;
2012
+ /** #543 — discovery is attempted at most once per connect. */
2013
+ triedDiscoveredEntryPath = false;
1890
2014
  _sessionInfo = null;
1891
2015
  _authState = { isAnonymous: true, scopes: [] };
1892
2016
  // Logging and tracing
@@ -1894,6 +2018,8 @@ var McpTestClient = class {
1894
2018
  _traces = [];
1895
2019
  _notifications = [];
1896
2020
  _progressUpdates = [];
2021
+ collectingProgress = false;
2022
+ progressTokenCounter = 0;
1897
2023
  // Interceptor chain
1898
2024
  _interceptors;
1899
2025
  // Elicitation handler for server→client elicit requests
@@ -1902,8 +2028,14 @@ var McpTestClient = class {
1902
2028
  // CONSTRUCTOR & FACTORY
1903
2029
  // ═══════════════════════════════════════════════════════════════════
1904
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
+ }
1905
2036
  this.config = {
1906
2037
  baseUrl: config.baseUrl,
2038
+ entryPath: config.entryPath,
1907
2039
  transport: config.transport ?? "streamable-http",
1908
2040
  auth: config.auth ?? {},
1909
2041
  publicMode: config.publicMode ?? false,
@@ -1939,11 +2071,13 @@ var McpTestClient = class {
1939
2071
  */
1940
2072
  async connect() {
1941
2073
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
2074
+ this.triedDiscoveredEntryPath = false;
1942
2075
  this.transport = this.createTransport();
1943
2076
  await this.transport.connect();
1944
2077
  const maxRetries = 3;
1945
2078
  const retryDelayMs = 500;
1946
2079
  let lastError;
2080
+ let reportedEntryPaths;
1947
2081
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
1948
2082
  const initResponse = await this.initialize();
1949
2083
  if (initResponse.success && initResponse.data) {
@@ -1951,13 +2085,27 @@ var McpTestClient = class {
1951
2085
  break;
1952
2086
  }
1953
2087
  lastError = initResponse.error?.message ?? "Unknown error";
2088
+ reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
2089
+ if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
2090
+ this.triedDiscoveredEntryPath = true;
2091
+ const discovered = reportedEntryPaths[0];
2092
+ this.log("info", `Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);
2093
+ this.config.entryPath = discovered;
2094
+ await this.transport.close();
2095
+ this.transport = this.createTransport();
2096
+ await this.transport.connect();
2097
+ continue;
2098
+ }
1954
2099
  if (attempt < maxRetries) {
1955
2100
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1956
2101
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
1957
2102
  }
1958
2103
  }
1959
2104
  if (!this.initResult) {
1960
- throw new Error(`Failed to initialize MCP connection after ${maxRetries} attempts: ${lastError}`);
2105
+ const served = reportedEntryPaths?.length ? ` The server reports it serves MCP at ${reportedEntryPaths.join(", ")}; set \`entryPath\` in test.use() (or \`http.entryPath\` on the server) to match.` : "";
2106
+ throw new Error(
2107
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2108
+ );
1961
2109
  }
1962
2110
  this._sessionId = this.transport.getSessionId();
1963
2111
  this._sessionInfo = {
@@ -1970,6 +2118,7 @@ var McpTestClient = class {
1970
2118
  jsonrpc: "2.0",
1971
2119
  method: "notifications/initialized"
1972
2120
  });
2121
+ await this.transport.openNotificationStream?.(this.initResult.protocolVersion);
1973
2122
  this.log("info", `Connected to ${this.initResult.serverInfo?.name ?? "MCP Server"}`);
1974
2123
  return this.initResult;
1975
2124
  }
@@ -2080,8 +2229,8 @@ var McpTestClient = class {
2080
2229
  /**
2081
2230
  * Call a tool by name with arguments
2082
2231
  */
2083
- call: async (name, args) => {
2084
- const response = await this.callTool(name, args);
2232
+ call: async (name, args, options = {}) => {
2233
+ const response = await this.callTool(name, args, this.progressTokenFor(options));
2085
2234
  return this.wrapToolResult(response);
2086
2235
  }
2087
2236
  };
@@ -2223,6 +2372,7 @@ var McpTestClient = class {
2223
2372
  * Collect progress notifications specifically
2224
2373
  */
2225
2374
  collectProgress: () => {
2375
+ this.collectingProgress = true;
2226
2376
  return new ProgressCollector(this._progressUpdates);
2227
2377
  },
2228
2378
  /**
@@ -2528,12 +2678,29 @@ var McpTestClient = class {
2528
2678
  async listTools() {
2529
2679
  return this.request("tools/list", {});
2530
2680
  }
2531
- async callTool(name, args) {
2681
+ async callTool(name, args, progressToken) {
2532
2682
  return this.request("tools/call", {
2533
2683
  name,
2534
- arguments: args ?? {}
2684
+ arguments: args ?? {},
2685
+ ...progressToken !== void 0 && { _meta: { progressToken } }
2535
2686
  });
2536
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
+ };
2537
2704
  async listResources() {
2538
2705
  return this.request("resources/list", {});
2539
2706
  }
@@ -2555,6 +2722,11 @@ var McpTestClient = class {
2555
2722
  // ═══════════════════════════════════════════════════════════════════
2556
2723
  // PRIVATE: TRANSPORT & REQUEST HELPERS
2557
2724
  // ═══════════════════════════════════════════════════════════════════
2725
+ /** The MCP path this client is currently pointed at, for log and error text. */
2726
+ mcpPathLabel() {
2727
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
2728
+ return entry ? `/${entry}` : "/";
2729
+ }
2558
2730
  createTransport() {
2559
2731
  let baseUrl = this.config.baseUrl;
2560
2732
  if (this.config.queryParams && Object.keys(this.config.queryParams).length > 0) {
@@ -2568,13 +2740,15 @@ var McpTestClient = class {
2568
2740
  case "streamable-http":
2569
2741
  return new StreamableHttpTransport({
2570
2742
  baseUrl,
2743
+ entryPath: this.config.entryPath,
2571
2744
  timeout: this.config.timeout,
2572
2745
  auth: this.config.auth,
2573
2746
  publicMode: this.config.publicMode,
2574
2747
  debug: this.config.debug,
2575
2748
  interceptors: this._interceptors,
2576
2749
  clientInfo: this.config.clientInfo,
2577
- elicitationHandler: this._elicitationHandler
2750
+ elicitationHandler: this._elicitationHandler,
2751
+ notificationHandler: this.recordNotification
2578
2752
  });
2579
2753
  case "sse":
2580
2754
  throw new Error("SSE transport not yet implemented");