@frontmcp/testing 0.7.1 → 0.8.0

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.
Files changed (67) hide show
  1. package/auth/index.d.ts +2 -0
  2. package/auth/index.d.ts.map +1 -1
  3. package/auth/mock-cimd-server.d.ts +174 -0
  4. package/auth/mock-cimd-server.d.ts.map +1 -0
  5. package/auth/mock-oauth-server.d.ts +136 -6
  6. package/auth/mock-oauth-server.d.ts.map +1 -1
  7. package/auth/token-factory.d.ts.map +1 -1
  8. package/client/index.d.ts +1 -1
  9. package/client/index.d.ts.map +1 -1
  10. package/client/mcp-test-client.builder.d.ts +12 -0
  11. package/client/mcp-test-client.builder.d.ts.map +1 -1
  12. package/client/mcp-test-client.d.ts +48 -2
  13. package/client/mcp-test-client.d.ts.map +1 -1
  14. package/client/mcp-test-client.types.d.ts +60 -0
  15. package/client/mcp-test-client.types.d.ts.map +1 -1
  16. package/esm/fixtures/index.mjs +661 -83
  17. package/esm/index.mjs +3245 -219
  18. package/esm/package.json +5 -4
  19. package/esm/perf/index.mjs +4334 -0
  20. package/esm/perf/perf-setup.mjs +31 -0
  21. package/fixtures/fixture-types.d.ts +10 -1
  22. package/fixtures/fixture-types.d.ts.map +1 -1
  23. package/fixtures/index.js +661 -93
  24. package/fixtures/test-fixture.d.ts +1 -1
  25. package/fixtures/test-fixture.d.ts.map +1 -1
  26. package/index.d.ts +5 -1
  27. package/index.d.ts.map +1 -1
  28. package/index.js +3271 -219
  29. package/interceptor/interceptor-chain.d.ts +1 -0
  30. package/interceptor/interceptor-chain.d.ts.map +1 -1
  31. package/package.json +5 -4
  32. package/perf/baseline-store.d.ts +67 -0
  33. package/perf/baseline-store.d.ts.map +1 -0
  34. package/perf/index.d.ts +44 -0
  35. package/perf/index.d.ts.map +1 -0
  36. package/perf/index.js +4404 -0
  37. package/perf/jest-perf-reporter.d.ts +6 -0
  38. package/perf/jest-perf-reporter.d.ts.map +1 -0
  39. package/perf/leak-detector.d.ts +81 -0
  40. package/perf/leak-detector.d.ts.map +1 -0
  41. package/perf/metrics-collector.d.ts +83 -0
  42. package/perf/metrics-collector.d.ts.map +1 -0
  43. package/perf/perf-fixtures.d.ts +107 -0
  44. package/perf/perf-fixtures.d.ts.map +1 -0
  45. package/perf/perf-setup.d.ts +9 -0
  46. package/perf/perf-setup.d.ts.map +1 -0
  47. package/perf/perf-setup.js +50 -0
  48. package/perf/perf-test.d.ts +69 -0
  49. package/perf/perf-test.d.ts.map +1 -0
  50. package/perf/regression-detector.d.ts +55 -0
  51. package/perf/regression-detector.d.ts.map +1 -0
  52. package/perf/report-generator.d.ts +66 -0
  53. package/perf/report-generator.d.ts.map +1 -0
  54. package/perf/types.d.ts +439 -0
  55. package/perf/types.d.ts.map +1 -0
  56. package/platform/platform-client-info.d.ts +18 -0
  57. package/platform/platform-client-info.d.ts.map +1 -1
  58. package/server/index.d.ts +2 -0
  59. package/server/index.d.ts.map +1 -1
  60. package/server/port-registry.d.ts +179 -0
  61. package/server/port-registry.d.ts.map +1 -0
  62. package/server/test-server.d.ts +9 -5
  63. package/server/test-server.d.ts.map +1 -1
  64. package/transport/streamable-http.transport.d.ts +26 -0
  65. package/transport/streamable-http.transport.d.ts.map +1 -1
  66. package/transport/transport.interface.d.ts +9 -1
  67. package/transport/transport.interface.d.ts.map +1 -1
package/fixtures/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  "use strict";
2
- var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
6
  var __export = (target, all) => {
9
7
  for (var name in all)
@@ -17,14 +15,6 @@ var __copyProps = (to, from, except, desc) => {
17
15
  }
18
16
  return to;
19
17
  };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
19
 
30
20
  // libs/testing/src/fixtures/index.ts
@@ -92,7 +82,12 @@ function getPlatformClientInfo(platform) {
92
82
  }
93
83
  function getPlatformCapabilities(platform) {
94
84
  const baseCapabilities = {
95
- sampling: {}
85
+ sampling: {},
86
+ // Include elicitation.form by default for testing elicitation workflows
87
+ // Note: MCP SDK expects form to be an object, not boolean
88
+ elicitation: {
89
+ form: {}
90
+ }
96
91
  };
97
92
  if (platform === "ext-apps") {
98
93
  return {
@@ -230,6 +225,21 @@ var McpTestClientBuilder = class {
230
225
  this.config.capabilities = capabilities;
231
226
  return this;
232
227
  }
228
+ /**
229
+ * Set query parameters to append to the connection URL.
230
+ * Useful for testing mode switches like `?mode=skills_only`.
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * const client = await McpTestClient.create({ baseUrl })
235
+ * .withQueryParams({ mode: 'skills_only' })
236
+ * .buildAndConnect();
237
+ * ```
238
+ */
239
+ withQueryParams(params) {
240
+ this.config.queryParams = { ...this.config.queryParams, ...params };
241
+ return this;
242
+ }
233
243
  /**
234
244
  * Build the McpTestClient instance (does not connect)
235
245
  */
@@ -258,6 +268,7 @@ var StreamableHttpTransport = class {
258
268
  lastRequestHeaders = {};
259
269
  interceptors;
260
270
  publicMode;
271
+ elicitationHandler;
261
272
  constructor(config) {
262
273
  this.config = {
263
274
  baseUrl: config.baseUrl.replace(/\/$/, ""),
@@ -272,6 +283,7 @@ var StreamableHttpTransport = class {
272
283
  this.authToken = config.auth?.token;
273
284
  this.interceptors = config.interceptors;
274
285
  this.publicMode = config.publicMode ?? false;
286
+ this.elicitationHandler = config.elicitationHandler;
275
287
  }
276
288
  async connect() {
277
289
  this.state = "connecting";
@@ -364,7 +376,6 @@ var StreamableHttpTransport = class {
364
376
  body: JSON.stringify(message),
365
377
  signal: controller.signal
366
378
  });
367
- clearTimeout(timeoutId);
368
379
  const newSessionId = response.headers.get("mcp-session-id");
369
380
  if (newSessionId) {
370
381
  this.sessionId = newSessionId;
@@ -384,28 +395,26 @@ var StreamableHttpTransport = class {
384
395
  };
385
396
  } else {
386
397
  const contentType = response.headers.get("content-type") ?? "";
387
- const text = await response.text();
388
- this.log("Response:", text);
389
- if (!text.trim()) {
390
- jsonResponse = {
391
- jsonrpc: "2.0",
392
- id: message.id ?? null,
393
- result: void 0
394
- };
395
- } else if (contentType.includes("text/event-stream")) {
396
- const { response: sseResponse, sseSessionId } = this.parseSSEResponseWithSession(text, message.id);
397
- jsonResponse = sseResponse;
398
- if (sseSessionId && !this.sessionId) {
399
- this.sessionId = sseSessionId;
400
- this.log("Session ID from SSE:", this.sessionId);
401
- }
398
+ if (contentType.includes("text/event-stream")) {
399
+ jsonResponse = await this.handleSSEResponseWithElicitation(response, message);
402
400
  } else {
403
- jsonResponse = JSON.parse(text);
401
+ const text = await response.text();
402
+ this.log("Response:", text);
403
+ if (!text.trim()) {
404
+ jsonResponse = {
405
+ jsonrpc: "2.0",
406
+ id: message.id ?? null,
407
+ result: void 0
408
+ };
409
+ } else {
410
+ jsonResponse = JSON.parse(text);
411
+ }
404
412
  }
405
413
  }
406
414
  if (this.interceptors) {
407
415
  jsonResponse = await this.interceptors.processResponse(message, jsonResponse, Date.now() - startTime);
408
416
  }
417
+ clearTimeout(timeoutId);
409
418
  return jsonResponse;
410
419
  } catch (error) {
411
420
  clearTimeout(timeoutId);
@@ -520,6 +529,9 @@ var StreamableHttpTransport = class {
520
529
  getInterceptors() {
521
530
  return this.interceptors;
522
531
  }
532
+ setElicitationHandler(handler) {
533
+ this.elicitationHandler = handler;
534
+ }
523
535
  getConnectionCount() {
524
536
  return this.connectionCount;
525
537
  }
@@ -548,6 +560,215 @@ var StreamableHttpTransport = class {
548
560
  // ═══════════════════════════════════════════════════════════════════
549
561
  // PRIVATE HELPERS
550
562
  // ═══════════════════════════════════════════════════════════════════
563
+ /**
564
+ * Handle SSE response with elicitation support.
565
+ *
566
+ * Streams the SSE response, detects elicitation/create requests, and handles them
567
+ * by calling the registered handler and sending the response back to the server.
568
+ */
569
+ async handleSSEResponseWithElicitation(response, originalRequest) {
570
+ this.log("handleSSEResponseWithElicitation: starting", { requestId: originalRequest.id });
571
+ const reader = response.body?.getReader();
572
+ if (!reader) {
573
+ this.log("handleSSEResponseWithElicitation: no response body");
574
+ return {
575
+ jsonrpc: "2.0",
576
+ id: originalRequest.id ?? null,
577
+ error: { code: -32e3, message: "No response body" }
578
+ };
579
+ }
580
+ const decoder = new TextDecoder();
581
+ let buffer = "";
582
+ let finalResponse = null;
583
+ let sseSessionId;
584
+ try {
585
+ let readCount = 0;
586
+ while (true) {
587
+ readCount++;
588
+ this.log(`handleSSEResponseWithElicitation: reading chunk ${readCount}`);
589
+ const { done, value } = await reader.read();
590
+ this.log(`handleSSEResponseWithElicitation: read result`, { done, valueLength: value?.length });
591
+ if (done) {
592
+ if (buffer.trim()) {
593
+ const parsed = this.parseSSEEvents(buffer, originalRequest.id);
594
+ for (const event of parsed.events) {
595
+ const handled = await this.handleSSEEvent(event);
596
+ if (handled.isFinal) {
597
+ finalResponse = handled.response;
598
+ }
599
+ }
600
+ if (parsed.sessionId && !sseSessionId) {
601
+ sseSessionId = parsed.sessionId;
602
+ }
603
+ }
604
+ break;
605
+ }
606
+ buffer += decoder.decode(value, { stream: true });
607
+ const eventEndPattern = /\n\n/g;
608
+ let lastEventEnd = 0;
609
+ let match;
610
+ while ((match = eventEndPattern.exec(buffer)) !== null) {
611
+ const eventText = buffer.slice(lastEventEnd, match.index);
612
+ lastEventEnd = match.index + 2;
613
+ if (eventText.trim()) {
614
+ const parsed = this.parseSSEEvents(eventText, originalRequest.id);
615
+ for (const event of parsed.events) {
616
+ const handled = await this.handleSSEEvent(event);
617
+ if (handled.isFinal) {
618
+ finalResponse = handled.response;
619
+ }
620
+ }
621
+ if (parsed.sessionId && !sseSessionId) {
622
+ sseSessionId = parsed.sessionId;
623
+ }
624
+ }
625
+ }
626
+ buffer = buffer.slice(lastEventEnd);
627
+ }
628
+ } finally {
629
+ reader.releaseLock();
630
+ }
631
+ if (sseSessionId && !this.sessionId) {
632
+ this.sessionId = sseSessionId;
633
+ this.log("Session ID from SSE:", this.sessionId);
634
+ }
635
+ if (finalResponse) {
636
+ return finalResponse;
637
+ }
638
+ return {
639
+ jsonrpc: "2.0",
640
+ id: originalRequest.id ?? null,
641
+ error: { code: -32e3, message: "No final response received in SSE stream" }
642
+ };
643
+ }
644
+ /**
645
+ * Parse SSE event text into structured events
646
+ */
647
+ parseSSEEvents(text, _requestId) {
648
+ const lines = text.split("\n");
649
+ const events = [];
650
+ let currentEvent = { type: "message", data: [] };
651
+ let sessionId;
652
+ for (const line of lines) {
653
+ if (line.startsWith("event: ")) {
654
+ currentEvent.type = line.slice(7);
655
+ } else if (line.startsWith("data: ")) {
656
+ currentEvent.data.push(line.slice(6));
657
+ } else if (line === "data:") {
658
+ currentEvent.data.push("");
659
+ } else if (line.startsWith("id: ")) {
660
+ const idValue = line.slice(4);
661
+ currentEvent.id = idValue;
662
+ const colonIndex = idValue.lastIndexOf(":");
663
+ if (colonIndex > 0) {
664
+ sessionId = idValue.substring(0, colonIndex);
665
+ } else {
666
+ sessionId = idValue;
667
+ }
668
+ } else if (line === "" && currentEvent.data.length > 0) {
669
+ events.push({
670
+ type: currentEvent.type,
671
+ data: currentEvent.data.join("\n"),
672
+ id: currentEvent.id
673
+ });
674
+ currentEvent = { type: "message", data: [] };
675
+ }
676
+ }
677
+ if (currentEvent.data.length > 0) {
678
+ events.push({
679
+ type: currentEvent.type,
680
+ data: currentEvent.data.join("\n"),
681
+ id: currentEvent.id
682
+ });
683
+ }
684
+ return { events, sessionId };
685
+ }
686
+ /**
687
+ * Handle a single SSE event, including elicitation requests
688
+ */
689
+ async handleSSEEvent(event) {
690
+ this.log("SSE Event:", { type: event.type, data: event.data.slice(0, 200) });
691
+ try {
692
+ const parsed = JSON.parse(event.data);
693
+ if ("method" in parsed && parsed.method === "elicitation/create") {
694
+ await this.handleElicitationRequest(parsed);
695
+ return {
696
+ isFinal: false,
697
+ response: { jsonrpc: "2.0", id: null, result: void 0 }
698
+ };
699
+ }
700
+ if ("result" in parsed || "error" in parsed) {
701
+ return { isFinal: true, response: parsed };
702
+ }
703
+ return {
704
+ isFinal: false,
705
+ response: { jsonrpc: "2.0", id: null, result: void 0 }
706
+ };
707
+ } catch {
708
+ this.log("Failed to parse SSE event data:", event.data);
709
+ return {
710
+ isFinal: false,
711
+ response: { jsonrpc: "2.0", id: null, result: void 0 }
712
+ };
713
+ }
714
+ }
715
+ /**
716
+ * Handle an elicitation/create request from the server
717
+ */
718
+ async handleElicitationRequest(request) {
719
+ const params = request.params;
720
+ this.log("Elicitation request received:", {
721
+ mode: params?.mode,
722
+ message: params?.message?.slice(0, 100)
723
+ });
724
+ const requestId = request.id;
725
+ if (requestId === void 0 || requestId === null) {
726
+ this.log("Elicitation request has no ID, cannot respond");
727
+ return;
728
+ }
729
+ if (!this.elicitationHandler) {
730
+ this.log("No elicitation handler registered, sending error");
731
+ await this.sendElicitationResponse(requestId, {
732
+ action: "decline"
733
+ });
734
+ return;
735
+ }
736
+ try {
737
+ const response = await this.elicitationHandler(params);
738
+ this.log("Elicitation handler response:", response);
739
+ await this.sendElicitationResponse(requestId, response);
740
+ } catch (error) {
741
+ this.log("Elicitation handler error:", error);
742
+ await this.sendElicitationResponse(requestId, {
743
+ action: "cancel"
744
+ });
745
+ }
746
+ }
747
+ /**
748
+ * Send an elicitation response back to the server
749
+ */
750
+ async sendElicitationResponse(requestId, response) {
751
+ const headers = this.buildHeaders();
752
+ const url = `${this.config.baseUrl}/`;
753
+ const rpcResponse = {
754
+ jsonrpc: "2.0",
755
+ id: requestId,
756
+ result: response
757
+ };
758
+ this.log("Sending elicitation response:", rpcResponse);
759
+ try {
760
+ const fetchResponse = await fetch(url, {
761
+ method: "POST",
762
+ headers,
763
+ body: JSON.stringify(rpcResponse)
764
+ });
765
+ if (!fetchResponse.ok) {
766
+ this.log(`Elicitation response HTTP error: ${fetchResponse.status}`);
767
+ }
768
+ } catch (error) {
769
+ this.log("Failed to send elicitation response:", error);
770
+ }
771
+ }
551
772
  buildHeaders() {
552
773
  const headers = {
553
774
  "Content-Type": "application/json",
@@ -915,7 +1136,7 @@ var DEFAULT_CLIENT_INFO = {
915
1136
  version: "0.4.0"
916
1137
  };
917
1138
  var McpTestClient = class {
918
- // Platform and capabilities are optional - only set when testing platform-specific behavior
1139
+ // Platform, capabilities, and queryParams are optional - only set when needed
919
1140
  config;
920
1141
  transport = null;
921
1142
  initResult = null;
@@ -931,6 +1152,8 @@ var McpTestClient = class {
931
1152
  _progressUpdates = [];
932
1153
  // Interceptor chain
933
1154
  _interceptors;
1155
+ // Elicitation handler for server→client elicit requests
1156
+ _elicitationHandler;
934
1157
  // ═══════════════════════════════════════════════════════════════════
935
1158
  // CONSTRUCTOR & FACTORY
936
1159
  // ═══════════════════════════════════════════════════════════════════
@@ -945,7 +1168,8 @@ var McpTestClient = class {
945
1168
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
946
1169
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
947
1170
  platform: config.platform,
948
- capabilities: config.capabilities
1171
+ capabilities: config.capabilities,
1172
+ queryParams: config.queryParams
949
1173
  };
950
1174
  if (config.auth?.token) {
951
1175
  this._authState = {
@@ -1178,9 +1402,9 @@ var McpTestClient = class {
1178
1402
  * Send any JSON-RPC request
1179
1403
  */
1180
1404
  request: async (message) => {
1181
- this.ensureConnected();
1405
+ const transport = this.getConnectedTransport();
1182
1406
  const start = Date.now();
1183
- const response = await this.transport.request(message);
1407
+ const response = await transport.request(message);
1184
1408
  this.traceRequest(message.method, message.params, message.id, response, Date.now() - start);
1185
1409
  return response;
1186
1410
  },
@@ -1188,15 +1412,15 @@ var McpTestClient = class {
1188
1412
  * Send a notification (no response expected)
1189
1413
  */
1190
1414
  notify: async (message) => {
1191
- this.ensureConnected();
1192
- await this.transport.notify(message);
1415
+ const transport = this.getConnectedTransport();
1416
+ await transport.notify(message);
1193
1417
  },
1194
1418
  /**
1195
1419
  * Send raw string data (for error testing)
1196
1420
  */
1197
1421
  sendRaw: async (data) => {
1198
- this.ensureConnected();
1199
- return this.transport.sendRaw(data);
1422
+ const transport = this.getConnectedTransport();
1423
+ return transport.sendRaw(data);
1200
1424
  }
1201
1425
  };
1202
1426
  get lastRequestId() {
@@ -1252,6 +1476,63 @@ var McpTestClient = class {
1252
1476
  }
1253
1477
  };
1254
1478
  // ═══════════════════════════════════════════════════════════════════
1479
+ // ELICITATION
1480
+ // ═══════════════════════════════════════════════════════════════════
1481
+ /**
1482
+ * Register a handler for elicitation requests from the server.
1483
+ *
1484
+ * When a tool calls `this.elicit()` during execution, the server sends an
1485
+ * `elicitation/create` request to the client. This handler is called to
1486
+ * provide the response that would normally come from user interaction.
1487
+ *
1488
+ * @param handler - Function that receives the elicitation request and returns a response
1489
+ *
1490
+ * @example
1491
+ * ```typescript
1492
+ * // Simple acceptance
1493
+ * mcp.onElicitation(async () => ({
1494
+ * action: 'accept',
1495
+ * content: { confirmed: true }
1496
+ * }));
1497
+ *
1498
+ * // Conditional response based on request
1499
+ * mcp.onElicitation(async (request) => {
1500
+ * if (request.message.includes('delete')) {
1501
+ * return { action: 'decline' };
1502
+ * }
1503
+ * return { action: 'accept', content: { approved: true } };
1504
+ * });
1505
+ *
1506
+ * // Multi-step wizard
1507
+ * let step = 0;
1508
+ * mcp.onElicitation(async () => {
1509
+ * step++;
1510
+ * if (step === 1) return { action: 'accept', content: { name: 'Alice' } };
1511
+ * return { action: 'accept', content: { color: 'blue' } };
1512
+ * });
1513
+ * ```
1514
+ */
1515
+ onElicitation(handler) {
1516
+ this._elicitationHandler = handler;
1517
+ if (this.transport?.setElicitationHandler) {
1518
+ this.transport.setElicitationHandler(handler);
1519
+ }
1520
+ this.log("debug", "Elicitation handler registered");
1521
+ }
1522
+ /**
1523
+ * Clear the elicitation handler.
1524
+ *
1525
+ * After calling this, elicitation requests from the server will not be
1526
+ * handled automatically. This can be used to test timeout scenarios.
1527
+ */
1528
+ clearElicitationHandler() {
1529
+ this._elicitationHandler = void 0;
1530
+ if (this.transport?.setElicitationHandler) {
1531
+ this.transport.setElicitationHandler(void 0);
1532
+ }
1533
+ this.log("debug", "Elicitation handler cleared");
1534
+ }
1535
+ // ═══════════════════════════════════════════════════════════════════
1255
1536
  // LOGGING & DEBUGGING
1256
1537
  // ═══════════════════════════════════════════════════════════════════
1257
1538
  logs = {
@@ -1476,7 +1757,10 @@ var McpTestClient = class {
1476
1757
  // ═══════════════════════════════════════════════════════════════════
1477
1758
  async initialize() {
1478
1759
  const capabilities = this.config.capabilities ?? {
1479
- sampling: {}
1760
+ sampling: {},
1761
+ elicitation: {
1762
+ form: {}
1763
+ }
1480
1764
  };
1481
1765
  return this.request("initialize", {
1482
1766
  protocolVersion: this.config.protocolVersion,
@@ -1515,16 +1799,25 @@ var McpTestClient = class {
1515
1799
  // PRIVATE: TRANSPORT & REQUEST HELPERS
1516
1800
  // ═══════════════════════════════════════════════════════════════════
1517
1801
  createTransport() {
1802
+ let baseUrl = this.config.baseUrl;
1803
+ if (this.config.queryParams && Object.keys(this.config.queryParams).length > 0) {
1804
+ const url = new URL(baseUrl);
1805
+ Object.entries(this.config.queryParams).forEach(([key, value]) => {
1806
+ url.searchParams.set(key, String(value));
1807
+ });
1808
+ baseUrl = url.toString();
1809
+ }
1518
1810
  switch (this.config.transport) {
1519
1811
  case "streamable-http":
1520
1812
  return new StreamableHttpTransport({
1521
- baseUrl: this.config.baseUrl,
1813
+ baseUrl,
1522
1814
  timeout: this.config.timeout,
1523
1815
  auth: this.config.auth,
1524
1816
  publicMode: this.config.publicMode,
1525
1817
  debug: this.config.debug,
1526
1818
  interceptors: this._interceptors,
1527
- clientInfo: this.config.clientInfo
1819
+ clientInfo: this.config.clientInfo,
1820
+ elicitationHandler: this._elicitationHandler
1528
1821
  });
1529
1822
  case "sse":
1530
1823
  throw new Error("SSE transport not yet implemented");
@@ -1533,12 +1826,12 @@ var McpTestClient = class {
1533
1826
  }
1534
1827
  }
1535
1828
  async request(method, params) {
1536
- this.ensureConnected();
1829
+ const transport = this.getConnectedTransport();
1537
1830
  const id = ++this.requestIdCounter;
1538
1831
  this._lastRequestId = id;
1539
1832
  const start = Date.now();
1540
1833
  try {
1541
- const response = await this.transport.request({
1834
+ const response = await transport.request({
1542
1835
  jsonrpc: "2.0",
1543
1836
  id,
1544
1837
  method,
@@ -1577,10 +1870,14 @@ var McpTestClient = class {
1577
1870
  };
1578
1871
  }
1579
1872
  }
1580
- ensureConnected() {
1581
- if (!this.transport?.isConnected()) {
1873
+ /**
1874
+ * Get the transport, throwing if not connected.
1875
+ */
1876
+ getConnectedTransport() {
1877
+ if (!this.transport || !this.transport.isConnected()) {
1582
1878
  throw new Error("Not connected to MCP server. Call connect() first.");
1583
1879
  }
1880
+ return this.transport;
1584
1881
  }
1585
1882
  updateSessionActivity() {
1586
1883
  if (this._sessionInfo) {
@@ -1833,6 +2130,9 @@ var TestTokenFactory = class {
1833
2130
  scope: options.scopes?.join(" "),
1834
2131
  ...options.claims
1835
2132
  };
2133
+ if (!this.privateKey) {
2134
+ throw new Error("Private key not initialized");
2135
+ }
1836
2136
  const token = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
1837
2137
  return token;
1838
2138
  }
@@ -1892,6 +2192,9 @@ var TestTokenFactory = class {
1892
2192
  exp: now - 3600
1893
2193
  // Expired 1 hour ago
1894
2194
  };
2195
+ if (!this.privateKey) {
2196
+ throw new Error("Private key not initialized");
2197
+ }
1895
2198
  const token = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
1896
2199
  return token;
1897
2200
  }
@@ -1918,6 +2221,9 @@ var TestTokenFactory = class {
1918
2221
  */
1919
2222
  async getPublicJwks() {
1920
2223
  await this.ensureKeys();
2224
+ if (!this.jwk) {
2225
+ throw new Error("JWK not initialized");
2226
+ }
1921
2227
  return {
1922
2228
  keys: [this.jwk]
1923
2229
  };
@@ -1938,21 +2244,195 @@ var TestTokenFactory = class {
1938
2244
 
1939
2245
  // libs/testing/src/server/test-server.ts
1940
2246
  var import_child_process = require("child_process");
2247
+
2248
+ // libs/testing/src/errors/index.ts
2249
+ var TestClientError = class extends Error {
2250
+ constructor(message) {
2251
+ super(message);
2252
+ this.name = "TestClientError";
2253
+ Object.setPrototypeOf(this, new.target.prototype);
2254
+ }
2255
+ };
2256
+ var ServerStartError = class extends TestClientError {
2257
+ constructor(message, cause) {
2258
+ super(message);
2259
+ this.cause = cause;
2260
+ this.name = "ServerStartError";
2261
+ Object.setPrototypeOf(this, new.target.prototype);
2262
+ }
2263
+ };
2264
+
2265
+ // libs/testing/src/server/port-registry.ts
2266
+ var import_net = require("net");
2267
+ var E2E_PORT_RANGES = {
2268
+ // Core E2E tests (50000-50099)
2269
+ "demo-e2e-public": { start: 5e4, size: 10 },
2270
+ "demo-e2e-cache": { start: 50010, size: 10 },
2271
+ "demo-e2e-config": { start: 50020, size: 10 },
2272
+ "demo-e2e-direct": { start: 50030, size: 10 },
2273
+ "demo-e2e-errors": { start: 50040, size: 10 },
2274
+ "demo-e2e-hooks": { start: 50050, size: 10 },
2275
+ "demo-e2e-multiapp": { start: 50060, size: 10 },
2276
+ "demo-e2e-notifications": { start: 50070, size: 10 },
2277
+ "demo-e2e-providers": { start: 50080, size: 10 },
2278
+ "demo-e2e-standalone": { start: 50090, size: 10 },
2279
+ // Auth E2E tests (50100-50199)
2280
+ "demo-e2e-orchestrated": { start: 50100, size: 10 },
2281
+ "demo-e2e-transparent": { start: 50110, size: 10 },
2282
+ "demo-e2e-cimd": { start: 50120, size: 10 },
2283
+ // Feature E2E tests (50200-50299)
2284
+ "demo-e2e-skills": { start: 50200, size: 10 },
2285
+ "demo-e2e-remote": { start: 50210, size: 10 },
2286
+ "demo-e2e-openapi": { start: 50220, size: 10 },
2287
+ "demo-e2e-ui": { start: 50230, size: 10 },
2288
+ "demo-e2e-codecall": { start: 50240, size: 10 },
2289
+ "demo-e2e-remember": { start: 50250, size: 10 },
2290
+ "demo-e2e-elicitation": { start: 50260, size: 10 },
2291
+ "demo-e2e-agents": { start: 50270, size: 10 },
2292
+ "demo-e2e-transport-recreation": { start: 50280, size: 10 },
2293
+ // Infrastructure E2E tests (50300-50399)
2294
+ "demo-e2e-redis": { start: 50300, size: 10 },
2295
+ "demo-e2e-serverless": { start: 50310, size: 10 },
2296
+ // Mock servers and utilities (50900-50999)
2297
+ "mock-oauth": { start: 50900, size: 10 },
2298
+ "mock-api": { start: 50910, size: 10 },
2299
+ "mock-cimd": { start: 50920, size: 10 },
2300
+ // Dynamic/unknown projects (51000+)
2301
+ default: { start: 51e3, size: 100 }
2302
+ };
2303
+ var reservedPorts = /* @__PURE__ */ new Map();
2304
+ var projectPortIndex = /* @__PURE__ */ new Map();
2305
+ function getPortRange(project) {
2306
+ const key = project;
2307
+ if (key in E2E_PORT_RANGES) {
2308
+ return E2E_PORT_RANGES[key];
2309
+ }
2310
+ return E2E_PORT_RANGES.default;
2311
+ }
2312
+ async function reservePort(project, preferredPort) {
2313
+ const range = getPortRange(project);
2314
+ if (preferredPort !== void 0) {
2315
+ const reservation = await tryReservePort(preferredPort, project);
2316
+ if (reservation) {
2317
+ return {
2318
+ port: preferredPort,
2319
+ release: async () => {
2320
+ await releasePort(preferredPort);
2321
+ }
2322
+ };
2323
+ }
2324
+ console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
2325
+ }
2326
+ let index = projectPortIndex.get(project) ?? 0;
2327
+ const maxAttempts = range.size;
2328
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2329
+ const port = range.start + index % range.size;
2330
+ index = (index + 1) % range.size;
2331
+ if (reservedPorts.has(port)) {
2332
+ continue;
2333
+ }
2334
+ const reservation = await tryReservePort(port, project);
2335
+ if (reservation) {
2336
+ projectPortIndex.set(project, index);
2337
+ return {
2338
+ port,
2339
+ release: async () => {
2340
+ await releasePort(port);
2341
+ }
2342
+ };
2343
+ }
2344
+ }
2345
+ const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
2346
+ if (dynamicPort) {
2347
+ const reservation = await tryReservePort(dynamicPort, project);
2348
+ if (reservation) {
2349
+ return {
2350
+ port: dynamicPort,
2351
+ release: async () => {
2352
+ await releasePort(dynamicPort);
2353
+ }
2354
+ };
2355
+ }
2356
+ }
2357
+ throw new Error(
2358
+ `[PortRegistry] Could not reserve a port for ${project}. Range: ${range.start}-${range.start + range.size - 1}. Currently reserved: ${Array.from(reservedPorts.keys()).join(", ")}`
2359
+ );
2360
+ }
2361
+ async function tryReservePort(port, project) {
2362
+ return new Promise((resolve) => {
2363
+ const server = (0, import_net.createServer)();
2364
+ server.once("error", () => {
2365
+ resolve(false);
2366
+ });
2367
+ server.listen(port, "::", () => {
2368
+ reservedPorts.set(port, {
2369
+ port,
2370
+ project,
2371
+ holder: server,
2372
+ reservedAt: Date.now()
2373
+ });
2374
+ resolve(true);
2375
+ });
2376
+ });
2377
+ }
2378
+ async function releasePort(port) {
2379
+ const reservation = reservedPorts.get(port);
2380
+ if (!reservation) {
2381
+ return;
2382
+ }
2383
+ return new Promise((resolve) => {
2384
+ reservation.holder.close(() => {
2385
+ reservedPorts.delete(port);
2386
+ resolve();
2387
+ });
2388
+ });
2389
+ }
2390
+ async function findAvailablePortInRange(start, end) {
2391
+ for (let port = start; port < end; port++) {
2392
+ if (reservedPorts.has(port)) {
2393
+ continue;
2394
+ }
2395
+ const available = await isPortAvailable(port);
2396
+ if (available) {
2397
+ return port;
2398
+ }
2399
+ }
2400
+ return null;
2401
+ }
2402
+ async function isPortAvailable(port) {
2403
+ return new Promise((resolve) => {
2404
+ const server = (0, import_net.createServer)();
2405
+ server.once("error", () => {
2406
+ resolve(false);
2407
+ });
2408
+ server.listen(port, "::", () => {
2409
+ server.close(() => {
2410
+ resolve(true);
2411
+ });
2412
+ });
2413
+ });
2414
+ }
2415
+
2416
+ // libs/testing/src/server/test-server.ts
2417
+ var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
1941
2418
  var TestServer = class _TestServer {
1942
2419
  process = null;
1943
2420
  options;
1944
2421
  _info;
1945
2422
  logs = [];
1946
- constructor(options, port) {
2423
+ portRelease = null;
2424
+ constructor(options, port, portRelease) {
1947
2425
  this.options = {
1948
2426
  port,
2427
+ project: options.project,
1949
2428
  command: options.command ?? "",
1950
2429
  cwd: options.cwd ?? process.cwd(),
1951
2430
  env: options.env ?? {},
1952
2431
  startupTimeout: options.startupTimeout ?? 3e4,
1953
2432
  healthCheckPath: options.healthCheckPath ?? "/health",
1954
- debug: options.debug ?? false
2433
+ debug: options.debug ?? DEBUG_SERVER
1955
2434
  };
2435
+ this.portRelease = portRelease ?? null;
1956
2436
  this._info = {
1957
2437
  baseUrl: `http://localhost:${port}`,
1958
2438
  port
@@ -1962,7 +2442,9 @@ var TestServer = class _TestServer {
1962
2442
  * Start a test server with custom command
1963
2443
  */
1964
2444
  static async start(options) {
1965
- const port = options.port ?? await findAvailablePort();
2445
+ const project = options.project ?? "default";
2446
+ const { port, release } = await reservePort(project, options.port);
2447
+ await release();
1966
2448
  const server = new _TestServer(options, port);
1967
2449
  try {
1968
2450
  await server.startProcess();
@@ -1981,10 +2463,12 @@ var TestServer = class _TestServer {
1981
2463
  `Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`
1982
2464
  );
1983
2465
  }
1984
- const port = options.port ?? await findAvailablePort();
2466
+ const { port, release } = await reservePort(project, options.port);
2467
+ await release();
1985
2468
  const serverOptions = {
1986
2469
  ...options,
1987
2470
  port,
2471
+ project,
1988
2472
  command: `npx nx serve ${project} --port ${port}`,
1989
2473
  cwd: options.cwd ?? process.cwd()
1990
2474
  };
@@ -2142,22 +2626,54 @@ var TestServer = class _TestServer {
2142
2626
  exitCode = code;
2143
2627
  this.log(`Server process exited with code ${code}`);
2144
2628
  });
2145
- await this.waitForReadyWithExitDetection(() => {
2146
- if (exitError) {
2147
- return { exited: true, error: exitError };
2148
- }
2149
- if (processExited) {
2150
- const recentLogs = this.logs.slice(-10).join("\n");
2151
- return {
2152
- exited: true,
2153
- error: new Error(`Server process exited unexpectedly with code ${exitCode}.
2629
+ try {
2630
+ await this.waitForReadyWithExitDetection(() => {
2631
+ if (exitError) {
2632
+ return { exited: true, error: exitError };
2633
+ }
2634
+ if (processExited) {
2635
+ const allLogs = this.logs.join("\n");
2636
+ const errorLogs = this.logs.filter((l) => l.includes("[ERROR]") || l.toLowerCase().includes("error")).join("\n");
2637
+ return {
2638
+ exited: true,
2639
+ error: new ServerStartError(
2640
+ `Server process exited unexpectedly with code ${exitCode}.
2154
2641
 
2155
- Recent logs:
2156
- ${recentLogs}`)
2157
- };
2158
- }
2159
- return { exited: false };
2160
- });
2642
+ Command: ${this.options.command}
2643
+ CWD: ${this.options.cwd}
2644
+ Port: ${this.options.port}
2645
+
2646
+ === Error Logs ===
2647
+ ${errorLogs || "No error logs captured"}
2648
+
2649
+ === Full Logs ===
2650
+ ${allLogs || "No logs captured"}`
2651
+ )
2652
+ };
2653
+ }
2654
+ return { exited: false };
2655
+ });
2656
+ } catch (error) {
2657
+ this.printLogsOnFailure("Server startup failed");
2658
+ throw error;
2659
+ }
2660
+ }
2661
+ /**
2662
+ * Print server logs on failure for debugging
2663
+ */
2664
+ printLogsOnFailure(context) {
2665
+ const allLogs = this.logs.join("\n");
2666
+ if (allLogs) {
2667
+ console.error(`
2668
+ [TestServer] ${context}`);
2669
+ console.error(`[TestServer] Command: ${this.options.command}`);
2670
+ console.error(`[TestServer] Port: ${this.options.port}`);
2671
+ console.error(`[TestServer] CWD: ${this.options.cwd}`);
2672
+ console.error(`[TestServer] === Server Logs ===
2673
+ ${allLogs}`);
2674
+ console.error(`[TestServer] === End Logs ===
2675
+ `);
2676
+ }
2161
2677
  }
2162
2678
  /**
2163
2679
  * Wait for server to be ready, but also detect early process exit
@@ -2166,29 +2682,57 @@ ${recentLogs}`)
2166
2682
  const timeoutMs = this.options.startupTimeout;
2167
2683
  const deadline = Date.now() + timeoutMs;
2168
2684
  const checkInterval = 100;
2685
+ let lastHealthCheckError = null;
2686
+ let healthCheckAttempts = 0;
2687
+ this.log(`Waiting for server to be ready (timeout: ${timeoutMs}ms)...`);
2169
2688
  while (Date.now() < deadline) {
2170
2689
  const exitStatus = checkExit();
2171
2690
  if (exitStatus.exited) {
2172
- throw exitStatus.error ?? new Error("Server process exited unexpectedly");
2691
+ throw exitStatus.error ?? new ServerStartError("Server process exited unexpectedly");
2173
2692
  }
2693
+ healthCheckAttempts++;
2174
2694
  try {
2175
- const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {
2695
+ const healthUrl = `${this._info.baseUrl}${this.options.healthCheckPath}`;
2696
+ const response = await fetch(healthUrl, {
2176
2697
  method: "GET",
2177
2698
  signal: AbortSignal.timeout(1e3)
2178
2699
  });
2179
2700
  if (response.ok || response.status === 404) {
2180
- this.log("Server is ready");
2701
+ this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
2181
2702
  return;
2182
2703
  }
2183
- } catch {
2704
+ lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
2705
+ } catch (err) {
2706
+ lastHealthCheckError = err instanceof Error ? err.message : String(err);
2707
+ }
2708
+ const elapsed = Date.now() - (deadline - timeoutMs);
2709
+ if (elapsed > 0 && elapsed % 5e3 < checkInterval) {
2710
+ this.log(
2711
+ `Still waiting for server... (${Math.round(elapsed / 1e3)}s elapsed, last error: ${lastHealthCheckError})`
2712
+ );
2184
2713
  }
2185
2714
  await sleep2(checkInterval);
2186
2715
  }
2187
2716
  const finalExitStatus = checkExit();
2188
2717
  if (finalExitStatus.exited) {
2189
- throw finalExitStatus.error ?? new Error("Server process exited unexpectedly");
2718
+ throw finalExitStatus.error ?? new ServerStartError("Server process exited unexpectedly");
2190
2719
  }
2191
- throw new Error(`Server did not become ready within ${timeoutMs}ms`);
2720
+ const allLogs = this.logs.join("\n");
2721
+ throw new ServerStartError(
2722
+ `Server did not become ready within ${timeoutMs}ms.
2723
+
2724
+ Command: ${this.options.command}
2725
+ CWD: ${this.options.cwd}
2726
+ Port: ${this.options.port}
2727
+ Health check URL: ${this._info.baseUrl}${this.options.healthCheckPath}
2728
+ Health check attempts: ${healthCheckAttempts}
2729
+ Last health check error: ${lastHealthCheckError ?? "none"}
2730
+
2731
+ === Server Logs ===
2732
+ ${allLogs || "No logs captured"}
2733
+
2734
+ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
2735
+ );
2192
2736
  }
2193
2737
  log(message) {
2194
2738
  if (this.options.debug) {
@@ -2196,22 +2740,6 @@ ${recentLogs}`)
2196
2740
  }
2197
2741
  }
2198
2742
  };
2199
- async function findAvailablePort() {
2200
- const { createServer } = await import("net");
2201
- return new Promise((resolve, reject) => {
2202
- const server = createServer();
2203
- server.listen(0, () => {
2204
- const address = server.address();
2205
- if (address && typeof address !== "string") {
2206
- const port = address.port;
2207
- server.close(() => resolve(port));
2208
- } else {
2209
- reject(new Error("Could not get port"));
2210
- }
2211
- });
2212
- server.on("error", reject);
2213
- });
2214
- }
2215
2743
  function sleep2(ms) {
2216
2744
  return new Promise((resolve) => setTimeout(resolve, ms));
2217
2745
  }
@@ -2230,14 +2758,36 @@ async function initializeSharedResources() {
2230
2758
  serverInstance = TestServer.connect(currentConfig.baseUrl);
2231
2759
  serverStartedByUs = false;
2232
2760
  } else if (currentConfig.server) {
2233
- serverInstance = await TestServer.start({
2234
- port: currentConfig.port,
2235
- command: resolveServerCommand(currentConfig.server),
2236
- env: currentConfig.env,
2237
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
2238
- debug: currentConfig.logLevel === "debug"
2239
- });
2240
- serverStartedByUs = true;
2761
+ const serverCommand = resolveServerCommand(currentConfig.server);
2762
+ const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
2763
+ if (isDebug) {
2764
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
2765
+ }
2766
+ try {
2767
+ serverInstance = await TestServer.start({
2768
+ project: currentConfig.project,
2769
+ port: currentConfig.port,
2770
+ command: serverCommand,
2771
+ env: currentConfig.env,
2772
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
2773
+ debug: isDebug
2774
+ });
2775
+ serverStartedByUs = true;
2776
+ if (isDebug) {
2777
+ console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
2778
+ }
2779
+ } catch (error) {
2780
+ const errMsg = error instanceof Error ? error.message : String(error);
2781
+ throw new Error(
2782
+ `Failed to start test server.
2783
+
2784
+ Server entry: ${currentConfig.server}
2785
+ Project: ${currentConfig.project ?? "default"}
2786
+ Command: ${serverCommand}
2787
+
2788
+ Error: ${errMsg}`
2789
+ );
2790
+ }
2241
2791
  } else {
2242
2792
  throw new Error(
2243
2793
  'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
@@ -2247,6 +2797,12 @@ async function initializeSharedResources() {
2247
2797
  }
2248
2798
  async function createTestFixtures() {
2249
2799
  await initializeSharedResources();
2800
+ if (!serverInstance) {
2801
+ throw new Error("Server instance not initialized");
2802
+ }
2803
+ if (!tokenFactory) {
2804
+ throw new Error("Token factory not initialized");
2805
+ }
2250
2806
  const clientInstance = await McpTestClient.create({
2251
2807
  baseUrl: serverInstance.info.baseUrl,
2252
2808
  transport: currentConfig.transport ?? "streamable-http",
@@ -2260,7 +2816,19 @@ async function createTestFixtures() {
2260
2816
  server
2261
2817
  };
2262
2818
  }
2263
- async function cleanupTestFixtures(fixtures, _testFailed = false) {
2819
+ async function cleanupTestFixtures(fixtures, testFailed = false) {
2820
+ if (testFailed && serverInstance) {
2821
+ const logs = serverInstance.getLogs();
2822
+ if (logs.length > 0) {
2823
+ console.error("\n[TestFixture] === Server Logs (test failed) ===");
2824
+ const recentLogs = logs.slice(-50);
2825
+ if (logs.length > 50) {
2826
+ console.error(`[TestFixture] (showing last 50 of ${logs.length} log entries)`);
2827
+ }
2828
+ console.error(recentLogs.join("\n"));
2829
+ console.error("[TestFixture] === End Server Logs ===\n");
2830
+ }
2831
+ }
2264
2832
  if (fixtures.mcp.isConnected()) {
2265
2833
  await fixtures.mcp.disconnect();
2266
2834
  }