@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.
package/perf/index.js CHANGED
@@ -834,236 +834,281 @@ function getMeasurementsForProject(project) {
834
834
  return getGlobalMeasurementsArray().filter((m) => m.project === project);
835
835
  }
836
836
 
837
- // libs/testing/src/platform/platform-client-info.ts
838
- var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
839
- function getPlatformClientInfo(platform) {
840
- switch (platform) {
841
- case "openai":
842
- return {
843
- name: "ChatGPT",
844
- version: "1.0"
845
- };
846
- case "ext-apps":
847
- return {
848
- name: "mcp-ext-apps",
849
- version: "1.0"
850
- };
851
- case "claude":
852
- return {
853
- name: "claude-desktop",
854
- version: "1.0"
855
- };
856
- case "cursor":
857
- return {
858
- name: "cursor",
859
- version: "1.0"
860
- };
861
- case "continue":
862
- return {
863
- name: "continue",
864
- version: "1.0"
865
- };
866
- case "cody":
867
- return {
868
- name: "cody",
869
- version: "1.0"
870
- };
871
- case "gemini":
872
- return {
873
- name: "gemini",
874
- version: "1.0"
875
- };
876
- case "generic-mcp":
877
- return {
878
- name: "generic-mcp-client",
879
- version: "1.0"
880
- };
881
- case "unknown":
882
- default:
883
- return {
884
- name: "mcp-test-client",
885
- version: "1.0"
886
- };
887
- }
888
- }
889
- function getPlatformCapabilities(platform) {
890
- const baseCapabilities = {
891
- sampling: {},
892
- // Include elicitation.form by default for testing elicitation workflows
893
- // Note: MCP SDK expects form to be an object, not boolean
894
- elicitation: {
895
- form: {}
896
- }
897
- };
898
- if (platform === "ext-apps") {
837
+ // libs/testing/src/interceptor/mock-registry.ts
838
+ var DefaultMockRegistry = class {
839
+ mocks = [];
840
+ add(mock) {
841
+ const entry = {
842
+ definition: mock,
843
+ callCount: 0,
844
+ calls: [],
845
+ remainingUses: mock.times ?? Infinity
846
+ };
847
+ this.mocks.push(entry);
899
848
  return {
900
- ...baseCapabilities,
901
- experimental: {
902
- [MCP_APPS_EXTENSION_KEY]: {
903
- mimeTypes: ["text/html;profile=mcp-app"]
849
+ remove: () => {
850
+ const index = this.mocks.indexOf(entry);
851
+ if (index !== -1) {
852
+ this.mocks.splice(index, 1);
904
853
  }
905
- }
854
+ },
855
+ callCount: () => entry.callCount,
856
+ calls: () => [...entry.calls]
906
857
  };
907
858
  }
908
- return baseCapabilities;
909
- }
910
-
911
- // libs/testing/src/client/mcp-test-client.builder.ts
912
- var McpTestClientBuilder = class {
913
- config;
914
- constructor(config) {
915
- this.config = { ...config };
859
+ clear() {
860
+ this.mocks = [];
916
861
  }
917
- /**
918
- * Set the authentication configuration
919
- */
920
- withAuth(auth) {
921
- this.config.auth = { ...this.config.auth, ...auth };
922
- return this;
862
+ getAll() {
863
+ return this.mocks.map((e) => e.definition);
864
+ }
865
+ match(request) {
866
+ for (const entry of this.mocks) {
867
+ if (entry.remainingUses <= 0) continue;
868
+ const { definition } = entry;
869
+ if (definition.method !== request.method) continue;
870
+ if (definition.params !== void 0) {
871
+ const params = request.params ?? {};
872
+ if (typeof definition.params === "function") {
873
+ if (!definition.params(params)) continue;
874
+ } else {
875
+ if (!this.paramsMatch(definition.params, params)) continue;
876
+ }
877
+ }
878
+ entry.callCount++;
879
+ entry.calls.push(request);
880
+ entry.remainingUses--;
881
+ return definition;
882
+ }
883
+ return void 0;
923
884
  }
924
885
  /**
925
- * Set the bearer token for authentication
886
+ * Check if request params match the mock params definition
926
887
  */
927
- withToken(token) {
928
- this.config.auth = { ...this.config.auth, token };
929
- return this;
888
+ paramsMatch(expected, actual) {
889
+ for (const [key, value] of Object.entries(expected)) {
890
+ if (!(key in actual)) return false;
891
+ const actualValue = actual[key];
892
+ if (Array.isArray(value)) {
893
+ if (!Array.isArray(actualValue)) return false;
894
+ if (value.length !== actualValue.length) return false;
895
+ for (let i = 0; i < value.length; i++) {
896
+ const expectedItem = value[i];
897
+ const actualItem = actualValue[i];
898
+ if (typeof expectedItem === "object" && expectedItem !== null) {
899
+ if (typeof actualItem !== "object" || actualItem === null) return false;
900
+ if (!this.paramsMatch(expectedItem, actualItem)) {
901
+ return false;
902
+ }
903
+ } else if (actualItem !== expectedItem) {
904
+ return false;
905
+ }
906
+ }
907
+ } else if (typeof value === "object" && value !== null) {
908
+ if (typeof actualValue !== "object" || actualValue === null) return false;
909
+ if (!this.paramsMatch(value, actualValue)) {
910
+ return false;
911
+ }
912
+ } else if (actualValue !== value) {
913
+ return false;
914
+ }
915
+ }
916
+ return true;
930
917
  }
918
+ };
919
+ var mockResponse = {
931
920
  /**
932
- * Add custom headers to all requests
921
+ * Create a successful JSON-RPC response
933
922
  */
934
- withHeaders(headers) {
935
- this.config.auth = {
936
- ...this.config.auth,
937
- headers: { ...this.config.auth?.headers, ...headers }
923
+ success(result, id = 1) {
924
+ return {
925
+ jsonrpc: "2.0",
926
+ id,
927
+ result
938
928
  };
939
- return this;
940
- }
929
+ },
941
930
  /**
942
- * Set the transport type
931
+ * Create an error JSON-RPC response
943
932
  */
944
- withTransport(transport) {
945
- this.config.transport = transport;
946
- return this;
947
- }
933
+ error(code, message, data, id = 1) {
934
+ return {
935
+ jsonrpc: "2.0",
936
+ id,
937
+ error: { code, message, data }
938
+ };
939
+ },
948
940
  /**
949
- * Set the request timeout in milliseconds
941
+ * Create a tool result response
950
942
  */
951
- withTimeout(timeoutMs) {
952
- this.config.timeout = timeoutMs;
953
- return this;
954
- }
943
+ toolResult(content, id = 1) {
944
+ return {
945
+ jsonrpc: "2.0",
946
+ id,
947
+ result: { content }
948
+ };
949
+ },
955
950
  /**
956
- * Enable debug logging
951
+ * Create a tools/list response
957
952
  */
958
- withDebug(enabled = true) {
959
- this.config.debug = enabled;
960
- return this;
961
- }
953
+ toolsList(tools, id = 1) {
954
+ return {
955
+ jsonrpc: "2.0",
956
+ id,
957
+ result: { tools }
958
+ };
959
+ },
962
960
  /**
963
- * Enable public mode - skip authentication entirely.
964
- * When true, no Authorization header is sent and anonymous token is not requested.
965
- * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
961
+ * Create a resources/list response
966
962
  */
967
- withPublicMode(enabled = true) {
968
- this.config.publicMode = enabled;
969
- return this;
970
- }
963
+ resourcesList(resources, id = 1) {
964
+ return {
965
+ jsonrpc: "2.0",
966
+ id,
967
+ result: { resources }
968
+ };
969
+ },
971
970
  /**
972
- * Set the MCP protocol version to request
971
+ * Create a resources/read response
973
972
  */
974
- withProtocolVersion(version) {
975
- this.config.protocolVersion = version;
976
- return this;
977
- }
973
+ resourceRead(contents, id = 1) {
974
+ return {
975
+ jsonrpc: "2.0",
976
+ id,
977
+ result: { contents }
978
+ };
979
+ },
978
980
  /**
979
- * Set the client info sent during initialization
981
+ * Common MCP errors
980
982
  */
981
- withClientInfo(info) {
982
- this.config.clientInfo = info;
983
- return this;
983
+ errors: {
984
+ methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
985
+ invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
986
+ internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
987
+ resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
988
+ toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
989
+ unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
990
+ forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
991
+ }
992
+ };
993
+
994
+ // libs/testing/src/interceptor/interceptor-chain.ts
995
+ var DefaultInterceptorChain = class {
996
+ request = [];
997
+ response = [];
998
+ mocks;
999
+ constructor() {
1000
+ this.mocks = new DefaultMockRegistry();
984
1001
  }
985
1002
  /**
986
- * Set the platform type for testing platform-specific meta keys.
987
- * Automatically configures clientInfo and capabilities for platform detection.
988
- *
989
- * Platform-specific behavior:
990
- * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
991
- * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
992
- * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
993
- * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
994
- * - Other platforms follow similar patterns
995
- *
996
- * @example
997
- * ```typescript
998
- * const client = await McpTestClient.create({ baseUrl })
999
- * .withPlatform('openai')
1000
- * .buildAndConnect();
1001
- *
1002
- * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
1003
- * const extAppsClient = await McpTestClient.create({ baseUrl })
1004
- * .withPlatform('ext-apps')
1005
- * .buildAndConnect();
1006
- * ```
1003
+ * Add a request interceptor
1007
1004
  */
1008
- withPlatform(platform) {
1009
- this.config.platform = platform;
1010
- this.config.clientInfo = getPlatformClientInfo(platform);
1011
- this.config.capabilities = getPlatformCapabilities(platform);
1012
- return this;
1005
+ addRequestInterceptor(interceptor) {
1006
+ this.request.push(interceptor);
1007
+ return () => {
1008
+ const index = this.request.indexOf(interceptor);
1009
+ if (index !== -1) {
1010
+ this.request.splice(index, 1);
1011
+ }
1012
+ };
1013
1013
  }
1014
1014
  /**
1015
- * Set custom client capabilities for MCP initialization.
1016
- * Use this for fine-grained control over capabilities sent during initialization.
1017
- *
1018
- * @example
1019
- * ```typescript
1020
- * const client = await McpTestClient.create({ baseUrl })
1021
- * .withCapabilities({
1022
- * sampling: {},
1023
- * experimental: {
1024
- * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
1025
- * }
1026
- * })
1027
- * .buildAndConnect();
1028
- * ```
1015
+ * Add a response interceptor
1029
1016
  */
1030
- withCapabilities(capabilities) {
1031
- this.config.capabilities = capabilities;
1032
- return this;
1017
+ addResponseInterceptor(interceptor) {
1018
+ this.response.push(interceptor);
1019
+ return () => {
1020
+ const index = this.response.indexOf(interceptor);
1021
+ if (index !== -1) {
1022
+ this.response.splice(index, 1);
1023
+ }
1024
+ };
1033
1025
  }
1034
1026
  /**
1035
- * Set query parameters to append to the connection URL.
1036
- * Useful for testing mode switches like `?mode=skills_only`.
1037
- *
1038
- * @example
1039
- * ```typescript
1040
- * const client = await McpTestClient.create({ baseUrl })
1041
- * .withQueryParams({ mode: 'skills_only' })
1042
- * .buildAndConnect();
1043
- * ```
1027
+ * Process a request through the interceptor chain
1028
+ * Returns either:
1029
+ * - { type: 'continue', request } - continue with (possibly modified) request
1030
+ * - { type: 'mock', response } - return mock response immediately
1031
+ * - { type: 'error', error } - throw error
1044
1032
  */
1045
- withQueryParams(params) {
1046
- this.config.queryParams = { ...this.config.queryParams, ...params };
1047
- return this;
1033
+ async processRequest(request, meta) {
1034
+ let currentRequest = request;
1035
+ const mockDef = this.mocks.match(request);
1036
+ if (mockDef) {
1037
+ if (mockDef.delay && mockDef.delay > 0) {
1038
+ await sleep3(mockDef.delay);
1039
+ }
1040
+ let mockResponse2;
1041
+ if (typeof mockDef.response === "function") {
1042
+ mockResponse2 = await mockDef.response(request);
1043
+ } else {
1044
+ mockResponse2 = mockDef.response;
1045
+ }
1046
+ return {
1047
+ type: "mock",
1048
+ response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
1049
+ };
1050
+ }
1051
+ for (const interceptor of this.request) {
1052
+ const ctx = {
1053
+ request: currentRequest,
1054
+ meta
1055
+ };
1056
+ const result = await interceptor(ctx);
1057
+ switch (result.action) {
1058
+ case "passthrough":
1059
+ break;
1060
+ case "modify":
1061
+ currentRequest = result.request;
1062
+ break;
1063
+ case "mock":
1064
+ return {
1065
+ type: "mock",
1066
+ response: { ...result.response, id: request.id ?? result.response.id }
1067
+ };
1068
+ case "error":
1069
+ return { type: "error", error: result.error };
1070
+ }
1071
+ }
1072
+ return { type: "continue", request: currentRequest };
1048
1073
  }
1049
1074
  /**
1050
- * Build the McpTestClient instance (does not connect)
1075
+ * Process a response through the interceptor chain
1051
1076
  */
1052
- build() {
1053
- return new McpTestClient(this.config);
1077
+ async processResponse(request, response, durationMs) {
1078
+ let currentResponse = response;
1079
+ for (const interceptor of this.response) {
1080
+ const ctx = {
1081
+ request,
1082
+ response: currentResponse,
1083
+ durationMs
1084
+ };
1085
+ const result = await interceptor(ctx);
1086
+ switch (result.action) {
1087
+ case "passthrough":
1088
+ break;
1089
+ case "modify":
1090
+ currentResponse = result.response;
1091
+ break;
1092
+ }
1093
+ }
1094
+ return currentResponse;
1054
1095
  }
1055
1096
  /**
1056
- * Build the McpTestClient and connect to the server
1097
+ * Clear all interceptors and mocks
1057
1098
  */
1058
- async buildAndConnect() {
1059
- const client = this.build();
1060
- await client.connect();
1061
- return client;
1099
+ clear() {
1100
+ this.request = [];
1101
+ this.response = [];
1102
+ this.mocks.clear();
1062
1103
  }
1063
1104
  };
1105
+ function sleep3(ms) {
1106
+ return new Promise((resolve) => setTimeout(resolve, ms));
1107
+ }
1064
1108
 
1065
1109
  // libs/testing/src/transport/streamable-http.transport.ts
1066
1110
  var DEFAULT_TIMEOUT = 3e4;
1111
+ var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
1067
1112
  var StreamableHttpTransport = class {
1068
1113
  config;
1069
1114
  state = "disconnected";
@@ -1075,10 +1120,15 @@ var StreamableHttpTransport = class {
1075
1120
  interceptors;
1076
1121
  publicMode;
1077
1122
  elicitationHandler;
1123
+ notificationHandler;
1124
+ notificationStream;
1125
+ notificationProtocolVersion = "";
1126
+ notificationStreamReopenTimer;
1078
1127
  constructor(config) {
1079
1128
  this.config = {
1080
1129
  baseUrl: config.baseUrl.replace(/\/$/, ""),
1081
1130
  // Remove trailing slash
1131
+ entryPath: config.entryPath,
1082
1132
  timeout: config.timeout ?? DEFAULT_TIMEOUT,
1083
1133
  auth: config.auth ?? {},
1084
1134
  publicMode: config.publicMode ?? false,
@@ -1090,6 +1140,7 @@ var StreamableHttpTransport = class {
1090
1140
  this.interceptors = config.interceptors;
1091
1141
  this.publicMode = config.publicMode ?? false;
1092
1142
  this.elicitationHandler = config.elicitationHandler;
1143
+ this.notificationHandler = config.notificationHandler;
1093
1144
  }
1094
1145
  async connect() {
1095
1146
  this.state = "connecting";
@@ -1171,7 +1222,7 @@ var StreamableHttpTransport = class {
1171
1222
  }
1172
1223
  const headers = this.buildHeaders();
1173
1224
  this.lastRequestHeaders = headers;
1174
- const url = `${this.config.baseUrl}/`;
1225
+ const url = this.mcpUrl();
1175
1226
  this.log(`POST ${url}`, message);
1176
1227
  const controller = new AbortController();
1177
1228
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -1241,7 +1292,7 @@ var StreamableHttpTransport = class {
1241
1292
  this.ensureConnected();
1242
1293
  const headers = this.buildHeaders();
1243
1294
  this.lastRequestHeaders = headers;
1244
- const url = `${this.config.baseUrl}/`;
1295
+ const url = this.mcpUrl();
1245
1296
  this.log(`POST ${url} (notification)`, message);
1246
1297
  const controller = new AbortController();
1247
1298
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -1272,7 +1323,7 @@ var StreamableHttpTransport = class {
1272
1323
  this.ensureConnected();
1273
1324
  const headers = this.buildHeaders();
1274
1325
  this.lastRequestHeaders = headers;
1275
- const url = `${this.config.baseUrl}/`;
1326
+ const url = this.mcpUrl();
1276
1327
  this.log(`POST ${url} (raw)`, data);
1277
1328
  const controller = new AbortController();
1278
1329
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
@@ -1309,7 +1360,12 @@ var StreamableHttpTransport = class {
1309
1360
  };
1310
1361
  }
1311
1362
  }
1363
+ async openNotificationStream(protocolVersion) {
1364
+ this.notificationProtocolVersion = protocolVersion;
1365
+ await this.connectNotificationStream();
1366
+ }
1312
1367
  async close() {
1368
+ this.closeNotificationStream();
1313
1369
  this.state = "disconnected";
1314
1370
  this.sessionId = void 0;
1315
1371
  this.log("StreamableHTTP transport closed");
@@ -1348,6 +1404,7 @@ var StreamableHttpTransport = class {
1348
1404
  return { ...this.lastRequestHeaders };
1349
1405
  }
1350
1406
  async simulateDisconnect() {
1407
+ this.closeNotificationStream();
1351
1408
  this.state = "disconnected";
1352
1409
  this.sessionId = void 0;
1353
1410
  }
@@ -1366,6 +1423,72 @@ var StreamableHttpTransport = class {
1366
1423
  // ═══════════════════════════════════════════════════════════════════
1367
1424
  // PRIVATE HELPERS
1368
1425
  // ═══════════════════════════════════════════════════════════════════
1426
+ closeNotificationStream() {
1427
+ clearTimeout(this.notificationStreamReopenTimer);
1428
+ this.notificationStream?.abort();
1429
+ this.notificationStream = void 0;
1430
+ }
1431
+ /** Opens the stream without holding up the caller past the request timeout; close() aborts it at any point. */
1432
+ async connectNotificationStream() {
1433
+ if (!this.sessionId || this.notificationStream) return;
1434
+ const controller = new AbortController();
1435
+ this.notificationStream = controller;
1436
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
1437
+ const established = fetch(this.mcpUrl(), {
1438
+ method: "GET",
1439
+ headers: { ...headers, Accept: "text/event-stream", "MCP-Protocol-Version": this.notificationProtocolVersion },
1440
+ signal: controller.signal
1441
+ }).then(
1442
+ async (response) => {
1443
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
1444
+ if (!response.ok || !response.body || !isEventStream) {
1445
+ await response.body?.cancel();
1446
+ this.log(`Server offers no notification stream (HTTP ${response.status})`);
1447
+ this.releaseNotificationStream(controller);
1448
+ return;
1449
+ }
1450
+ void this.readNotificationStream(controller, response.body.getReader());
1451
+ },
1452
+ (error) => {
1453
+ this.log("Failed to open the notification stream:", error);
1454
+ this.releaseNotificationStream(controller);
1455
+ }
1456
+ );
1457
+ let establishTimer;
1458
+ const establishDeadline = new Promise((resolve) => {
1459
+ establishTimer = setTimeout(resolve, this.config.timeout);
1460
+ });
1461
+ await Promise.race([established, establishDeadline]);
1462
+ clearTimeout(establishTimer);
1463
+ }
1464
+ releaseNotificationStream(controller) {
1465
+ if (this.notificationStream === controller) this.notificationStream = void 0;
1466
+ }
1467
+ async readNotificationStream(controller, reader) {
1468
+ const decoder = new TextDecoder();
1469
+ let buffer = "";
1470
+ try {
1471
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
1472
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1473
+ buffer = remainder;
1474
+ for (const eventText of completeEvents) {
1475
+ for (const event of this.parseSSEEvents(eventText, void 0).events) {
1476
+ await this.handleSSEEvent(event);
1477
+ }
1478
+ }
1479
+ }
1480
+ } catch (error) {
1481
+ this.log("Notification stream ended:", error);
1482
+ } finally {
1483
+ reader.releaseLock();
1484
+ }
1485
+ if (controller.signal.aborted || this.notificationStream !== controller) return;
1486
+ this.releaseNotificationStream(controller);
1487
+ this.notificationStreamReopenTimer = setTimeout(
1488
+ () => void this.connectNotificationStream(),
1489
+ NOTIFICATION_STREAM_REOPEN_DELAY_MS
1490
+ );
1491
+ }
1369
1492
  /**
1370
1493
  * Handle SSE response with elicitation support.
1371
1494
  *
@@ -1503,6 +1626,13 @@ var StreamableHttpTransport = class {
1503
1626
  response: { jsonrpc: "2.0", id: null, result: void 0 }
1504
1627
  };
1505
1628
  }
1629
+ if ("method" in parsed && parsed.id === void 0) {
1630
+ this.notificationHandler?.(parsed);
1631
+ return {
1632
+ isFinal: false,
1633
+ response: { jsonrpc: "2.0", id: null, result: void 0 }
1634
+ };
1635
+ }
1506
1636
  if ("result" in parsed || "error" in parsed) {
1507
1637
  return { isFinal: true, response: parsed };
1508
1638
  }
@@ -1555,7 +1685,7 @@ var StreamableHttpTransport = class {
1555
1685
  */
1556
1686
  async sendElicitationResponse(requestId, response) {
1557
1687
  const headers = this.buildHeaders();
1558
- const url = `${this.config.baseUrl}/`;
1688
+ const url = this.mcpUrl();
1559
1689
  const rpcResponse = {
1560
1690
  jsonrpc: "2.0",
1561
1691
  id: requestId,
@@ -1575,6 +1705,18 @@ var StreamableHttpTransport = class {
1575
1705
  this.log("Failed to send elicitation response:", error);
1576
1706
  }
1577
1707
  }
1708
+ /**
1709
+ * The MCP endpoint URL. `entryPath` is where the server mounts MCP; the
1710
+ * trailing slash is kept because both hosts normalize it and the original
1711
+ * behaviour (`${baseUrl}/`) relied on it (issue #543).
1712
+ */
1713
+ mcpUrl() {
1714
+ const url = new URL(this.config.baseUrl);
1715
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
1716
+ const basePath = url.pathname.replace(/\/+$/, "");
1717
+ url.pathname = entry ? `${basePath}/${entry}` : `${basePath}/`;
1718
+ return url.toString();
1719
+ }
1578
1720
  buildHeaders() {
1579
1721
  const headers = {
1580
1722
  "Content-Type": "application/json",
@@ -1661,286 +1803,266 @@ var StreamableHttpTransport = class {
1661
1803
  };
1662
1804
  }
1663
1805
  };
1806
+ function splitSSEEvents(buffer) {
1807
+ const endsWithCarriageReturn = buffer.endsWith("\r");
1808
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1809
+ const completeEvents = normalized.split("\n\n");
1810
+ const remainder = completeEvents.pop() ?? "";
1811
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
1812
+ }
1664
1813
 
1665
- // libs/testing/src/interceptor/mock-registry.ts
1666
- var DefaultMockRegistry = class {
1667
- mocks = [];
1668
- add(mock) {
1669
- const entry = {
1670
- definition: mock,
1671
- callCount: 0,
1672
- calls: [],
1673
- remainingUses: mock.times ?? Infinity
1674
- };
1675
- this.mocks.push(entry);
1814
+ // libs/testing/src/platform/platform-client-info.ts
1815
+ var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
1816
+ function getPlatformClientInfo(platform) {
1817
+ switch (platform) {
1818
+ case "openai":
1819
+ return {
1820
+ name: "ChatGPT",
1821
+ version: "1.0"
1822
+ };
1823
+ case "ext-apps":
1824
+ return {
1825
+ name: "mcp-ext-apps",
1826
+ version: "1.0"
1827
+ };
1828
+ case "claude":
1829
+ return {
1830
+ name: "claude-desktop",
1831
+ version: "1.0"
1832
+ };
1833
+ case "cursor":
1834
+ return {
1835
+ name: "cursor",
1836
+ version: "1.0"
1837
+ };
1838
+ case "continue":
1839
+ return {
1840
+ name: "continue",
1841
+ version: "1.0"
1842
+ };
1843
+ case "cody":
1844
+ return {
1845
+ name: "cody",
1846
+ version: "1.0"
1847
+ };
1848
+ case "gemini":
1849
+ return {
1850
+ name: "gemini",
1851
+ version: "1.0"
1852
+ };
1853
+ case "generic-mcp":
1854
+ return {
1855
+ name: "generic-mcp-client",
1856
+ version: "1.0"
1857
+ };
1858
+ case "unknown":
1859
+ default:
1860
+ return {
1861
+ name: "mcp-test-client",
1862
+ version: "1.0"
1863
+ };
1864
+ }
1865
+ }
1866
+ function getPlatformCapabilities(platform) {
1867
+ const baseCapabilities = {
1868
+ sampling: {},
1869
+ // Include elicitation.form by default for testing elicitation workflows
1870
+ // Note: MCP SDK expects form to be an object, not boolean
1871
+ elicitation: {
1872
+ form: {}
1873
+ }
1874
+ };
1875
+ if (platform === "ext-apps") {
1676
1876
  return {
1677
- remove: () => {
1678
- const index = this.mocks.indexOf(entry);
1679
- if (index !== -1) {
1680
- this.mocks.splice(index, 1);
1877
+ ...baseCapabilities,
1878
+ experimental: {
1879
+ [MCP_APPS_EXTENSION_KEY]: {
1880
+ mimeTypes: ["text/html;profile=mcp-app"]
1681
1881
  }
1682
- },
1683
- callCount: () => entry.callCount,
1684
- calls: () => [...entry.calls]
1882
+ }
1685
1883
  };
1686
1884
  }
1687
- clear() {
1688
- this.mocks = [];
1689
- }
1690
- getAll() {
1691
- return this.mocks.map((e) => e.definition);
1692
- }
1693
- match(request) {
1694
- for (const entry of this.mocks) {
1695
- if (entry.remainingUses <= 0) continue;
1696
- const { definition } = entry;
1697
- if (definition.method !== request.method) continue;
1698
- if (definition.params !== void 0) {
1699
- const params = request.params ?? {};
1700
- if (typeof definition.params === "function") {
1701
- if (!definition.params(params)) continue;
1702
- } else {
1703
- if (!this.paramsMatch(definition.params, params)) continue;
1704
- }
1705
- }
1706
- entry.callCount++;
1707
- entry.calls.push(request);
1708
- entry.remainingUses--;
1709
- return definition;
1710
- }
1711
- return void 0;
1885
+ return baseCapabilities;
1886
+ }
1887
+
1888
+ // libs/testing/src/client/mcp-test-client.builder.ts
1889
+ var McpTestClientBuilder = class {
1890
+ config;
1891
+ constructor(config) {
1892
+ this.config = { ...config };
1712
1893
  }
1713
1894
  /**
1714
- * Check if request params match the mock params definition
1895
+ * Set the authentication configuration
1715
1896
  */
1716
- paramsMatch(expected, actual) {
1717
- for (const [key, value] of Object.entries(expected)) {
1718
- if (!(key in actual)) return false;
1719
- const actualValue = actual[key];
1720
- if (Array.isArray(value)) {
1721
- if (!Array.isArray(actualValue)) return false;
1722
- if (value.length !== actualValue.length) return false;
1723
- for (let i = 0; i < value.length; i++) {
1724
- const expectedItem = value[i];
1725
- const actualItem = actualValue[i];
1726
- if (typeof expectedItem === "object" && expectedItem !== null) {
1727
- if (typeof actualItem !== "object" || actualItem === null) return false;
1728
- if (!this.paramsMatch(expectedItem, actualItem)) {
1729
- return false;
1730
- }
1731
- } else if (actualItem !== expectedItem) {
1732
- return false;
1733
- }
1734
- }
1735
- } else if (typeof value === "object" && value !== null) {
1736
- if (typeof actualValue !== "object" || actualValue === null) return false;
1737
- if (!this.paramsMatch(value, actualValue)) {
1738
- return false;
1739
- }
1740
- } else if (actualValue !== value) {
1741
- return false;
1742
- }
1743
- }
1744
- return true;
1897
+ withAuth(auth) {
1898
+ this.config.auth = { ...this.config.auth, ...auth };
1899
+ return this;
1745
1900
  }
1746
- };
1747
- var mockResponse = {
1748
1901
  /**
1749
- * Create a successful JSON-RPC response
1902
+ * Set the bearer token for authentication
1750
1903
  */
1751
- success(result, id = 1) {
1752
- return {
1753
- jsonrpc: "2.0",
1754
- id,
1755
- result
1756
- };
1757
- },
1904
+ withToken(token) {
1905
+ this.config.auth = { ...this.config.auth, token };
1906
+ return this;
1907
+ }
1758
1908
  /**
1759
- * Create an error JSON-RPC response
1909
+ * Add custom headers to all requests
1760
1910
  */
1761
- error(code, message, data, id = 1) {
1762
- return {
1763
- jsonrpc: "2.0",
1764
- id,
1765
- error: { code, message, data }
1911
+ withHeaders(headers) {
1912
+ this.config.auth = {
1913
+ ...this.config.auth,
1914
+ headers: { ...this.config.auth?.headers, ...headers }
1766
1915
  };
1767
- },
1916
+ return this;
1917
+ }
1768
1918
  /**
1769
- * Create a tool result response
1919
+ * Set the transport type
1770
1920
  */
1771
- toolResult(content, id = 1) {
1772
- return {
1773
- jsonrpc: "2.0",
1774
- id,
1775
- result: { content }
1776
- };
1777
- },
1921
+ withTransport(transport) {
1922
+ this.config.transport = transport;
1923
+ return this;
1924
+ }
1778
1925
  /**
1779
- * Create a tools/list response
1926
+ * Set the request timeout in milliseconds
1780
1927
  */
1781
- toolsList(tools, id = 1) {
1782
- return {
1783
- jsonrpc: "2.0",
1784
- id,
1785
- result: { tools }
1786
- };
1787
- },
1928
+ withTimeout(timeoutMs) {
1929
+ this.config.timeout = timeoutMs;
1930
+ return this;
1931
+ }
1788
1932
  /**
1789
- * Create a resources/list response
1933
+ * Enable debug logging
1790
1934
  */
1791
- resourcesList(resources, id = 1) {
1792
- return {
1793
- jsonrpc: "2.0",
1794
- id,
1795
- result: { resources }
1796
- };
1797
- },
1935
+ withDebug(enabled = true) {
1936
+ this.config.debug = enabled;
1937
+ return this;
1938
+ }
1798
1939
  /**
1799
- * Create a resources/read response
1940
+ * Enable public mode - skip authentication entirely.
1941
+ * When true, no Authorization header is sent and anonymous token is not requested.
1942
+ * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
1800
1943
  */
1801
- resourceRead(contents, id = 1) {
1802
- return {
1803
- jsonrpc: "2.0",
1804
- id,
1805
- result: { contents }
1806
- };
1807
- },
1944
+ withPublicMode(enabled = true) {
1945
+ this.config.publicMode = enabled;
1946
+ return this;
1947
+ }
1808
1948
  /**
1809
- * Common MCP errors
1949
+ * Set the MCP protocol version to request (default '2025-06-18').
1950
+ * '2026-07-28' has no initialize handshake, so `build()` throws for it.
1810
1951
  */
1811
- errors: {
1812
- methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
1813
- invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
1814
- internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
1815
- resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
1816
- toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
1817
- unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
1818
- forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
1952
+ withProtocolVersion(version) {
1953
+ this.config.protocolVersion = version;
1954
+ return this;
1819
1955
  }
1820
- };
1821
-
1822
- // libs/testing/src/interceptor/interceptor-chain.ts
1823
- var DefaultInterceptorChain = class {
1824
- request = [];
1825
- response = [];
1826
- mocks;
1827
- constructor() {
1828
- this.mocks = new DefaultMockRegistry();
1956
+ /**
1957
+ * Set the client info sent during initialization
1958
+ */
1959
+ withClientInfo(info) {
1960
+ this.config.clientInfo = info;
1961
+ return this;
1829
1962
  }
1830
1963
  /**
1831
- * Add a request interceptor
1964
+ * Set the platform type for testing platform-specific meta keys.
1965
+ * Automatically configures clientInfo and capabilities for platform detection.
1966
+ *
1967
+ * Platform-specific behavior:
1968
+ * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
1969
+ * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
1970
+ * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
1971
+ * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
1972
+ * - Other platforms follow similar patterns
1973
+ *
1974
+ * @example
1975
+ * ```typescript
1976
+ * const client = await McpTestClient.create({ baseUrl })
1977
+ * .withPlatform('openai')
1978
+ * .buildAndConnect();
1979
+ *
1980
+ * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
1981
+ * const extAppsClient = await McpTestClient.create({ baseUrl })
1982
+ * .withPlatform('ext-apps')
1983
+ * .buildAndConnect();
1984
+ * ```
1832
1985
  */
1833
- addRequestInterceptor(interceptor) {
1834
- this.request.push(interceptor);
1835
- return () => {
1836
- const index = this.request.indexOf(interceptor);
1837
- if (index !== -1) {
1838
- this.request.splice(index, 1);
1839
- }
1840
- };
1986
+ withPlatform(platform) {
1987
+ this.config.platform = platform;
1988
+ this.config.clientInfo = getPlatformClientInfo(platform);
1989
+ this.config.capabilities = getPlatformCapabilities(platform);
1990
+ return this;
1841
1991
  }
1842
1992
  /**
1843
- * Add a response interceptor
1993
+ * Set custom client capabilities for MCP initialization.
1994
+ * Use this for fine-grained control over capabilities sent during initialization.
1995
+ *
1996
+ * @example
1997
+ * ```typescript
1998
+ * const client = await McpTestClient.create({ baseUrl })
1999
+ * .withCapabilities({
2000
+ * sampling: {},
2001
+ * experimental: {
2002
+ * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html;profile=mcp-app'] }
2003
+ * }
2004
+ * })
2005
+ * .buildAndConnect();
2006
+ * ```
1844
2007
  */
1845
- addResponseInterceptor(interceptor) {
1846
- this.response.push(interceptor);
1847
- return () => {
1848
- const index = this.response.indexOf(interceptor);
1849
- if (index !== -1) {
1850
- this.response.splice(index, 1);
1851
- }
1852
- };
2008
+ withCapabilities(capabilities) {
2009
+ this.config.capabilities = capabilities;
2010
+ return this;
1853
2011
  }
1854
2012
  /**
1855
- * Process a request through the interceptor chain
1856
- * Returns either:
1857
- * - { type: 'continue', request } - continue with (possibly modified) request
1858
- * - { type: 'mock', response } - return mock response immediately
1859
- * - { type: 'error', error } - throw error
2013
+ * Set query parameters to append to the connection URL.
2014
+ * Useful for testing mode switches like `?mode=skills_only`.
2015
+ *
2016
+ * @example
2017
+ * ```typescript
2018
+ * const client = await McpTestClient.create({ baseUrl })
2019
+ * .withQueryParams({ mode: 'skills_only' })
2020
+ * .buildAndConnect();
2021
+ * ```
1860
2022
  */
1861
- async processRequest(request, meta) {
1862
- let currentRequest = request;
1863
- const mockDef = this.mocks.match(request);
1864
- if (mockDef) {
1865
- if (mockDef.delay && mockDef.delay > 0) {
1866
- await sleep3(mockDef.delay);
1867
- }
1868
- let mockResponse2;
1869
- if (typeof mockDef.response === "function") {
1870
- mockResponse2 = await mockDef.response(request);
1871
- } else {
1872
- mockResponse2 = mockDef.response;
1873
- }
1874
- return {
1875
- type: "mock",
1876
- response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
1877
- };
1878
- }
1879
- for (const interceptor of this.request) {
1880
- const ctx = {
1881
- request: currentRequest,
1882
- meta
1883
- };
1884
- const result = await interceptor(ctx);
1885
- switch (result.action) {
1886
- case "passthrough":
1887
- break;
1888
- case "modify":
1889
- currentRequest = result.request;
1890
- break;
1891
- case "mock":
1892
- return {
1893
- type: "mock",
1894
- response: { ...result.response, id: request.id ?? result.response.id }
1895
- };
1896
- case "error":
1897
- return { type: "error", error: result.error };
1898
- }
1899
- }
1900
- return { type: "continue", request: currentRequest };
2023
+ withQueryParams(params) {
2024
+ this.config.queryParams = { ...this.config.queryParams, ...params };
2025
+ return this;
1901
2026
  }
1902
2027
  /**
1903
- * Process a response through the interceptor chain
2028
+ * Build the McpTestClient instance (does not connect)
1904
2029
  */
1905
- async processResponse(request, response, durationMs) {
1906
- let currentResponse = response;
1907
- for (const interceptor of this.response) {
1908
- const ctx = {
1909
- request,
1910
- response: currentResponse,
1911
- durationMs
1912
- };
1913
- const result = await interceptor(ctx);
1914
- switch (result.action) {
1915
- case "passthrough":
1916
- break;
1917
- case "modify":
1918
- currentResponse = result.response;
1919
- break;
1920
- }
1921
- }
1922
- return currentResponse;
2030
+ build() {
2031
+ return new McpTestClient(this.config);
1923
2032
  }
1924
2033
  /**
1925
- * Clear all interceptors and mocks
2034
+ * Build the McpTestClient and connect to the server
1926
2035
  */
1927
- clear() {
1928
- this.request = [];
1929
- this.response = [];
1930
- this.mocks.clear();
2036
+ async buildAndConnect() {
2037
+ const client = this.build();
2038
+ await client.connect();
2039
+ return client;
1931
2040
  }
1932
2041
  };
1933
- function sleep3(ms) {
1934
- return new Promise((resolve) => setTimeout(resolve, ms));
1935
- }
1936
2042
 
1937
2043
  // libs/testing/src/client/mcp-test-client.ts
1938
2044
  var DEFAULT_TIMEOUT2 = 3e4;
1939
2045
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
2046
+ var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1940
2047
  var DEFAULT_CLIENT_INFO = {
1941
2048
  name: "@frontmcp/testing",
1942
2049
  version: "0.4.0"
1943
2050
  };
2051
+ function extractEntryPaths(data) {
2052
+ let payload = data;
2053
+ if (typeof payload === "string") {
2054
+ try {
2055
+ payload = JSON.parse(payload);
2056
+ } catch {
2057
+ return void 0;
2058
+ }
2059
+ }
2060
+ if (!payload || typeof payload !== "object") return void 0;
2061
+ const paths = payload.entryPaths;
2062
+ if (!Array.isArray(paths)) return void 0;
2063
+ const strings = paths.filter((p) => typeof p === "string");
2064
+ return strings.length > 0 ? strings : void 0;
2065
+ }
1944
2066
  var McpTestClient = class {
1945
2067
  // Platform, capabilities, and queryParams are optional - only set when needed
1946
2068
  config;
@@ -1949,6 +2071,8 @@ var McpTestClient = class {
1949
2071
  requestIdCounter = 0;
1950
2072
  _lastRequestId = 0;
1951
2073
  _sessionId;
2074
+ /** #543 — discovery is attempted at most once per connect. */
2075
+ triedDiscoveredEntryPath = false;
1952
2076
  _sessionInfo = null;
1953
2077
  _authState = { isAnonymous: true, scopes: [] };
1954
2078
  // Logging and tracing
@@ -1956,6 +2080,8 @@ var McpTestClient = class {
1956
2080
  _traces = [];
1957
2081
  _notifications = [];
1958
2082
  _progressUpdates = [];
2083
+ collectingProgress = false;
2084
+ progressTokenCounter = 0;
1959
2085
  // Interceptor chain
1960
2086
  _interceptors;
1961
2087
  // Elicitation handler for server→client elicit requests
@@ -1964,8 +2090,14 @@ var McpTestClient = class {
1964
2090
  // CONSTRUCTOR & FACTORY
1965
2091
  // ═══════════════════════════════════════════════════════════════════
1966
2092
  constructor(config) {
2093
+ if (config.protocolVersion && UNSUPPORTED_PROTOCOL_VERSIONS.includes(config.protocolVersion)) {
2094
+ throw new Error(
2095
+ `McpTestClient does not support protocol version ${config.protocolVersion}: it has no initialize handshake. Use ${DEFAULT_PROTOCOL_VERSION} or earlier.`
2096
+ );
2097
+ }
1967
2098
  this.config = {
1968
2099
  baseUrl: config.baseUrl,
2100
+ entryPath: config.entryPath,
1969
2101
  transport: config.transport ?? "streamable-http",
1970
2102
  auth: config.auth ?? {},
1971
2103
  publicMode: config.publicMode ?? false,
@@ -2001,11 +2133,13 @@ var McpTestClient = class {
2001
2133
  */
2002
2134
  async connect() {
2003
2135
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
2136
+ this.triedDiscoveredEntryPath = false;
2004
2137
  this.transport = this.createTransport();
2005
2138
  await this.transport.connect();
2006
2139
  const maxRetries = 3;
2007
2140
  const retryDelayMs = 500;
2008
2141
  let lastError;
2142
+ let reportedEntryPaths;
2009
2143
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2010
2144
  const initResponse = await this.initialize();
2011
2145
  if (initResponse.success && initResponse.data) {
@@ -2013,13 +2147,27 @@ var McpTestClient = class {
2013
2147
  break;
2014
2148
  }
2015
2149
  lastError = initResponse.error?.message ?? "Unknown error";
2150
+ reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
2151
+ if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
2152
+ this.triedDiscoveredEntryPath = true;
2153
+ const discovered = reportedEntryPaths[0];
2154
+ this.log("info", `Server serves MCP at ${discovered}; reconnecting there (configured: ${this.mcpPathLabel()})`);
2155
+ this.config.entryPath = discovered;
2156
+ await this.transport.close();
2157
+ this.transport = this.createTransport();
2158
+ await this.transport.connect();
2159
+ continue;
2160
+ }
2016
2161
  if (attempt < maxRetries) {
2017
2162
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
2018
2163
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
2019
2164
  }
2020
2165
  }
2021
2166
  if (!this.initResult) {
2022
- throw new Error(`Failed to initialize MCP connection after ${maxRetries} attempts: ${lastError}`);
2167
+ 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.` : "";
2168
+ throw new Error(
2169
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2170
+ );
2023
2171
  }
2024
2172
  this._sessionId = this.transport.getSessionId();
2025
2173
  this._sessionInfo = {
@@ -2032,6 +2180,7 @@ var McpTestClient = class {
2032
2180
  jsonrpc: "2.0",
2033
2181
  method: "notifications/initialized"
2034
2182
  });
2183
+ await this.transport.openNotificationStream?.(this.initResult.protocolVersion);
2035
2184
  this.log("info", `Connected to ${this.initResult.serverInfo?.name ?? "MCP Server"}`);
2036
2185
  return this.initResult;
2037
2186
  }
@@ -2142,8 +2291,8 @@ var McpTestClient = class {
2142
2291
  /**
2143
2292
  * Call a tool by name with arguments
2144
2293
  */
2145
- call: async (name, args) => {
2146
- const response = await this.callTool(name, args);
2294
+ call: async (name, args, options = {}) => {
2295
+ const response = await this.callTool(name, args, this.progressTokenFor(options));
2147
2296
  return this.wrapToolResult(response);
2148
2297
  }
2149
2298
  };
@@ -2285,6 +2434,7 @@ var McpTestClient = class {
2285
2434
  * Collect progress notifications specifically
2286
2435
  */
2287
2436
  collectProgress: () => {
2437
+ this.collectingProgress = true;
2288
2438
  return new ProgressCollector(this._progressUpdates);
2289
2439
  },
2290
2440
  /**
@@ -2590,12 +2740,29 @@ var McpTestClient = class {
2590
2740
  async listTools() {
2591
2741
  return this.request("tools/list", {});
2592
2742
  }
2593
- async callTool(name, args) {
2743
+ async callTool(name, args, progressToken) {
2594
2744
  return this.request("tools/call", {
2595
2745
  name,
2596
- arguments: args ?? {}
2746
+ arguments: args ?? {},
2747
+ ...progressToken !== void 0 && { _meta: { progressToken } }
2597
2748
  });
2598
2749
  }
2750
+ progressTokenFor(options) {
2751
+ if (options.progressToken !== void 0) return options.progressToken;
2752
+ return this.collectingProgress ? `progress-${++this.progressTokenCounter}` : void 0;
2753
+ }
2754
+ recordNotification = (notification) => {
2755
+ const timestamp = /* @__PURE__ */ new Date();
2756
+ this._notifications.push({ method: notification.method, params: notification.params, timestamp });
2757
+ if (notification.method !== "notifications/progress") return;
2758
+ const { progress, total, progressToken } = notification.params ?? {};
2759
+ this._progressUpdates.push({
2760
+ progress: Number(progress),
2761
+ total: typeof total === "number" ? total : void 0,
2762
+ progressToken: typeof progressToken === "string" || typeof progressToken === "number" ? progressToken : void 0,
2763
+ timestamp
2764
+ });
2765
+ };
2599
2766
  async listResources() {
2600
2767
  return this.request("resources/list", {});
2601
2768
  }
@@ -2617,6 +2784,11 @@ var McpTestClient = class {
2617
2784
  // ═══════════════════════════════════════════════════════════════════
2618
2785
  // PRIVATE: TRANSPORT & REQUEST HELPERS
2619
2786
  // ═══════════════════════════════════════════════════════════════════
2787
+ /** The MCP path this client is currently pointed at, for log and error text. */
2788
+ mcpPathLabel() {
2789
+ const entry = (this.config.entryPath ?? "").replace(/^\/+|\/+$/g, "");
2790
+ return entry ? `/${entry}` : "/";
2791
+ }
2620
2792
  createTransport() {
2621
2793
  let baseUrl = this.config.baseUrl;
2622
2794
  if (this.config.queryParams && Object.keys(this.config.queryParams).length > 0) {
@@ -2630,13 +2802,15 @@ var McpTestClient = class {
2630
2802
  case "streamable-http":
2631
2803
  return new StreamableHttpTransport({
2632
2804
  baseUrl,
2805
+ entryPath: this.config.entryPath,
2633
2806
  timeout: this.config.timeout,
2634
2807
  auth: this.config.auth,
2635
2808
  publicMode: this.config.publicMode,
2636
2809
  debug: this.config.debug,
2637
2810
  interceptors: this._interceptors,
2638
2811
  clientInfo: this.config.clientInfo,
2639
- elicitationHandler: this._elicitationHandler
2812
+ elicitationHandler: this._elicitationHandler,
2813
+ notificationHandler: this.recordNotification
2640
2814
  });
2641
2815
  case "sse":
2642
2816
  throw new Error("SSE transport not yet implemented");