@frontmcp/testing 1.8.6 → 1.9.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 (49) hide show
  1. package/auth/mock-oauth-server.d.ts.map +1 -1
  2. package/auth/token-factory.d.ts +2 -2
  3. package/auth/token-factory.d.ts.map +1 -1
  4. package/client/mcp-test-client.d.ts +6 -1
  5. package/client/mcp-test-client.d.ts.map +1 -1
  6. package/esm/fixtures/index.mjs +993 -237
  7. package/esm/index.mjs +1151 -360
  8. package/esm/matchers/index.mjs +3 -0
  9. package/esm/package.json +8 -6
  10. package/esm/perf/index.mjs +874 -280
  11. package/esm/perf/perf-setup.mjs +3 -0
  12. package/esm/playwright/index.mjs +3 -0
  13. package/esm/setup.mjs +3 -0
  14. package/expect.d.ts +12 -11
  15. package/expect.d.ts.map +1 -1
  16. package/fixtures/fixture-types.d.ts +41 -10
  17. package/fixtures/fixture-types.d.ts.map +1 -1
  18. package/fixtures/index.d.ts +1 -1
  19. package/fixtures/index.d.ts.map +1 -1
  20. package/fixtures/index.js +989 -229
  21. package/fixtures/test-fixture.d.ts +6 -5
  22. package/fixtures/test-fixture.d.ts.map +1 -1
  23. package/http-mock/http-mock.d.ts +5 -3
  24. package/http-mock/http-mock.d.ts.map +1 -1
  25. package/http-mock/http-mock.types.d.ts +17 -6
  26. package/http-mock/http-mock.types.d.ts.map +1 -1
  27. package/http-mock/index.d.ts +1 -1
  28. package/http-mock/index.d.ts.map +1 -1
  29. package/index.d.ts +3 -2
  30. package/index.d.ts.map +1 -1
  31. package/index.js +1157 -361
  32. package/package.json +8 -6
  33. package/perf/baseline-store.d.ts +1 -1
  34. package/perf/baseline-store.d.ts.map +1 -1
  35. package/perf/index.js +874 -293
  36. package/perf/perf-test.d.ts.map +1 -1
  37. package/perf/report-generator.d.ts +1 -1
  38. package/perf/report-generator.d.ts.map +1 -1
  39. package/server/port-registry.d.ts +7 -1
  40. package/server/port-registry.d.ts.map +1 -1
  41. package/server/test-server.d.ts +10 -1
  42. package/server/test-server.d.ts.map +1 -1
  43. package/transport/error-utils.d.ts +15 -0
  44. package/transport/error-utils.d.ts.map +1 -0
  45. package/transport/index.d.ts +1 -0
  46. package/transport/index.d.ts.map +1 -1
  47. package/transport/sse.transport.d.ts +80 -0
  48. package/transport/sse.transport.d.ts.map +1 -0
  49. package/transport/streamable-http.transport.d.ts.map +1 -1
package/index.js CHANGED
@@ -60,6 +60,7 @@ __export(index_exports, {
60
60
  RegressionDetector: () => RegressionDetector,
61
61
  ReportGenerator: () => ReportGenerator,
62
62
  ServerStartError: () => ServerStartError,
63
+ SseTransport: () => SseTransport,
63
64
  StreamableHttpTransport: () => StreamableHttpTransport,
64
65
  TestClientError: () => TestClientError,
65
66
  TestServer: () => TestServer,
@@ -460,8 +461,479 @@ var interceptors = {
460
461
  }
461
462
  };
462
463
 
463
- // libs/testing/src/transport/streamable-http.transport.ts
464
+ // libs/testing/src/transport/error-utils.ts
465
+ function errorName(err) {
466
+ if (typeof err !== "object" || err === null) return void 0;
467
+ const name = err.name;
468
+ return typeof name === "string" ? name : void 0;
469
+ }
470
+ function isAbortError(err) {
471
+ return errorName(err) === "AbortError";
472
+ }
473
+ function errorMessage(err, fallback = "Unknown error") {
474
+ if (typeof err === "string" && err) return err;
475
+ if (typeof err === "object" && err !== null) {
476
+ const message = err.message;
477
+ if (typeof message === "string" && message) return message;
478
+ }
479
+ return fallback;
480
+ }
481
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
482
+ function markInterceptedError(err) {
483
+ interceptedErrors.add(err);
484
+ return err;
485
+ }
486
+ function isInterceptedError(err) {
487
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
488
+ }
489
+
490
+ // libs/testing/src/transport/sse.transport.ts
464
491
  var DEFAULT_TIMEOUT = 3e4;
492
+ var SseTransport = class {
493
+ baseUrl;
494
+ entryPath;
495
+ timeout;
496
+ authHeaders;
497
+ publicMode;
498
+ debug;
499
+ clientInfo;
500
+ notificationHandler;
501
+ interceptors;
502
+ elicitationHandler;
503
+ state = "disconnected";
504
+ authToken;
505
+ sessionId;
506
+ messageEndpoint;
507
+ stream;
508
+ pending = /* @__PURE__ */ new Map();
509
+ connectionCount = 0;
510
+ reconnectCount = 0;
511
+ lastRequestHeaders = {};
512
+ constructor(config) {
513
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
514
+ this.entryPath = config.entryPath;
515
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
516
+ this.authHeaders = config.auth?.headers ?? {};
517
+ this.authToken = config.auth?.token;
518
+ this.publicMode = config.publicMode ?? false;
519
+ this.debug = config.debug ?? false;
520
+ this.clientInfo = config.clientInfo;
521
+ this.notificationHandler = config.notificationHandler;
522
+ this.interceptors = config.interceptors;
523
+ this.elicitationHandler = config.elicitationHandler;
524
+ }
525
+ async connect() {
526
+ this.state = "connecting";
527
+ this.connectionCount++;
528
+ try {
529
+ if (!this.publicMode && !this.authToken) {
530
+ await this.requestAnonymousToken();
531
+ }
532
+ await this.openStream();
533
+ this.state = "connected";
534
+ this.log(`Connected; messages go to ${this.messageEndpoint}`);
535
+ } catch (error) {
536
+ this.state = "error";
537
+ this.stream?.abort();
538
+ this.stream = void 0;
539
+ throw error;
540
+ }
541
+ }
542
+ async request(message) {
543
+ this.ensureConnected();
544
+ const startTime = Date.now();
545
+ if (this.interceptors) {
546
+ const intercepted = await this.interceptors.processRequest(message, {
547
+ timestamp: /* @__PURE__ */ new Date(),
548
+ transport: "sse",
549
+ sessionId: this.sessionId
550
+ });
551
+ switch (intercepted.type) {
552
+ case "mock":
553
+ return await this.interceptors.processResponse(
554
+ message,
555
+ intercepted.response,
556
+ Date.now() - startTime
557
+ );
558
+ case "error":
559
+ throw markInterceptedError(intercepted.error);
560
+ case "continue":
561
+ message = intercepted.request;
562
+ break;
563
+ }
564
+ }
565
+ let response;
566
+ if (message.id === void 0) {
567
+ await this.post(JSON.stringify(message));
568
+ response = { jsonrpc: "2.0", id: null, result: void 0 };
569
+ } else {
570
+ response = await this.postAndAwait(message.id, JSON.stringify(message));
571
+ }
572
+ if (this.interceptors) {
573
+ response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
574
+ }
575
+ return response;
576
+ }
577
+ async notify(message) {
578
+ this.ensureConnected();
579
+ const result = await this.post(JSON.stringify(message));
580
+ if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
581
+ }
582
+ async sendRaw(data) {
583
+ this.ensureConnected();
584
+ let id;
585
+ try {
586
+ const parsed = JSON.parse(data);
587
+ if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
588
+ } catch {
589
+ }
590
+ if (id !== void 0) return this.postAndAwait(id, data);
591
+ const result = await this.post(data);
592
+ if (!result.ok) {
593
+ return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
594
+ }
595
+ return { jsonrpc: "2.0", id: null, result: void 0 };
596
+ }
597
+ async close() {
598
+ this.closeStream("Transport closed");
599
+ this.state = "disconnected";
600
+ this.sessionId = void 0;
601
+ this.messageEndpoint = void 0;
602
+ this.log("SSE transport closed");
603
+ }
604
+ isConnected() {
605
+ return this.state === "connected";
606
+ }
607
+ getState() {
608
+ return this.state;
609
+ }
610
+ getSessionId() {
611
+ return this.sessionId;
612
+ }
613
+ setAuthToken(token) {
614
+ this.authToken = token;
615
+ }
616
+ setTimeout(ms) {
617
+ this.timeout = ms;
618
+ }
619
+ getMessageEndpoint() {
620
+ return this.messageEndpoint;
621
+ }
622
+ getConnectionCount() {
623
+ return this.connectionCount;
624
+ }
625
+ getReconnectCount() {
626
+ return this.reconnectCount;
627
+ }
628
+ getLastRequestHeaders() {
629
+ return { ...this.lastRequestHeaders };
630
+ }
631
+ setInterceptors(interceptors2) {
632
+ this.interceptors = interceptors2;
633
+ }
634
+ getInterceptors() {
635
+ return this.interceptors;
636
+ }
637
+ setElicitationHandler(handler) {
638
+ this.elicitationHandler = handler;
639
+ }
640
+ async simulateDisconnect() {
641
+ this.closeStream("Simulated disconnect");
642
+ this.state = "disconnected";
643
+ this.sessionId = void 0;
644
+ this.messageEndpoint = void 0;
645
+ }
646
+ async waitForReconnect(timeoutMs) {
647
+ this.reconnectCount++;
648
+ let timer;
649
+ try {
650
+ await Promise.race([
651
+ this.connect(),
652
+ new Promise((_, reject) => {
653
+ timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
654
+ })
655
+ ]);
656
+ } finally {
657
+ clearTimeout(timer);
658
+ }
659
+ }
660
+ // ═══════════════════════════════════════════════════════════════════
661
+ // STREAM
662
+ // ═══════════════════════════════════════════════════════════════════
663
+ /** Open `GET <entry>/sse` and wait for the `endpoint` event. */
664
+ async openStream() {
665
+ const controller = new AbortController();
666
+ this.stream = controller;
667
+ const url = this.sseUrl();
668
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
669
+ this.log(`GET ${url}`);
670
+ const connectTimer = setTimeout(() => controller.abort(), this.timeout);
671
+ let response;
672
+ try {
673
+ response = await fetch(url, {
674
+ method: "GET",
675
+ headers: { ...headers, Accept: "text/event-stream" },
676
+ signal: controller.signal
677
+ });
678
+ } catch (error) {
679
+ clearTimeout(connectTimer);
680
+ throw new Error(
681
+ isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
682
+ { cause: error }
683
+ );
684
+ }
685
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
686
+ if (!response.ok || !response.body || !isEventStream) {
687
+ clearTimeout(connectTimer);
688
+ const body = await response.text().catch(() => "");
689
+ throw new Error(
690
+ `SSE connection to ${url} failed: HTTP ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}. The legacy HTTP+SSE transport needs a server that serves it (@FrontMcp({ transport: { protocol: { legacy: true } } })).`
691
+ );
692
+ }
693
+ const endpoint = new Promise((resolve, reject) => {
694
+ const reader = response.body?.getReader();
695
+ if (!reader) {
696
+ reject(new Error(`SSE connection to ${url} has no body`));
697
+ return;
698
+ }
699
+ void this.readStream(controller, reader, resolve, reject);
700
+ });
701
+ try {
702
+ const data = await endpoint;
703
+ const messageUrl = new URL(data, url);
704
+ this.messageEndpoint = messageUrl.toString();
705
+ this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
706
+ } finally {
707
+ clearTimeout(connectTimer);
708
+ }
709
+ }
710
+ async readStream(controller, reader, onEndpoint, onEndpointError) {
711
+ const decoder = new TextDecoder();
712
+ let buffer = "";
713
+ let endpointSeen = false;
714
+ try {
715
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
716
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
717
+ buffer = remainder;
718
+ for (const text of completeEvents) {
719
+ const event = parseSSEEvent(text);
720
+ if (!event) continue;
721
+ if (event.type === "endpoint") {
722
+ if (!endpointSeen) {
723
+ endpointSeen = true;
724
+ onEndpoint(event.data);
725
+ }
726
+ continue;
727
+ }
728
+ await this.handleMessage(event.data);
729
+ }
730
+ }
731
+ } catch (error) {
732
+ if (!controller.signal.aborted) this.log("SSE stream failed:", error);
733
+ } finally {
734
+ reader.releaseLock();
735
+ }
736
+ if (!endpointSeen) {
737
+ onEndpointError(
738
+ new Error(
739
+ controller.signal.aborted ? `SSE stream at ${this.sseUrl()} sent no endpoint event within ${this.timeout}ms` : `SSE stream at ${this.sseUrl()} ended before the server sent its endpoint`
740
+ )
741
+ );
742
+ }
743
+ if (this.stream === controller) {
744
+ this.stream = void 0;
745
+ this.rejectPending("SSE stream closed by the server");
746
+ if (this.state === "connected") this.state = "disconnected";
747
+ }
748
+ }
749
+ async handleMessage(data) {
750
+ let message;
751
+ try {
752
+ message = JSON.parse(data);
753
+ } catch {
754
+ this.log("Ignoring SSE event that is not JSON:", data);
755
+ return;
756
+ }
757
+ if (!message || typeof message !== "object") return;
758
+ if (typeof message.method === "string") {
759
+ const request = message;
760
+ if (request.id === void 0 || request.id === null) {
761
+ this.notificationHandler?.(request);
762
+ return;
763
+ }
764
+ await this.answerServerRequest(request);
765
+ return;
766
+ }
767
+ if (message.id !== void 0 && message.id !== null) {
768
+ const pending = this.pending.get(message.id);
769
+ if (pending) {
770
+ clearTimeout(pending.timer);
771
+ this.pending.delete(message.id);
772
+ pending.resolve(message);
773
+ }
774
+ }
775
+ }
776
+ /** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
777
+ async answerServerRequest(request) {
778
+ const id = request.id;
779
+ let reply;
780
+ if (request.method === "elicitation/create") {
781
+ reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
782
+ } else if (request.method === "ping") {
783
+ reply = { jsonrpc: "2.0", id, result: {} };
784
+ } else {
785
+ reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
786
+ }
787
+ const result = await this.post(JSON.stringify(reply));
788
+ if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
789
+ }
790
+ async elicit(params) {
791
+ if (!this.elicitationHandler) return { action: "decline" };
792
+ try {
793
+ return await this.elicitationHandler(params);
794
+ } catch (error) {
795
+ this.log("Elicitation handler error:", error);
796
+ return { action: "cancel" };
797
+ }
798
+ }
799
+ closeStream(reason) {
800
+ const controller = this.stream;
801
+ this.stream = void 0;
802
+ controller?.abort();
803
+ this.rejectPending(reason);
804
+ }
805
+ rejectPending(reason) {
806
+ for (const [id, pending] of this.pending) {
807
+ clearTimeout(pending.timer);
808
+ pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
809
+ }
810
+ this.pending.clear();
811
+ }
812
+ // ═══════════════════════════════════════════════════════════════════
813
+ // POSTING
814
+ // ═══════════════════════════════════════════════════════════════════
815
+ /** POST a message and resolve with the response the stream carries for `id`. */
816
+ async postAndAwait(id, body) {
817
+ const answer = new Promise((resolve) => {
818
+ const timer = setTimeout(() => {
819
+ this.pending.delete(id);
820
+ resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
821
+ }, this.timeout);
822
+ this.pending.set(id, { resolve, timer });
823
+ });
824
+ const result = await this.post(body);
825
+ if (!result.ok) {
826
+ const pending = this.pending.get(id);
827
+ if (pending) {
828
+ clearTimeout(pending.timer);
829
+ this.pending.delete(id);
830
+ }
831
+ return {
832
+ jsonrpc: "2.0",
833
+ id,
834
+ error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
835
+ };
836
+ }
837
+ return answer;
838
+ }
839
+ async post(body) {
840
+ const endpoint = this.messageEndpoint;
841
+ if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
842
+ const headers = this.buildHeaders();
843
+ this.lastRequestHeaders = headers;
844
+ this.log(`POST ${endpoint}`, body);
845
+ const controller = new AbortController();
846
+ const timer = setTimeout(() => controller.abort(), this.timeout);
847
+ try {
848
+ const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
849
+ const text = await response.text();
850
+ return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
851
+ } catch (error) {
852
+ if (isAbortError(error)) {
853
+ return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
854
+ }
855
+ throw error;
856
+ } finally {
857
+ clearTimeout(timer);
858
+ }
859
+ }
860
+ // ═══════════════════════════════════════════════════════════════════
861
+ // HELPERS
862
+ // ═══════════════════════════════════════════════════════════════════
863
+ async requestAnonymousToken() {
864
+ const url = new URL(this.baseUrl);
865
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
866
+ url.search = "";
867
+ const tokenUrl = url.toString();
868
+ try {
869
+ const response = await fetch(tokenUrl, {
870
+ method: "POST",
871
+ headers: { "Content-Type": "application/json" },
872
+ body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
873
+ });
874
+ if (!response.ok) {
875
+ this.log(`No anonymous token (HTTP ${response.status})`);
876
+ return;
877
+ }
878
+ const token = await response.json();
879
+ if (token.access_token) this.authToken = token.access_token;
880
+ } catch (error) {
881
+ this.log("Error requesting anonymous token:", error);
882
+ }
883
+ }
884
+ /** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
885
+ sseUrl() {
886
+ const url = new URL(this.baseUrl);
887
+ const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
888
+ const basePath = url.pathname.replace(/\/+$/, "");
889
+ url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
890
+ return url.toString();
891
+ }
892
+ buildHeaders() {
893
+ const headers = {
894
+ "Content-Type": "application/json",
895
+ Accept: "application/json, text/event-stream"
896
+ };
897
+ if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
898
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
899
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
900
+ Object.assign(headers, this.authHeaders);
901
+ return headers;
902
+ }
903
+ ensureConnected() {
904
+ if (this.state !== "connected") {
905
+ throw new Error("Transport not connected. Call connect() first.");
906
+ }
907
+ }
908
+ log(message, data) {
909
+ if (this.debug) {
910
+ console.log(`[SSE] ${message}`, data ?? "");
911
+ }
912
+ }
913
+ };
914
+ function splitSSEEvents(buffer) {
915
+ const endsWithCarriageReturn = buffer.endsWith("\r");
916
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
917
+ const completeEvents = normalized.split("\n\n");
918
+ const remainder = completeEvents.pop() ?? "";
919
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
920
+ }
921
+ function parseSSEEvent(text) {
922
+ let type = "message";
923
+ const data = [];
924
+ for (const line of text.split("\n")) {
925
+ if (line.startsWith(":")) continue;
926
+ const colon = line.indexOf(":");
927
+ const field = colon === -1 ? line : line.slice(0, colon);
928
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
929
+ if (field === "event") type = value;
930
+ else if (field === "data") data.push(value);
931
+ }
932
+ return data.length > 0 ? { type, data: data.join("\n") } : void 0;
933
+ }
934
+
935
+ // libs/testing/src/transport/streamable-http.transport.ts
936
+ var DEFAULT_TIMEOUT2 = 3e4;
465
937
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
466
938
  var StreamableHttpTransport = class {
467
939
  config;
@@ -483,7 +955,7 @@ var StreamableHttpTransport = class {
483
955
  baseUrl: config.baseUrl.replace(/\/$/, ""),
484
956
  // Remove trailing slash
485
957
  entryPath: config.entryPath,
486
- timeout: config.timeout ?? DEFAULT_TIMEOUT,
958
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
487
959
  auth: config.auth ?? {},
488
960
  publicMode: config.publicMode ?? false,
489
961
  debug: config.debug ?? false,
@@ -568,7 +1040,7 @@ var StreamableHttpTransport = class {
568
1040
  return mockResponse2;
569
1041
  }
570
1042
  case "error":
571
- throw interceptResult.error;
1043
+ throw markInterceptedError(interceptResult.error);
572
1044
  case "continue":
573
1045
  message = interceptResult.request;
574
1046
  break;
@@ -629,7 +1101,7 @@ var StreamableHttpTransport = class {
629
1101
  return jsonResponse;
630
1102
  } catch (error) {
631
1103
  clearTimeout(timeoutId);
632
- if (error instanceof Error && error.name === "AbortError") {
1104
+ if (isAbortError(error)) {
633
1105
  return {
634
1106
  jsonrpc: "2.0",
635
1107
  id: message.id ?? null,
@@ -668,7 +1140,7 @@ var StreamableHttpTransport = class {
668
1140
  }
669
1141
  } catch (error) {
670
1142
  clearTimeout(timeoutId);
671
- if (error instanceof Error && error.name !== "AbortError") {
1143
+ if (!isAbortError(error)) {
672
1144
  throw error;
673
1145
  }
674
1146
  }
@@ -709,7 +1181,7 @@ var StreamableHttpTransport = class {
709
1181
  error: {
710
1182
  code: -32700,
711
1183
  message: "Parse error",
712
- data: error instanceof Error ? error.message : "Unknown error"
1184
+ data: errorMessage(error)
713
1185
  }
714
1186
  };
715
1187
  }
@@ -823,7 +1295,7 @@ var StreamableHttpTransport = class {
823
1295
  let buffer = "";
824
1296
  try {
825
1297
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
826
- const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1298
+ const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
827
1299
  buffer = remainder;
828
1300
  for (const eventText of completeEvents) {
829
1301
  for (const event of this.parseSSEEvents(eventText, void 0).events) {
@@ -1079,7 +1551,7 @@ var StreamableHttpTransport = class {
1079
1551
  if (this.config.clientInfo) {
1080
1552
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1081
1553
  }
1082
- if (this.authToken && !this.publicMode) {
1554
+ if (this.authToken) {
1083
1555
  headers["Authorization"] = `Bearer ${this.authToken}`;
1084
1556
  }
1085
1557
  if (this.sessionId) {
@@ -1157,7 +1629,7 @@ var StreamableHttpTransport = class {
1157
1629
  };
1158
1630
  }
1159
1631
  };
1160
- function splitSSEEvents(buffer) {
1632
+ function splitSSEEvents2(buffer) {
1161
1633
  const endsWithCarriageReturn = buffer.endsWith("\r");
1162
1634
  const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1163
1635
  const completeEvents = normalized.split("\n\n");
@@ -1414,7 +1886,7 @@ var McpTestClientBuilder = class {
1414
1886
  };
1415
1887
 
1416
1888
  // libs/testing/src/client/mcp-test-client.ts
1417
- var DEFAULT_TIMEOUT2 = 3e4;
1889
+ var DEFAULT_TIMEOUT3 = 3e4;
1418
1890
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
1419
1891
  var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
1420
1892
  var DEFAULT_CLIENT_INFO = {
@@ -1422,6 +1894,15 @@ var DEFAULT_CLIENT_INFO = {
1422
1894
  version: "0.4.0"
1423
1895
  };
1424
1896
  var MAX_LIST_PAGES = 1e3;
1897
+ function isAuthFailure(error) {
1898
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
1899
+ }
1900
+ function describeInitializeError(error) {
1901
+ const message = error?.message ?? "Unknown error";
1902
+ const data = error?.data;
1903
+ const detail = typeof data === "string" ? data.trim() : "";
1904
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
1905
+ }
1425
1906
  function extractEntryPaths(data) {
1426
1907
  let payload = data;
1427
1908
  if (typeof payload === "string") {
@@ -1475,7 +1956,7 @@ var McpTestClient = class {
1475
1956
  transport: config.transport ?? "streamable-http",
1476
1957
  auth: config.auth ?? {},
1477
1958
  publicMode: config.publicMode ?? false,
1478
- timeout: config.timeout ?? DEFAULT_TIMEOUT2,
1959
+ timeout: config.timeout ?? DEFAULT_TIMEOUT3,
1479
1960
  debug: config.debug ?? false,
1480
1961
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
1481
1962
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
@@ -1508,19 +1989,22 @@ var McpTestClient = class {
1508
1989
  async connect() {
1509
1990
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
1510
1991
  this.triedDiscoveredEntryPath = false;
1992
+ this.initResult = null;
1511
1993
  this.transport = this.createTransport();
1512
1994
  await this.transport.connect();
1513
1995
  const maxRetries = 3;
1514
1996
  const retryDelayMs = 500;
1515
1997
  let lastError;
1516
1998
  let reportedEntryPaths;
1999
+ let attemptsMade = 0;
1517
2000
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2001
+ attemptsMade = attempt;
1518
2002
  const initResponse = await this.initialize();
1519
2003
  if (initResponse.success && initResponse.data) {
1520
2004
  this.initResult = initResponse.data;
1521
2005
  break;
1522
2006
  }
1523
- lastError = initResponse.error?.message ?? "Unknown error";
2007
+ lastError = describeInitializeError(initResponse.error);
1524
2008
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
1525
2009
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
1526
2010
  this.triedDiscoveredEntryPath = true;
@@ -1532,6 +2016,7 @@ var McpTestClient = class {
1532
2016
  await this.transport.connect();
1533
2017
  continue;
1534
2018
  }
2019
+ if (isAuthFailure(initResponse.error)) break;
1535
2020
  if (attempt < maxRetries) {
1536
2021
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
1537
2022
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -1540,7 +2025,7 @@ var McpTestClient = class {
1540
2025
  if (!this.initResult) {
1541
2026
  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.` : "";
1542
2027
  throw new Error(
1543
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2028
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
1544
2029
  );
1545
2030
  }
1546
2031
  this._sessionId = this.transport.getSessionId();
@@ -1610,18 +2095,47 @@ var McpTestClient = class {
1610
2095
  return this._authState;
1611
2096
  }
1612
2097
  /**
1613
- * Authenticate with a token
2098
+ * Authenticate with a token.
2099
+ *
2100
+ * When connected, this opens a NEW session with the token (the previous session was created
2101
+ * for the previous identity) and rejects if the server refuses it — for example because the
2102
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
2103
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
1614
2104
  */
1615
2105
  async authenticate(token) {
2106
+ if (typeof token !== "string" || token.trim() === "") {
2107
+ throw new Error("authenticate() requires a non-empty token");
2108
+ }
2109
+ const previousAuthState = this._authState;
2110
+ const previousConfigAuth = this.config.auth;
2111
+ this.config.auth = { ...previousConfigAuth, token };
1616
2112
  this._authState = {
1617
2113
  isAnonymous: false,
1618
2114
  token,
1619
2115
  scopes: this.parseScopesFromToken(token),
1620
2116
  user: this.parseUserFromToken(token)
1621
2117
  };
1622
- if (this.transport) {
1623
- this.transport.setAuthToken(token);
2118
+ const previousTransport = this.transport;
2119
+ if (!previousTransport) {
2120
+ this.log("debug", "Authentication updated (not connected yet)");
2121
+ return;
2122
+ }
2123
+ const previousInit = this.initResult;
2124
+ const previousSessionId = this._sessionId;
2125
+ const previousSessionInfo = this._sessionInfo;
2126
+ try {
2127
+ await this.connect();
2128
+ } catch (err) {
2129
+ await this.transport?.close().catch(() => void 0);
2130
+ this.transport = previousTransport;
2131
+ this.initResult = previousInit;
2132
+ this._sessionId = previousSessionId;
2133
+ this._sessionInfo = previousSessionInfo;
2134
+ this.config.auth = previousConfigAuth;
2135
+ this._authState = previousAuthState;
2136
+ throw err;
1624
2137
  }
2138
+ await previousTransport.close().catch(() => void 0);
1625
2139
  this.log("debug", "Authentication updated");
1626
2140
  }
1627
2141
  // ═══════════════════════════════════════════════════════════════════
@@ -2208,7 +2722,18 @@ var McpTestClient = class {
2208
2722
  notificationHandler: this.recordNotification
2209
2723
  });
2210
2724
  case "sse":
2211
- throw new Error("SSE transport not yet implemented");
2725
+ return new SseTransport({
2726
+ baseUrl,
2727
+ entryPath: this.config.entryPath,
2728
+ timeout: this.config.timeout,
2729
+ auth: this.config.auth,
2730
+ publicMode: this.config.publicMode,
2731
+ debug: this.config.debug,
2732
+ interceptors: this._interceptors,
2733
+ clientInfo: this.config.clientInfo,
2734
+ elicitationHandler: this._elicitationHandler,
2735
+ notificationHandler: this.recordNotification
2736
+ });
2212
2737
  default:
2213
2738
  throw new Error(`Unknown transport type: ${this.config.transport}`);
2214
2739
  }
@@ -2245,10 +2770,11 @@ var McpTestClient = class {
2245
2770
  requestId: id
2246
2771
  };
2247
2772
  } catch (err) {
2773
+ if (isInterceptedError(err)) throw err;
2248
2774
  const durationMs = Date.now() - start;
2249
2775
  const error = {
2250
2776
  code: -32603,
2251
- message: err instanceof Error ? err.message : "Unknown error"
2777
+ message: errorMessage(err)
2252
2778
  };
2253
2779
  return {
2254
2780
  success: false,
@@ -2396,12 +2922,12 @@ var McpTestClient = class {
2396
2922
  const payload = this.decodeJwtPayload(token);
2397
2923
  if (!payload) return [];
2398
2924
  const scope = payload["scope"];
2399
- const scopes = payload["scopes"];
2925
+ const scopes2 = payload["scopes"];
2400
2926
  if (typeof scope === "string") {
2401
2927
  return scope.split(" ");
2402
2928
  }
2403
- if (Array.isArray(scopes)) {
2404
- return scopes;
2929
+ if (Array.isArray(scopes2)) {
2930
+ return scopes2;
2405
2931
  }
2406
2932
  return [];
2407
2933
  } catch {
@@ -2533,14 +3059,15 @@ var TestTokenFactory = class {
2533
3059
  */
2534
3060
  async createTestToken(options) {
2535
3061
  await this.ensureKeys();
2536
- const now = Math.floor(Date.now() / 1e3);
3062
+ const nowMs = Date.now();
3063
+ const iat = Math.floor(nowMs / 1e3);
2537
3064
  const exp = options.exp ?? 3600;
2538
3065
  const payload = {
2539
3066
  iss: options.iss ?? this.issuer,
2540
3067
  sub: options.sub,
2541
3068
  aud: options.aud ?? this.audience,
2542
- iat: now,
2543
- exp: now + exp,
3069
+ iat,
3070
+ exp: Math.ceil(nowMs / 1e3) + exp,
2544
3071
  scope: options.scopes?.join(" "),
2545
3072
  ...options.claims
2546
3073
  };
@@ -2565,10 +3092,10 @@ var TestTokenFactory = class {
2565
3092
  /**
2566
3093
  * Create a regular user token
2567
3094
  */
2568
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
3095
+ async createUserToken(sub = "user-001", scopes2 = ["read", "write"]) {
2569
3096
  return this.createTestToken({
2570
3097
  sub,
2571
- scopes,
3098
+ scopes: scopes2,
2572
3099
  claims: {
2573
3100
  email: "user@test.local",
2574
3101
  name: "Test User",
@@ -2579,10 +3106,11 @@ var TestTokenFactory = class {
2579
3106
  /**
2580
3107
  * Create an anonymous user token
2581
3108
  */
2582
- async createAnonymousToken() {
3109
+ async createAnonymousToken(expiresIn) {
2583
3110
  return this.createTestToken({
2584
3111
  sub: `anon:${Date.now()}`,
2585
3112
  scopes: ["anonymous"],
3113
+ exp: expiresIn,
2586
3114
  claims: {
2587
3115
  name: "Anonymous",
2588
3116
  role: "anonymous"
@@ -2793,17 +3321,7 @@ function createTestUser(overrides) {
2793
3321
 
2794
3322
  // libs/testing/src/auth/mock-oauth-server.ts
2795
3323
  var import_http = require("http");
2796
- var _randomBytes;
2797
- var _sha256Base64url;
2798
- var _base64urlEncode;
2799
- async function loadCryptoUtils() {
2800
- if (!_randomBytes) {
2801
- const utils = await import("@frontmcp/utils");
2802
- _randomBytes = utils.randomBytes;
2803
- _sha256Base64url = utils.sha256Base64url;
2804
- _base64urlEncode = utils.base64urlEncode;
2805
- }
2806
- }
3324
+ var import_utils = require("@frontmcp/utils");
2807
3325
  var MockOAuthServer = class {
2808
3326
  tokenFactory;
2809
3327
  options;
@@ -2818,8 +3336,8 @@ var MockOAuthServer = class {
2818
3336
  accessTokenTtlSeconds;
2819
3337
  /** Refresh token TTL in seconds */
2820
3338
  refreshTokenTtlSeconds;
2821
- constructor(tokenFactory3, options = {}) {
2822
- this.tokenFactory = tokenFactory3;
3339
+ constructor(tokenFactory2, options = {}) {
3340
+ this.tokenFactory = tokenFactory2;
2823
3341
  this.options = options;
2824
3342
  this.accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? 3600;
2825
3343
  this.refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? 30 * 24 * 3600;
@@ -3076,14 +3594,14 @@ ${error.stack}` : String(error);
3076
3594
  return;
3077
3595
  }
3078
3596
  const code = this.generateCode();
3079
- const scopes = scope.split(" ").filter(Boolean);
3597
+ const scopes2 = scope.split(" ").filter(Boolean);
3080
3598
  this.authCodes.set(code, {
3081
3599
  code,
3082
3600
  clientId,
3083
3601
  redirectUri,
3084
3602
  codeChallenge,
3085
3603
  codeChallengeMethod,
3086
- scopes,
3604
+ scopes: scopes2,
3087
3605
  user: testUser,
3088
3606
  state,
3089
3607
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3123,7 +3641,7 @@ ${error.stack}` : String(error);
3123
3641
  }
3124
3642
  const grantType = params.get("grant_type");
3125
3643
  if (grantType === "anonymous") {
3126
- const token = await this.tokenFactory.createAnonymousToken();
3644
+ const token = await this.tokenFactory.createAnonymousToken(this.accessTokenTtlSeconds);
3127
3645
  res.writeHead(200, { "Content-Type": "application/json" });
3128
3646
  res.end(
3129
3647
  JSON.stringify({
@@ -3255,6 +3773,8 @@ ${error.stack}` : String(error);
3255
3773
  codeRecord.used = true;
3256
3774
  const accessToken = await this.tokenFactory.createTestToken({
3257
3775
  sub: codeRecord.user.sub,
3776
+ scopes: codeRecord.scopes,
3777
+ exp: this.accessTokenTtlSeconds,
3258
3778
  claims: {
3259
3779
  email: codeRecord.user.email,
3260
3780
  name: codeRecord.user.name,
@@ -3263,6 +3783,7 @@ ${error.stack}` : String(error);
3263
3783
  });
3264
3784
  const idToken = await this.tokenFactory.createTestToken({
3265
3785
  sub: codeRecord.user.sub,
3786
+ exp: this.accessTokenTtlSeconds,
3266
3787
  claims: {
3267
3788
  email: codeRecord.user.email,
3268
3789
  name: codeRecord.user.name,
@@ -3340,6 +3861,8 @@ ${error.stack}` : String(error);
3340
3861
  }
3341
3862
  const accessToken = await this.tokenFactory.createTestToken({
3342
3863
  sub: tokenRecord.user.sub,
3864
+ scopes: tokenRecord.scopes,
3865
+ exp: this.accessTokenTtlSeconds,
3343
3866
  claims: {
3344
3867
  email: tokenRecord.user.email,
3345
3868
  name: tokenRecord.user.name,
@@ -3444,14 +3967,14 @@ ${error.stack}` : String(error);
3444
3967
  name
3445
3968
  };
3446
3969
  const code = this.generateCode();
3447
- const scopes = scope.split(" ").filter(Boolean);
3970
+ const scopes2 = scope.split(" ").filter(Boolean);
3448
3971
  this.authCodes.set(code, {
3449
3972
  code,
3450
3973
  clientId,
3451
3974
  redirectUri,
3452
3975
  codeChallenge,
3453
3976
  codeChallengeMethod,
3454
- scopes,
3977
+ scopes: scopes2,
3455
3978
  user: testUser,
3456
3979
  state,
3457
3980
  expiresAt: Date.now() + 5 * 60 * 1e3,
@@ -3557,8 +4080,7 @@ ${error.stack}` : String(error);
3557
4080
  * Generate a random authorization code
3558
4081
  */
3559
4082
  async generateCodeAsync() {
3560
- await loadCryptoUtils();
3561
- return _base64urlEncode(_randomBytes(32));
4083
+ return (0, import_utils.base64urlEncode)((0, import_utils.randomBytes)(32));
3562
4084
  }
3563
4085
  /**
3564
4086
  * Generate a random authorization code (sync wrapper for compatibility)
@@ -3574,8 +4096,7 @@ ${error.stack}` : String(error);
3574
4096
  */
3575
4097
  async computeCodeChallengeAsync(verifier, method) {
3576
4098
  if (method === "S256") {
3577
- await loadCryptoUtils();
3578
- return _sha256Base64url(verifier);
4099
+ return (0, import_utils.sha256Base64url)(verifier);
3579
4100
  }
3580
4101
  return verifier;
3581
4102
  }
@@ -4163,7 +4684,7 @@ var MockCimdServer = class {
4163
4684
 
4164
4685
  // libs/testing/src/server/test-server.ts
4165
4686
  var import_child_process = require("child_process");
4166
- var import_utils = require("@frontmcp/utils");
4687
+ var import_utils2 = require("@frontmcp/utils");
4167
4688
 
4168
4689
  // libs/testing/src/errors/index.ts
4169
4690
  var TestClientError = class extends Error {
@@ -4269,6 +4790,8 @@ var E2E_PORT_RANGES = {
4269
4790
  "demo-e2e-distributed": { start: 50440, size: 20 },
4270
4791
  // Protocol revision E2E tests (50460-50479)
4271
4792
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
4793
+ // Testing library E2E tests (50480-50499)
4794
+ "demo-e2e-testing": { start: 50480, size: 20 },
4272
4795
  // Mock servers and utilities (50900-50999)
4273
4796
  "mock-oauth": { start: 50900, size: 10 },
4274
4797
  "mock-api": { start: 50910, size: 10 },
@@ -4277,6 +4800,66 @@ var E2E_PORT_RANGES = {
4277
4800
  default: { start: 51e3, size: 100 }
4278
4801
  };
4279
4802
  var reservedPorts = /* @__PURE__ */ new Map();
4803
+ var lockedPorts = /* @__PURE__ */ new Set();
4804
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
4805
+ function lockDir() {
4806
+ try {
4807
+ const os = require("node:os");
4808
+ const path = require("node:path");
4809
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
4810
+ } catch {
4811
+ return void 0;
4812
+ }
4813
+ }
4814
+ function pidIsAlive(pid) {
4815
+ try {
4816
+ process.kill(pid, 0);
4817
+ return true;
4818
+ } catch (error) {
4819
+ return error.code === "EPERM";
4820
+ }
4821
+ }
4822
+ function acquirePortLock(port) {
4823
+ if (lockedPorts.has(port)) return false;
4824
+ let fs;
4825
+ let file;
4826
+ try {
4827
+ fs = require("node:fs");
4828
+ const dir = lockDir();
4829
+ if (!dir) return true;
4830
+ fs.mkdirSync(dir, { recursive: true });
4831
+ file = require("node:path").join(dir, `${port}.lock`);
4832
+ } catch {
4833
+ return true;
4834
+ }
4835
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
4836
+ for (let attempt = 0; attempt < 2; attempt++) {
4837
+ try {
4838
+ fs.writeFileSync(file, payload, { flag: "wx" });
4839
+ lockedPorts.add(port);
4840
+ return true;
4841
+ } catch (error) {
4842
+ if (error.code !== "EEXIST") return true;
4843
+ try {
4844
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
4845
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
4846
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
4847
+ fs.unlinkSync(file);
4848
+ } catch {
4849
+ }
4850
+ }
4851
+ }
4852
+ return false;
4853
+ }
4854
+ function releasePortLock(port) {
4855
+ if (!lockedPorts.delete(port)) return;
4856
+ try {
4857
+ const fs = require("node:fs");
4858
+ const dir = lockDir();
4859
+ if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
4860
+ } catch {
4861
+ }
4862
+ }
4280
4863
  var projectPortIndex = /* @__PURE__ */ new Map();
4281
4864
  function getPortRange(project) {
4282
4865
  const key = project;
@@ -4287,15 +4870,22 @@ function getPortRange(project) {
4287
4870
  }
4288
4871
  async function reservePort(project, preferredPort) {
4289
4872
  const range = getPortRange(project);
4290
- if (preferredPort !== void 0) {
4873
+ const handle = (port) => ({
4874
+ port,
4875
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
4876
+ releaseSocket: async () => {
4877
+ await releasePort(port);
4878
+ },
4879
+ release: async () => {
4880
+ await releasePort(port);
4881
+ releasePortLock(port);
4882
+ }
4883
+ });
4884
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
4885
+ if (wantsSpecificPort) {
4291
4886
  const reservation = await tryReservePort(preferredPort, project);
4292
4887
  if (reservation) {
4293
- return {
4294
- port: preferredPort,
4295
- release: async () => {
4296
- await releasePort(preferredPort);
4297
- }
4298
- };
4888
+ return handle(preferredPort);
4299
4889
  }
4300
4890
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
4301
4891
  }
@@ -4304,30 +4894,19 @@ async function reservePort(project, preferredPort) {
4304
4894
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
4305
4895
  const port = range.start + index % range.size;
4306
4896
  index = (index + 1) % range.size;
4307
- if (reservedPorts.has(port)) {
4897
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
4308
4898
  continue;
4309
4899
  }
4310
4900
  const reservation = await tryReservePort(port, project);
4311
4901
  if (reservation) {
4312
4902
  projectPortIndex.set(project, index);
4313
- return {
4314
- port,
4315
- release: async () => {
4316
- await releasePort(port);
4317
- }
4318
- };
4903
+ return handle(port);
4319
4904
  }
4320
4905
  }
4321
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
4322
- if (dynamicPort) {
4323
- const reservation = await tryReservePort(dynamicPort, project);
4324
- if (reservation) {
4325
- return {
4326
- port: dynamicPort,
4327
- release: async () => {
4328
- await releasePort(dynamicPort);
4329
- }
4330
- };
4906
+ for (let port = 51e3; port < 52e3; port++) {
4907
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
4908
+ if (await tryReservePort(port, project)) {
4909
+ return handle(port);
4331
4910
  }
4332
4911
  }
4333
4912
  throw new Error(
@@ -4335,9 +4914,13 @@ async function reservePort(project, preferredPort) {
4335
4914
  );
4336
4915
  }
4337
4916
  async function tryReservePort(port, project) {
4917
+ if (!acquirePortLock(port)) {
4918
+ return false;
4919
+ }
4338
4920
  return new Promise((resolve) => {
4339
4921
  const server = (0, import_net.createServer)();
4340
4922
  server.once("error", () => {
4923
+ releasePortLock(port);
4341
4924
  resolve(false);
4342
4925
  });
4343
4926
  server.listen(port, () => {
@@ -4363,31 +4946,6 @@ async function releasePort(port) {
4363
4946
  });
4364
4947
  });
4365
4948
  }
4366
- async function findAvailablePortInRange(start, end) {
4367
- for (let port = start; port < end; port++) {
4368
- if (reservedPorts.has(port)) {
4369
- continue;
4370
- }
4371
- const available = await isPortAvailable(port);
4372
- if (available) {
4373
- return port;
4374
- }
4375
- }
4376
- return null;
4377
- }
4378
- async function isPortAvailable(port) {
4379
- return new Promise((resolve) => {
4380
- const server = (0, import_net.createServer)();
4381
- server.once("error", () => {
4382
- resolve(false);
4383
- });
4384
- server.listen(port, () => {
4385
- server.close(() => {
4386
- resolve(true);
4387
- });
4388
- });
4389
- });
4390
- }
4391
4949
 
4392
4950
  // libs/testing/src/server/test-server.ts
4393
4951
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -4396,8 +4954,11 @@ var TestServer = class _TestServer {
4396
4954
  options;
4397
4955
  _info;
4398
4956
  logs = [];
4957
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
4399
4958
  portRelease = null;
4400
- constructor(options, port, portRelease) {
4959
+ /** Frees only the reservation socket, just before the server is spawned */
4960
+ portReleaseSocket = null;
4961
+ constructor(options, port, portRelease, portReleaseSocket) {
4401
4962
  this.options = {
4402
4963
  port,
4403
4964
  project: options.project,
@@ -4409,6 +4970,7 @@ var TestServer = class _TestServer {
4409
4970
  debug: options.debug ?? DEBUG_SERVER
4410
4971
  };
4411
4972
  this.portRelease = portRelease ?? null;
4973
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
4412
4974
  this._info = {
4413
4975
  baseUrl: `http://localhost:${port}`,
4414
4976
  port
@@ -4421,8 +4983,8 @@ var TestServer = class _TestServer {
4421
4983
  const project = options.project ?? "default";
4422
4984
  const maxAttempts = 3;
4423
4985
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4424
- const { port, release } = await reservePort(project, options.port);
4425
- const server = new _TestServer(options, port, release);
4986
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4987
+ const server = new _TestServer(options, port, release, releaseSocket);
4426
4988
  try {
4427
4989
  await server.startProcess();
4428
4990
  return server;
@@ -4462,7 +5024,7 @@ var TestServer = class _TestServer {
4462
5024
  }
4463
5025
  const maxAttempts = 3;
4464
5026
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
4465
- const { port, release } = await reservePort(project, options.port);
5027
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4466
5028
  const serverOptions = {
4467
5029
  ...options,
4468
5030
  port,
@@ -4470,7 +5032,7 @@ var TestServer = class _TestServer {
4470
5032
  command: `npx nx serve ${project} --port ${port}`,
4471
5033
  cwd: options.cwd ?? process.cwd()
4472
5034
  };
4473
- const server = new _TestServer(serverOptions, port, release);
5035
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
4474
5036
  try {
4475
5037
  await server.startProcess();
4476
5038
  return server;
@@ -4528,10 +5090,17 @@ var TestServer = class _TestServer {
4528
5090
  * Stop the test server
4529
5091
  */
4530
5092
  async stop() {
4531
- if (this.portRelease) {
4532
- await this.portRelease();
4533
- this.portRelease = null;
5093
+ try {
5094
+ await this.stopProcess();
5095
+ } finally {
5096
+ if (this.portRelease) {
5097
+ await this.portRelease();
5098
+ this.portRelease = null;
5099
+ }
5100
+ this.portReleaseSocket = null;
4534
5101
  }
5102
+ }
5103
+ async stopProcess() {
4535
5104
  if (this.process) {
4536
5105
  this.log("Stopping server...");
4537
5106
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -4604,7 +5173,7 @@ var TestServer = class _TestServer {
4604
5173
  * Restart the server
4605
5174
  */
4606
5175
  async restart() {
4607
- await this.stop();
5176
+ await this.stopProcess();
4608
5177
  await this.startProcess();
4609
5178
  }
4610
5179
  /**
@@ -4634,9 +5203,9 @@ var TestServer = class _TestServer {
4634
5203
  PORT: String(this.options.port)
4635
5204
  };
4636
5205
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
4637
- if (this.portRelease) {
4638
- await this.portRelease();
4639
- this.portRelease = null;
5206
+ if (this.portReleaseSocket) {
5207
+ await this.portReleaseSocket();
5208
+ this.portReleaseSocket = null;
4640
5209
  await sleep2(300);
4641
5210
  }
4642
5211
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -4646,9 +5215,8 @@ var TestServer = class _TestServer {
4646
5215
  detached: true,
4647
5216
  stdio: ["pipe", "pipe", "pipe"]
4648
5217
  });
4649
- if (this.process.pid !== void 0) {
4650
- this._info.pid = this.process.pid;
4651
- }
5218
+ this._info.pid = this.process.pid;
5219
+ this._info.shellPid = this.process.pid;
4652
5220
  let processExited = false;
4653
5221
  let exitCode = null;
4654
5222
  let exitError = null;
@@ -4751,6 +5319,8 @@ ${allLogs}`);
4751
5319
  });
4752
5320
  if (response.ok || response.status === 404) {
4753
5321
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
5322
+ const listeningPid = await findListeningPid(this.options.port);
5323
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
4754
5324
  return;
4755
5325
  }
4756
5326
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -4795,6 +5365,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
4795
5365
  function sleep2(ms) {
4796
5366
  return new Promise((resolve) => setTimeout(resolve, ms));
4797
5367
  }
5368
+ function findListeningPid(port) {
5369
+ if (process.platform === "win32") return Promise.resolve(void 0);
5370
+ return new Promise((resolve) => {
5371
+ (0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
5372
+ if (error) {
5373
+ resolve(void 0);
5374
+ return;
5375
+ }
5376
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
5377
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
5378
+ });
5379
+ });
5380
+ }
4798
5381
  function withWorkspaceProtocolFallback(env, cwd) {
4799
5382
  if (findInstalledProtocolPackageDir(cwd)) {
4800
5383
  return env;
@@ -4844,7 +5427,7 @@ function withProtocolNodePathAlias(env, cwd, workspacePackageDir) {
4844
5427
  const fs = require("node:fs");
4845
5428
  const os = require("node:os");
4846
5429
  const path = require("node:path");
4847
- const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils.sha256Hex)(cwd).slice(0, 12));
5430
+ const aliasRoot = path.join(os.tmpdir(), "frontmcp-test-node-path", (0, import_utils2.sha256Hex)(cwd).slice(0, 12));
4848
5431
  const scopeDir = path.join(aliasRoot, "@frontmcp");
4849
5432
  const aliasPackageDir = path.join(scopeDir, "protocol");
4850
5433
  fs.mkdirSync(scopeDir, { recursive: true });
@@ -5066,94 +5649,173 @@ function hasMimeType(result, mimeType) {
5066
5649
  return result.hasMimeType(mimeType);
5067
5650
  }
5068
5651
 
5069
- // libs/testing/src/fixtures/test-fixture.ts
5070
- var import_globals = require("@jest/globals");
5071
-
5072
5652
  // libs/testing/src/fixtures/gateway-token-binding.ts
5073
- var import_utils2 = require("@frontmcp/utils");
5653
+ var import_utils3 = require("@frontmcp/utils");
5074
5654
  function gatewayTokenBinding(serverUrl, entryPath) {
5075
- const base = (0, import_utils2.trimTrailing)(serverUrl, "/");
5076
- const prefix = (0, import_utils2.trimSlashes)(entryPath ?? "");
5655
+ const base = (0, import_utils3.trimTrailing)(serverUrl, "/");
5656
+ const prefix = (0, import_utils3.trimSlashes)(entryPath ?? "");
5077
5657
  const mcpUrl = prefix ? `${base}/${prefix}` : base;
5078
5658
  return { issuer: mcpUrl, audience: mcpUrl };
5079
5659
  }
5080
5660
 
5081
5661
  // libs/testing/src/fixtures/test-fixture.ts
5082
- var describe2 = import_globals.describe;
5083
- var beforeAll2 = import_globals.beforeAll;
5084
- var beforeEach2 = import_globals.beforeEach;
5085
- var afterEach2 = import_globals.afterEach;
5086
- var afterAll2 = import_globals.afterAll;
5087
- var it2 = import_globals.it;
5088
- var currentConfig = {};
5089
- var serverInstance = null;
5090
- var tokenFactory = null;
5091
- var serverStartedByUs = false;
5092
- var skipScopes = [{ skipped: false }];
5093
- function currentSkipScope() {
5094
- return skipScopes[skipScopes.length - 1];
5662
+ var jestApi;
5663
+ function jestGlobals() {
5664
+ if (jestApi) return jestApi;
5665
+ try {
5666
+ const api = require("@jest/globals");
5667
+ jestApi = {
5668
+ describe: api["describe"],
5669
+ beforeAll: api["beforeAll"],
5670
+ beforeEach: api["beforeEach"],
5671
+ afterEach: api["afterEach"],
5672
+ afterAll: api["afterAll"],
5673
+ it: api["it"]
5674
+ };
5675
+ return jestApi;
5676
+ } catch (error) {
5677
+ throw new Error(
5678
+ `@frontmcp/testing fixtures ("test", "test.use", ...) can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
5679
+ { cause: error }
5680
+ );
5681
+ }
5095
5682
  }
5096
- async function initializeSharedResources() {
5097
- if (!serverInstance) {
5098
- if (currentConfig.baseUrl && !currentConfig.server) {
5099
- serverInstance = TestServer.connect(currentConfig.baseUrl);
5100
- serverStartedByUs = false;
5101
- } else if (currentConfig.server) {
5102
- const serverCommand = resolveServerCommand(currentConfig.server);
5103
- const isDebug = currentConfig.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5683
+ function newScope(parent) {
5684
+ return {
5685
+ skipped: parent?.skipped ?? false,
5686
+ reason: parent?.reason,
5687
+ config: {},
5688
+ envs: [],
5689
+ configured: false,
5690
+ cleanupRegistered: false,
5691
+ beforeEachHooks: [],
5692
+ afterEachHooks: []
5693
+ };
5694
+ }
5695
+ var scopes = [newScope()];
5696
+ function currentScope() {
5697
+ return scopes[scopes.length - 1];
5698
+ }
5699
+ var serverEntries = /* @__PURE__ */ new Map();
5700
+ function resolveConfig(chain) {
5701
+ const merged = {};
5702
+ for (const scope of chain) {
5703
+ Object.assign(merged, scope.config);
5704
+ for (const env of scope.envs) merged.env = { ...merged.env, ...env };
5705
+ }
5706
+ return merged;
5707
+ }
5708
+ function ownerScopeOf(chain) {
5709
+ for (let i = chain.length - 1; i > 0; i--) {
5710
+ if (chain[i].configured) return chain[i];
5711
+ }
5712
+ return chain[0];
5713
+ }
5714
+ function serverKey(config) {
5715
+ const env = Object.fromEntries(Object.entries(config.env ?? {}).sort(([a], [b]) => a.localeCompare(b)));
5716
+ return JSON.stringify({
5717
+ server: config.server,
5718
+ baseUrl: config.baseUrl,
5719
+ project: config.project,
5720
+ port: config.port,
5721
+ entryPath: config.entryPath,
5722
+ startupTimeout: config.startupTimeout,
5723
+ logLevel: config.logLevel,
5724
+ auth: config.auth,
5725
+ env
5726
+ });
5727
+ }
5728
+ function serverEnv(config) {
5729
+ const extra = {};
5730
+ if (config.auth?.mode) extra["FRONTMCP_TEST_AUTH_MODE"] = config.auth.mode;
5731
+ if (config.auth?.type) extra["FRONTMCP_TEST_AUTH_TYPE"] = config.auth.type;
5732
+ if (Object.keys(extra).length === 0 && !config.env) return void 0;
5733
+ return { ...extra, ...config.env };
5734
+ }
5735
+ async function startServerEntry(config, owner) {
5736
+ let server;
5737
+ let startedByUs = false;
5738
+ if (config.baseUrl && !config.server) {
5739
+ server = TestServer.connect(config.baseUrl);
5740
+ } else if (config.server) {
5741
+ const serverCommand = resolveServerCommand(config.server);
5742
+ const isDebug = config.logLevel === "debug" || process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
5743
+ if (isDebug) {
5744
+ console.log(`[TestFixture] Starting server: ${serverCommand}`);
5745
+ }
5746
+ try {
5747
+ server = await TestServer.start({
5748
+ project: config.project,
5749
+ port: config.port,
5750
+ command: serverCommand,
5751
+ env: serverEnv(config),
5752
+ startupTimeout: config.startupTimeout ?? 3e4,
5753
+ debug: isDebug
5754
+ });
5755
+ startedByUs = true;
5104
5756
  if (isDebug) {
5105
- console.log(`[TestFixture] Starting server: ${serverCommand}`);
5757
+ console.log(`[TestFixture] Server started at ${server.info.baseUrl}`);
5106
5758
  }
5107
- try {
5108
- serverInstance = await TestServer.start({
5109
- project: currentConfig.project,
5110
- port: currentConfig.port,
5111
- command: serverCommand,
5112
- env: currentConfig.env,
5113
- startupTimeout: currentConfig.startupTimeout ?? 3e4,
5114
- debug: isDebug
5115
- });
5116
- serverStartedByUs = true;
5117
- if (isDebug) {
5118
- console.log(`[TestFixture] Server started at ${serverInstance.info.baseUrl}`);
5119
- }
5120
- } catch (error) {
5121
- const errMsg = error instanceof Error ? error.message : String(error);
5122
- throw new Error(
5123
- `Failed to start test server.
5759
+ } catch (error) {
5760
+ const errMsg = error instanceof Error ? error.message : String(error);
5761
+ throw new Error(
5762
+ `Failed to start test server.
5124
5763
 
5125
- Server entry: ${currentConfig.server}
5126
- Project: ${currentConfig.project ?? "default"}
5764
+ Server entry: ${config.server}
5765
+ Project: ${config.project ?? "default"}
5127
5766
  Command: ${serverCommand}
5128
5767
 
5129
- Error: ${errMsg}`
5130
- );
5131
- }
5132
- } else {
5133
- throw new Error(
5134
- 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
5768
+ Error: ${errMsg}`,
5769
+ { cause: error }
5135
5770
  );
5136
5771
  }
5772
+ } else {
5773
+ throw new Error('test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option');
5137
5774
  }
5138
- if (!tokenFactory) {
5139
- const sharedSecret = currentConfig.env?.["JWT_SECRET"];
5140
- const binding = serverInstance ? gatewayTokenBinding(resolveClientBaseUrl(serverInstance), currentConfig.entryPath) : void 0;
5141
- tokenFactory = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5142
- }
5775
+ const sharedSecret = config.env?.["JWT_SECRET"];
5776
+ const binding = sharedSecret ? gatewayTokenBinding(config.baseUrl ?? server.info.baseUrl, config.entryPath) : void 0;
5777
+ const tokenFactory2 = new TestTokenFactory(sharedSecret ? { hmacSecret: sharedSecret, ...binding } : {});
5778
+ return { server, startedByUs, tokenFactory: tokenFactory2, owner };
5143
5779
  }
5144
- async function createTestFixtures() {
5145
- await initializeSharedResources();
5146
- if (!serverInstance) {
5147
- throw new Error("Server instance not initialized");
5148
- }
5149
- if (!tokenFactory) {
5150
- throw new Error("Token factory not initialized");
5780
+ function acquireServer(chain) {
5781
+ const config = resolveConfig(chain);
5782
+ const key = serverKey(config);
5783
+ let entry = serverEntries.get(key);
5784
+ if (!entry) {
5785
+ entry = startServerEntry(config, ownerScopeOf(chain));
5786
+ serverEntries.set(key, entry);
5787
+ entry.catch(() => serverEntries.delete(key));
5788
+ }
5789
+ return entry;
5790
+ }
5791
+ async function stopServers(owner) {
5792
+ for (const [key, pending] of Array.from(serverEntries)) {
5793
+ let entry;
5794
+ try {
5795
+ entry = await pending;
5796
+ } catch {
5797
+ serverEntries.delete(key);
5798
+ continue;
5799
+ }
5800
+ if (owner && entry.owner !== owner) continue;
5801
+ serverEntries.delete(key);
5802
+ if (entry.startedByUs) {
5803
+ await entry.server.stop();
5804
+ }
5151
5805
  }
5806
+ }
5807
+ var fixtureContexts = /* @__PURE__ */ new WeakMap();
5808
+ async function buildFixtures(chain) {
5809
+ const config = resolveConfig(chain);
5810
+ const entry = await acquireServer(chain);
5811
+ const { server, tokenFactory: tokenFactory2 } = entry;
5812
+ const publicMode = config.publicMode ?? config.auth?.mode === "public";
5813
+ const clientConfig = { ...config, publicMode };
5152
5814
  const clientInstance = McpTestClient.create({
5153
- baseUrl: resolveClientBaseUrl(serverInstance),
5154
- entryPath: currentConfig.entryPath,
5155
- transport: currentConfig.transport ?? "streamable-http",
5156
- publicMode: currentConfig.publicMode
5815
+ baseUrl: resolveClientBaseUrl(server, clientConfig),
5816
+ entryPath: clientConfig.entryPath,
5817
+ transport: clientConfig.transport ?? "streamable-http",
5818
+ publicMode
5157
5819
  }).build();
5158
5820
  try {
5159
5821
  await clientInstance.connect();
@@ -5167,17 +5829,18 @@ async function createTestFixtures() {
5167
5829
  throw err;
5168
5830
  }
5169
5831
  }
5170
- const auth = createAuthFixture(tokenFactory);
5171
- const server = createServerFixture(serverInstance);
5172
- return {
5832
+ const context = { extraClients: [] };
5833
+ const fixtures = {
5173
5834
  mcp: clientInstance,
5174
- auth,
5175
- server
5835
+ auth: createAuthFixture(tokenFactory2),
5836
+ server: createServerFixture(server, clientConfig, clientInstance, context)
5176
5837
  };
5838
+ fixtureContexts.set(fixtures, context);
5839
+ return fixtures;
5177
5840
  }
5178
5841
  async function cleanupTestFixtures(fixtures, testFailed = false) {
5179
- if (testFailed && serverInstance) {
5180
- const logs = serverInstance.getLogs();
5842
+ if (testFailed) {
5843
+ const logs = fixtures.server.getLogs();
5181
5844
  if (logs.length > 0) {
5182
5845
  console.error("\n[TestFixture] === Server Logs (test failed) ===");
5183
5846
  const recentLogs = logs.slice(-50);
@@ -5188,20 +5851,18 @@ async function cleanupTestFixtures(fixtures, testFailed = false) {
5188
5851
  console.error("[TestFixture] === End Server Logs ===\n");
5189
5852
  }
5190
5853
  }
5854
+ const extras = fixtureContexts.get(fixtures)?.extraClients ?? [];
5855
+ for (const client of extras) {
5856
+ if (client.isConnected()) {
5857
+ await client.disconnect().catch(() => void 0);
5858
+ }
5859
+ }
5191
5860
  if (fixtures.mcp.isConnected()) {
5192
5861
  await fixtures.mcp.disconnect();
5193
5862
  }
5194
5863
  }
5195
- async function cleanupSharedResources() {
5196
- if (serverInstance && serverStartedByUs) {
5197
- await serverInstance.stop();
5198
- }
5199
- serverInstance = null;
5200
- tokenFactory = null;
5201
- serverStartedByUs = false;
5202
- }
5203
- function resolveClientBaseUrl(server) {
5204
- return currentConfig.baseUrl ?? server.info.baseUrl;
5864
+ function resolveClientBaseUrl(server, config) {
5865
+ return config.baseUrl ?? server.info.baseUrl;
5205
5866
  }
5206
5867
  function createAuthFixture(factory) {
5207
5868
  const users = {
@@ -5247,28 +5908,39 @@ function createAuthFixture(factory) {
5247
5908
  getAudience: () => factory.getAudience()
5248
5909
  };
5249
5910
  }
5250
- function createServerFixture(server) {
5911
+ function createServerFixture(server, config, mcp, context) {
5251
5912
  return {
5252
- info: server.info,
5913
+ // Live view: `pid` is filled in once the server is listening and follows restarts
5914
+ get info() {
5915
+ return server.info;
5916
+ },
5253
5917
  createClient: async (opts) => {
5254
- return McpTestClient.create({
5255
- baseUrl: resolveClientBaseUrl(server),
5256
- entryPath: opts?.entryPath ?? currentConfig.entryPath,
5918
+ const client = await McpTestClient.create({
5919
+ baseUrl: resolveClientBaseUrl(server, config),
5920
+ entryPath: opts?.entryPath ?? config.entryPath,
5257
5921
  transport: opts?.transport ?? "streamable-http",
5258
5922
  auth: opts?.token ? { token: opts.token } : void 0,
5259
5923
  clientInfo: opts?.clientInfo,
5260
- publicMode: currentConfig.publicMode
5924
+ publicMode: config.publicMode
5261
5925
  }).buildAndConnect();
5926
+ context.extraClients.push(client);
5927
+ return client;
5262
5928
  },
5263
5929
  createClientBuilder: () => {
5264
5930
  const builder = new McpTestClientBuilder({
5265
- baseUrl: resolveClientBaseUrl(server),
5266
- entryPath: currentConfig.entryPath,
5267
- publicMode: currentConfig.publicMode
5931
+ baseUrl: resolveClientBaseUrl(server, config),
5932
+ entryPath: config.entryPath,
5933
+ publicMode: config.publicMode
5268
5934
  });
5269
5935
  return builder;
5270
5936
  },
5271
- restart: () => server.restart(),
5937
+ restart: async () => {
5938
+ const reconnect = [mcp, ...context.extraClients].filter((client) => client.isConnected());
5939
+ await server.restart();
5940
+ for (const client of reconnect) {
5941
+ await client.reconnect();
5942
+ }
5943
+ },
5272
5944
  getLogs: () => server.getLogs(),
5273
5945
  clearLogs: () => server.clearLogs()
5274
5946
  };
@@ -5279,38 +5951,71 @@ function resolveServerCommand(server) {
5279
5951
  }
5280
5952
  return `npx tsx ${server}`;
5281
5953
  }
5282
- function runWithFixtures(fn) {
5283
- return async () => {
5284
- const fixtures = await createTestFixtures();
5285
- let testFailed = false;
5954
+ function runWithFixtures(fn, chain) {
5955
+ return async (...extra) => {
5956
+ const fixtures = await buildFixtures(chain);
5957
+ let failure;
5286
5958
  try {
5287
- await fn(fixtures);
5959
+ for (const scope of chain) {
5960
+ for (const hook of scope.beforeEachHooks) await hook(fixtures);
5961
+ }
5962
+ await fn(fixtures, ...extra);
5288
5963
  } catch (error) {
5289
- testFailed = true;
5290
- throw error;
5291
- } finally {
5292
- await cleanupTestFixtures(fixtures, testFailed);
5964
+ failure = { error };
5965
+ }
5966
+ try {
5967
+ await runAfterEachHooks(chain, fixtures);
5968
+ } catch (error) {
5969
+ failure ??= { error };
5293
5970
  }
5971
+ await cleanupTestFixtures(fixtures, failure !== void 0);
5972
+ if (failure) throw failure.error;
5294
5973
  };
5295
5974
  }
5975
+ async function runAfterEachHooks(chain, fixtures) {
5976
+ let firstError;
5977
+ let failed = false;
5978
+ for (let i = chain.length - 1; i >= 0; i--) {
5979
+ for (const hook of chain[i].afterEachHooks) {
5980
+ try {
5981
+ await hook(fixtures);
5982
+ } catch (error) {
5983
+ if (!failed) firstError = error;
5984
+ failed = true;
5985
+ }
5986
+ }
5987
+ }
5988
+ if (failed) throw firstError;
5989
+ }
5990
+ function skipTitle(name, scope) {
5991
+ return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
5992
+ }
5296
5993
  function testWithFixtures(name, fn) {
5297
- const scope = currentSkipScope();
5994
+ const chain = scopes.slice();
5995
+ const scope = currentScope();
5298
5996
  if (scope.skipped) {
5299
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
5997
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, chain));
5300
5998
  return;
5301
5999
  }
5302
- it2(name, runWithFixtures(fn));
6000
+ jestGlobals().it(name, runWithFixtures(fn, chain));
5303
6001
  }
5304
6002
  function use(config) {
5305
- currentConfig = { ...currentConfig, ...config };
5306
- afterAll2(async () => {
5307
- await cleanupSharedResources();
5308
- });
6003
+ const scope = currentScope();
6004
+ const { env, ...rest } = config;
6005
+ scope.config = { ...scope.config, ...rest };
6006
+ if (env) scope.envs.push(env);
6007
+ scope.configured = true;
6008
+ if (!scope.cleanupRegistered) {
6009
+ scope.cleanupRegistered = true;
6010
+ jestGlobals().afterAll(async () => {
6011
+ await stopServers(scope === scopes[0] ? void 0 : scope);
6012
+ });
6013
+ }
5309
6014
  }
5310
6015
  function skip(nameOrCondition, fnOrReason) {
5311
6016
  if (typeof nameOrCondition === "boolean") {
5312
6017
  if (nameOrCondition) {
5313
- const scope = currentSkipScope();
6018
+ const scope = currentScope();
5314
6019
  scope.skipped = true;
5315
6020
  scope.reason = typeof fnOrReason === "string" ? fnOrReason : void 0;
5316
6021
  }
@@ -5321,57 +6026,114 @@ function skip(nameOrCondition, fnOrReason) {
5321
6026
  `test.skip expects either (name: string, fn) or (condition: boolean, reason?: string); received (${typeof nameOrCondition}, ${typeof fnOrReason}).`
5322
6027
  );
5323
6028
  }
5324
- it2.skip(nameOrCondition, runWithFixtures(fnOrReason));
6029
+ jestGlobals().it.skip(nameOrCondition, runWithFixtures(fnOrReason, scopes.slice()));
5325
6030
  }
5326
6031
  function only(name, fn) {
5327
- const scope = currentSkipScope();
6032
+ const scope = currentScope();
5328
6033
  if (scope.skipped) {
5329
- it2.skip(scope.reason ? `${name} (skipped: ${scope.reason})` : name, runWithFixtures(fn));
6034
+ jestGlobals().it.skip(skipTitle(name, scope), runWithFixtures(fn, scopes.slice()));
5330
6035
  return;
5331
6036
  }
5332
- it2.only(name, runWithFixtures(fn));
6037
+ jestGlobals().it.only(name, runWithFixtures(fn, scopes.slice()));
5333
6038
  }
5334
6039
  function todo(name) {
5335
- it2.todo(name);
6040
+ jestGlobals().it.todo(name);
5336
6041
  }
5337
- function withSkipScope(register) {
6042
+ function each(table, ...tagged) {
6043
+ const chain = scopes.slice();
6044
+ const register = jestGlobals().it.each(table, ...tagged);
5338
6045
  return (name, fn) => {
5339
- register(name, () => {
5340
- const parent = currentSkipScope();
5341
- skipScopes.push({ skipped: parent.skipped, reason: parent.reason });
5342
- try {
5343
- fn();
5344
- } finally {
5345
- skipScopes.pop();
5346
- }
5347
- });
6046
+ const scope = currentScope();
6047
+ if (scope.skipped) {
6048
+ const skipRegister = jestGlobals().it.skip.each(table, ...tagged);
6049
+ skipRegister(skipTitle(name, scope), runWithFixtures(fn, chain));
6050
+ return;
6051
+ }
6052
+ register(name, runWithFixtures(fn, chain));
5348
6053
  };
5349
6054
  }
5350
- function eachWithSkipScope(each) {
5351
- return ((...eachArgs) => {
5352
- const register = each(...eachArgs);
5353
- return withSkipScope(register);
5354
- });
6055
+ function withScope(fn) {
6056
+ return (...args) => {
6057
+ scopes.push(newScope(currentScope()));
6058
+ try {
6059
+ fn(...args);
6060
+ } finally {
6061
+ scopes.pop();
6062
+ }
6063
+ };
6064
+ }
6065
+ function scopedRegister(pick) {
6066
+ return (name, fn, timeout) => {
6067
+ pick()(name, withScope(fn), timeout);
6068
+ };
6069
+ }
6070
+ function scopedEach(pick) {
6071
+ return (...eachArgs) => (name, fn, timeout) => {
6072
+ const register = pick()(...eachArgs);
6073
+ register(name, withScope(fn), timeout);
6074
+ };
6075
+ }
6076
+ var describeWithScope = Object.assign(
6077
+ scopedRegister(() => jestGlobals().describe),
6078
+ {
6079
+ // `describe.skip` still evaluates its callback during collection, so it needs a scope too
6080
+ only: Object.assign(
6081
+ scopedRegister(() => jestGlobals().describe.only),
6082
+ {
6083
+ each: scopedEach(() => jestGlobals().describe.only.each)
6084
+ }
6085
+ ),
6086
+ skip: Object.assign(
6087
+ scopedRegister(() => jestGlobals().describe.skip),
6088
+ {
6089
+ each: scopedEach(() => jestGlobals().describe.skip.each)
6090
+ }
6091
+ ),
6092
+ each: scopedEach(() => jestGlobals().describe.each)
6093
+ }
6094
+ );
6095
+ function eachHook(kind) {
6096
+ return (fn, timeout) => {
6097
+ if (typeof fn === "function" && fn.length > 0) {
6098
+ const scope = currentScope();
6099
+ (kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
6100
+ return;
6101
+ }
6102
+ jestGlobals()[kind](fn, timeout);
6103
+ };
5355
6104
  }
5356
- var describeWithSkipScope = Object.assign(withSkipScope(describe2), describe2, {
5357
- only: Object.assign(withSkipScope(describe2.only), { each: eachWithSkipScope(describe2.only.each) }),
5358
- skip: Object.assign(withSkipScope(describe2.skip), { each: eachWithSkipScope(describe2.skip.each) }),
5359
- each: eachWithSkipScope(describe2.each)
5360
- });
5361
6105
  var test = testWithFixtures;
5362
6106
  test.use = use;
5363
- test.describe = describeWithSkipScope;
5364
- test.beforeAll = beforeAll2;
5365
- test.beforeEach = beforeEach2;
5366
- test.afterEach = afterEach2;
5367
- test.afterAll = afterAll2;
6107
+ test.describe = describeWithScope;
6108
+ test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
6109
+ test.beforeEach = eachHook("beforeEach");
6110
+ test.afterEach = eachHook("afterEach");
6111
+ test.afterAll = ((...args) => jestGlobals().afterAll(...args));
5368
6112
  test.skip = skip;
5369
6113
  test.only = only;
5370
6114
  test.todo = todo;
6115
+ test.each = each;
5371
6116
 
5372
6117
  // libs/testing/src/expect.ts
5373
- var import_globals2 = require("@jest/globals");
5374
- var expect = import_globals2.expect;
6118
+ var resolvedExpect;
6119
+ function resolveJestExpect() {
6120
+ if (resolvedExpect) return resolvedExpect;
6121
+ try {
6122
+ resolvedExpect = require("@jest/globals").expect;
6123
+ return resolvedExpect;
6124
+ } catch (error) {
6125
+ throw new Error(
6126
+ `expect() from @frontmcp/testing can only be used inside a Jest test file: ${error instanceof Error ? error.message : String(error)}`,
6127
+ { cause: error }
6128
+ );
6129
+ }
6130
+ }
6131
+ var expect = new Proxy(function frontmcpExpect() {
6132
+ }, {
6133
+ apply: (_target, thisArg, args) => Reflect.apply(resolveJestExpect(), thisArg, args),
6134
+ get: (_target, prop) => Reflect.get(resolveJestExpect(), prop),
6135
+ has: (_target, prop) => Reflect.has(resolveJestExpect(), prop)
6136
+ });
5375
6137
 
5376
6138
  // libs/testing/src/platform/platform-types.ts
5377
6139
  function getPlatformMetaNamespace(platform) {
@@ -6034,34 +6796,44 @@ var HttpInterceptorImpl = class {
6034
6796
  }
6035
6797
  };
6036
6798
  }
6037
- get(url, response) {
6799
+ get(url, response, options) {
6038
6800
  return this.mock({
6039
- match: { url, method: "GET" },
6040
- response: normalizeResponse(response)
6801
+ match: { url, method: "GET", ...options?.match },
6802
+ response: normalizeResponse(response),
6803
+ ...options?.times !== void 0 && { times: options.times },
6804
+ ...options?.name !== void 0 && { name: options.name }
6041
6805
  });
6042
6806
  }
6043
- post(url, response) {
6807
+ post(url, response, options) {
6044
6808
  return this.mock({
6045
- match: { url, method: "POST" },
6046
- response: normalizeResponse(response)
6809
+ match: { url, method: "POST", ...options?.match },
6810
+ response: normalizeResponse(response),
6811
+ ...options?.times !== void 0 && { times: options.times },
6812
+ ...options?.name !== void 0 && { name: options.name }
6047
6813
  });
6048
6814
  }
6049
- put(url, response) {
6815
+ put(url, response, options) {
6050
6816
  return this.mock({
6051
- match: { url, method: "PUT" },
6052
- response: normalizeResponse(response)
6817
+ match: { url, method: "PUT", ...options?.match },
6818
+ response: normalizeResponse(response),
6819
+ ...options?.times !== void 0 && { times: options.times },
6820
+ ...options?.name !== void 0 && { name: options.name }
6053
6821
  });
6054
6822
  }
6055
- delete(url, response) {
6823
+ delete(url, response, options) {
6056
6824
  return this.mock({
6057
- match: { url, method: "DELETE" },
6058
- response: normalizeResponse(response)
6825
+ match: { url, method: "DELETE", ...options?.match },
6826
+ response: normalizeResponse(response),
6827
+ ...options?.times !== void 0 && { times: options.times },
6828
+ ...options?.name !== void 0 && { name: options.name }
6059
6829
  });
6060
6830
  }
6061
- any(url, response) {
6831
+ any(url, response, options) {
6062
6832
  return this.mock({
6063
- match: { url },
6064
- response: normalizeResponse(response)
6833
+ match: { url, ...options?.match },
6834
+ response: normalizeResponse(response),
6835
+ ...options?.times !== void 0 && { times: options.times },
6836
+ ...options?.name !== void 0 && { name: options.name }
6065
6837
  });
6066
6838
  }
6067
6839
  clear() {
@@ -6093,6 +6865,9 @@ ${descriptions.join("\n")}`);
6093
6865
  if (index !== -1) {
6094
6866
  activeInterceptors.splice(index, 1);
6095
6867
  }
6868
+ if (activeInterceptors.length === 0) {
6869
+ httpMock.disable();
6870
+ }
6096
6871
  }
6097
6872
  // Internal methods
6098
6873
  isActive() {
@@ -6192,43 +6967,49 @@ ${descriptions.join("\n")}`);
6192
6967
  }
6193
6968
  };
6194
6969
  async function interceptedFetch(input, init) {
6195
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
6196
- const method = (init?.method ?? "GET").toUpperCase();
6970
+ const request = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
6971
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : request?.url ?? String(input);
6972
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
6197
6973
  const headers = {};
6198
- if (init?.headers) {
6199
- if (init.headers instanceof Headers) {
6200
- init.headers.forEach((value, key) => {
6974
+ const collectHeaders = (source) => {
6975
+ if (!source) return;
6976
+ if (source instanceof Headers) {
6977
+ source.forEach((value, key) => {
6201
6978
  headers[key.toLowerCase()] = value;
6202
6979
  });
6203
- } else if (Array.isArray(init.headers)) {
6204
- for (const [key, value] of init.headers) {
6980
+ } else if (Array.isArray(source)) {
6981
+ for (const [key, value] of source) {
6205
6982
  headers[key.toLowerCase()] = value;
6206
6983
  }
6207
6984
  } else {
6208
- for (const [key, value] of Object.entries(init.headers)) {
6985
+ for (const [key, value] of Object.entries(source)) {
6209
6986
  headers[key.toLowerCase()] = value;
6210
6987
  }
6211
6988
  }
6212
- }
6989
+ };
6990
+ collectHeaders(init?.headers ?? request?.headers);
6213
6991
  let body;
6214
6992
  let rawBody;
6215
- if (init?.body) {
6216
- if (typeof init.body === "string") {
6217
- rawBody = init.body;
6218
- try {
6219
- body = JSON.parse(init.body);
6220
- } catch {
6221
- body = init.body;
6222
- }
6223
- } else if (init.body instanceof ArrayBuffer || init.body instanceof Uint8Array) {
6224
- rawBody = new TextDecoder().decode(init.body);
6993
+ let bodySource = init?.body;
6994
+ if ((bodySource === void 0 || bodySource === null) && request && request.body !== null) {
6995
+ bodySource = await request.clone().text();
6996
+ }
6997
+ if (bodySource) {
6998
+ if (typeof bodySource === "string") {
6999
+ rawBody = bodySource;
7000
+ } else if (bodySource instanceof ArrayBuffer || ArrayBuffer.isView(bodySource)) {
7001
+ rawBody = new TextDecoder().decode(bodySource);
7002
+ } else if (bodySource instanceof URLSearchParams) {
7003
+ rawBody = bodySource.toString();
7004
+ }
7005
+ if (rawBody !== void 0) {
6225
7006
  try {
6226
7007
  body = JSON.parse(rawBody);
6227
7008
  } catch {
6228
7009
  body = rawBody;
6229
7010
  }
6230
7011
  } else {
6231
- body = init.body;
7012
+ body = bodySource;
6232
7013
  }
6233
7014
  }
6234
7015
  const requestInfo = {
@@ -6258,8 +7039,20 @@ async function interceptedFetch(input, init) {
6258
7039
  Add a mock using httpMock.interceptor().${method.toLowerCase()}('${url}', { body: ... })`
6259
7040
  );
6260
7041
  }
7042
+ var RESPONSE_KEYS = /* @__PURE__ */ new Set(["status", "statusText", "headers", "body", "delay"]);
7043
+ function isResponseDescriptor(value) {
7044
+ const keys = Object.keys(value);
7045
+ if (keys.length === 0 || !keys.every((key) => RESPONSE_KEYS.has(key))) return false;
7046
+ if ("status" in value) {
7047
+ const status = value["status"];
7048
+ if (typeof status !== "number" || !Number.isInteger(status) || status < 200 || status > 599) return false;
7049
+ }
7050
+ return true;
7051
+ }
6261
7052
  function normalizeResponse(response) {
6262
- if (!("status" in response) && !("body" in response) && !("headers" in response)) {
7053
+ const isNetworkError = !Array.isArray(response) && response["_throwError"] === true;
7054
+ if (isNetworkError) return response;
7055
+ if (Array.isArray(response) || !isResponseDescriptor(response)) {
6263
7056
  return { body: response };
6264
7057
  }
6265
7058
  return response;
@@ -7593,47 +8386,48 @@ function clearGlobalMeasurements() {
7593
8386
  }
7594
8387
 
7595
8388
  // libs/testing/src/perf/perf-test.ts
7596
- var currentConfig2 = {};
7597
- var serverInstance2 = null;
7598
- var tokenFactory2 = null;
7599
- var serverStartedByUs2 = false;
7600
- async function initializeSharedResources2() {
7601
- if (!tokenFactory2) {
7602
- tokenFactory2 = new TestTokenFactory();
7603
- }
7604
- if (!serverInstance2) {
7605
- if (currentConfig2.baseUrl) {
7606
- serverInstance2 = TestServer.connect(currentConfig2.baseUrl);
7607
- serverStartedByUs2 = false;
7608
- } else if (currentConfig2.server) {
7609
- const serverCommand = resolveServerCommand2(currentConfig2.server);
8389
+ var currentConfig = {};
8390
+ var serverInstance = null;
8391
+ var tokenFactory = null;
8392
+ var serverStartedByUs = false;
8393
+ async function initializeSharedResources() {
8394
+ if (!tokenFactory) {
8395
+ tokenFactory = new TestTokenFactory();
8396
+ }
8397
+ if (!serverInstance) {
8398
+ if (currentConfig.baseUrl) {
8399
+ serverInstance = TestServer.connect(currentConfig.baseUrl);
8400
+ serverStartedByUs = false;
8401
+ } else if (currentConfig.server) {
8402
+ const serverCommand = resolveServerCommand2(currentConfig.server);
7610
8403
  const isDebug = process.env["DEBUG"] === "1" || process.env["DEBUG_SERVER"] === "1";
7611
8404
  if (isDebug) {
7612
8405
  console.log(`[PerfTest] Starting server: ${serverCommand}`);
7613
8406
  }
7614
8407
  try {
7615
- serverInstance2 = await TestServer.start({
7616
- project: currentConfig2.project,
7617
- port: currentConfig2.port,
8408
+ serverInstance = await TestServer.start({
8409
+ project: currentConfig.project,
8410
+ port: currentConfig.port,
7618
8411
  command: serverCommand,
7619
- env: currentConfig2.env,
7620
- startupTimeout: currentConfig2.startupTimeout ?? 3e4,
8412
+ env: currentConfig.env,
8413
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
7621
8414
  debug: isDebug
7622
8415
  });
7623
- serverStartedByUs2 = true;
8416
+ serverStartedByUs = true;
7624
8417
  if (isDebug) {
7625
- console.log(`[PerfTest] Server started at ${serverInstance2.info.baseUrl}`);
8418
+ console.log(`[PerfTest] Server started at ${serverInstance.info.baseUrl}`);
7626
8419
  }
7627
8420
  } catch (error) {
7628
8421
  const errMsg = error instanceof Error ? error.message : String(error);
7629
8422
  throw new Error(
7630
8423
  `Failed to start test server.
7631
8424
 
7632
- Server entry: ${currentConfig2.server}
7633
- Project: ${currentConfig2.project ?? "default"}
8425
+ Server entry: ${currentConfig.server}
8426
+ Project: ${currentConfig.project ?? "default"}
7634
8427
  Command: ${serverCommand}
7635
8428
 
7636
- Error: ${errMsg}`
8429
+ Error: ${errMsg}`,
8430
+ { cause: error }
7637
8431
  );
7638
8432
  }
7639
8433
  } else {
@@ -7641,23 +8435,23 @@ Error: ${errMsg}`
7641
8435
  }
7642
8436
  }
7643
8437
  }
7644
- async function createTestFixtures2(testName) {
7645
- await initializeSharedResources2();
7646
- if (!serverInstance2) {
8438
+ async function createTestFixtures(testName) {
8439
+ await initializeSharedResources();
8440
+ if (!serverInstance) {
7647
8441
  throw new Error("Server instance not initialized");
7648
8442
  }
7649
- if (!tokenFactory2) {
8443
+ if (!tokenFactory) {
7650
8444
  throw new Error("Token factory not initialized");
7651
8445
  }
7652
8446
  const clientInstance = await McpTestClient.create({
7653
- baseUrl: serverInstance2.info.baseUrl,
7654
- transport: currentConfig2.transport ?? "streamable-http",
7655
- publicMode: currentConfig2.publicMode
8447
+ baseUrl: serverInstance.info.baseUrl,
8448
+ transport: currentConfig.transport ?? "streamable-http",
8449
+ publicMode: currentConfig.publicMode
7656
8450
  }).buildAndConnect();
7657
- const auth = createAuthFixture2(tokenFactory2);
7658
- const server = createServerFixture2(serverInstance2);
7659
- const perfImpl = createPerfFixtures(testName, currentConfig2.project ?? "unknown");
7660
- if (currentConfig2.forceGcOnBaseline !== false) {
8451
+ const auth = createAuthFixture2(tokenFactory);
8452
+ const server = createServerFixture2(serverInstance);
8453
+ const perfImpl = createPerfFixtures(testName, currentConfig.project ?? "unknown");
8454
+ if (currentConfig.forceGcOnBaseline !== false) {
7661
8455
  await perfImpl.baseline();
7662
8456
  }
7663
8457
  return {
@@ -7673,8 +8467,8 @@ async function createTestFixtures2(testName) {
7673
8467
  async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7674
8468
  const measurement = perfImpl.buildMeasurement();
7675
8469
  addGlobalMeasurement(measurement);
7676
- if (testFailed && serverInstance2) {
7677
- const logs = serverInstance2.getLogs();
8470
+ if (testFailed && serverInstance) {
8471
+ const logs = serverInstance.getLogs();
7678
8472
  if (logs.length > 0) {
7679
8473
  console.error("\n[PerfTest] === Server Logs (test failed) ===");
7680
8474
  const recentLogs = logs.slice(-50);
@@ -7689,13 +8483,13 @@ async function cleanupTestFixtures2(fixtures, perfImpl, testFailed = false) {
7689
8483
  await fixtures.mcp.disconnect();
7690
8484
  }
7691
8485
  }
7692
- async function cleanupSharedResources2() {
7693
- if (serverInstance2 && serverStartedByUs2) {
7694
- await serverInstance2.stop();
8486
+ async function cleanupSharedResources() {
8487
+ if (serverInstance && serverStartedByUs) {
8488
+ await serverInstance.stop();
7695
8489
  }
7696
- serverInstance2 = null;
7697
- tokenFactory2 = null;
7698
- serverStartedByUs2 = false;
8490
+ serverInstance = null;
8491
+ tokenFactory = null;
8492
+ serverStartedByUs = false;
7699
8493
  }
7700
8494
  function createAuthFixture2(factory) {
7701
8495
  const users = {
@@ -7750,13 +8544,13 @@ function createServerFixture2(server) {
7750
8544
  transport: opts?.transport ?? "streamable-http",
7751
8545
  auth: opts?.token ? { token: opts.token } : void 0,
7752
8546
  clientInfo: opts?.clientInfo,
7753
- publicMode: currentConfig2.publicMode
8547
+ publicMode: currentConfig.publicMode
7754
8548
  }).buildAndConnect();
7755
8549
  },
7756
8550
  createClientBuilder: () => {
7757
8551
  return new McpTestClientBuilder({
7758
8552
  baseUrl: server.info.baseUrl,
7759
- publicMode: currentConfig2.publicMode
8553
+ publicMode: currentConfig.publicMode
7760
8554
  });
7761
8555
  },
7762
8556
  restart: () => server.restart(),
@@ -7772,7 +8566,7 @@ function resolveServerCommand2(server) {
7772
8566
  }
7773
8567
  function perfTestWithFixtures(name, fn) {
7774
8568
  it(name, async () => {
7775
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8569
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7776
8570
  let testFailed = false;
7777
8571
  try {
7778
8572
  await fn(fixtures);
@@ -7785,14 +8579,14 @@ function perfTestWithFixtures(name, fn) {
7785
8579
  });
7786
8580
  }
7787
8581
  function use2(config) {
7788
- currentConfig2 = { ...currentConfig2, ...config };
8582
+ currentConfig = { ...currentConfig, ...config };
7789
8583
  afterAll(async () => {
7790
- await cleanupSharedResources2();
8584
+ await cleanupSharedResources();
7791
8585
  });
7792
8586
  }
7793
8587
  function skip2(name, fn) {
7794
8588
  it.skip(name, async () => {
7795
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8589
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7796
8590
  let testFailed = false;
7797
8591
  try {
7798
8592
  await fn(fixtures);
@@ -7806,7 +8600,7 @@ function skip2(name, fn) {
7806
8600
  }
7807
8601
  function only2(name, fn) {
7808
8602
  it.only(name, async () => {
7809
- const { fixtures, perfImpl } = await createTestFixtures2(name);
8603
+ const { fixtures, perfImpl } = await createTestFixtures(name);
7810
8604
  let testFailed = false;
7811
8605
  try {
7812
8606
  await fn(fixtures);
@@ -7823,16 +8617,19 @@ function todo2(name) {
7823
8617
  }
7824
8618
  var perfTest = perfTestWithFixtures;
7825
8619
  perfTest.use = use2;
7826
- perfTest.describe = describe;
7827
- perfTest.beforeAll = beforeAll;
7828
- perfTest.beforeEach = beforeEach;
7829
- perfTest.afterEach = afterEach;
7830
- perfTest.afterAll = afterAll;
8620
+ for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
8621
+ Object.defineProperty(perfTest, hook, {
8622
+ get: () => globalThis[hook],
8623
+ enumerable: true,
8624
+ configurable: true
8625
+ });
8626
+ }
7831
8627
  perfTest.skip = skip2;
7832
8628
  perfTest.only = only2;
7833
8629
  perfTest.todo = todo2;
7834
8630
 
7835
8631
  // libs/testing/src/perf/baseline-store.ts
8632
+ var import_utils4 = require("@frontmcp/utils");
7836
8633
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
7837
8634
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
7838
8635
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -7847,11 +8644,10 @@ var BaselineStore = class {
7847
8644
  */
7848
8645
  async load() {
7849
8646
  try {
7850
- const { readFile, fileExists } = await import("@frontmcp/utils");
7851
- if (!await fileExists(this.baselinePath)) {
8647
+ if (!await (0, import_utils4.fileExists)(this.baselinePath)) {
7852
8648
  return null;
7853
8649
  }
7854
- const content = await readFile(this.baselinePath);
8650
+ const content = await (0, import_utils4.readFile)(this.baselinePath);
7855
8651
  this.baseline = JSON.parse(content);
7856
8652
  return this.baseline;
7857
8653
  } catch {
@@ -7862,10 +8658,9 @@ var BaselineStore = class {
7862
8658
  * Save baseline to local file.
7863
8659
  */
7864
8660
  async save(baseline) {
7865
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
7866
8661
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
7867
- await ensureDir(dir);
7868
- await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
8662
+ await (0, import_utils4.ensureDir)(dir);
8663
+ await (0, import_utils4.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
7869
8664
  this.baseline = baseline;
7870
8665
  }
7871
8666
  /**
@@ -8118,6 +8913,7 @@ function getRegressionDetector(config) {
8118
8913
  }
8119
8914
 
8120
8915
  // libs/testing/src/perf/report-generator.ts
8916
+ var import_utils5 = require("@frontmcp/utils");
8121
8917
  var ReportGenerator = class {
8122
8918
  detector;
8123
8919
  constructor() {
@@ -8373,14 +9169,13 @@ var ReportGenerator = class {
8373
9169
  }
8374
9170
  };
8375
9171
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
8376
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
8377
- await ensureDir(outputDir);
9172
+ await (0, import_utils5.ensureDir)(outputDir);
8378
9173
  const generator = new ReportGenerator();
8379
9174
  const report = generator.generateReport(measurements, baseline, gitInfo);
8380
9175
  const jsonPath = `${outputDir}/report.json`;
8381
9176
  const markdownPath = `${outputDir}/report.md`;
8382
- await writeFile(jsonPath, generator.generateJsonReport(report));
8383
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
9177
+ await (0, import_utils5.writeFile)(jsonPath, generator.generateJsonReport(report));
9178
+ await (0, import_utils5.writeFile)(markdownPath, generator.generateMarkdownReport(report));
8384
9179
  return { jsonPath, markdownPath };
8385
9180
  }
8386
9181
  function createReportGenerator() {
@@ -8442,6 +9237,7 @@ function isTcpPortListening(port, host = "127.0.0.1", timeoutMs = 1500) {
8442
9237
  RegressionDetector,
8443
9238
  ReportGenerator,
8444
9239
  ServerStartError,
9240
+ SseTransport,
8445
9241
  StreamableHttpTransport,
8446
9242
  TestClientError,
8447
9243
  TestServer,