@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/perf/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/perf/index.ts
@@ -834,6 +824,192 @@ function getMeasurementsForProject(project) {
834
824
  return getGlobalMeasurementsArray().filter((m) => m.project === project);
835
825
  }
836
826
 
827
+ // libs/testing/src/auth/token-factory.ts
828
+ var import_jose = require("jose");
829
+ var TestTokenFactory = class {
830
+ issuer;
831
+ audience;
832
+ privateKey = null;
833
+ publicKey = null;
834
+ jwk = null;
835
+ keyId;
836
+ /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
837
+ hmacSecret;
838
+ constructor(options = {}) {
839
+ this.issuer = options.issuer ?? "https://test.frontmcp.local";
840
+ this.audience = options.audience ?? "frontmcp-test";
841
+ this.keyId = `test-key-${Date.now()}`;
842
+ this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
843
+ }
844
+ /** True when this factory signs HS256 with a shared secret (gateway mode). */
845
+ get isHmac() {
846
+ return this.hmacSecret !== null;
847
+ }
848
+ /** The JWS `alg` this factory signs with. */
849
+ get alg() {
850
+ return this.isHmac ? "HS256" : "RS256";
851
+ }
852
+ /**
853
+ * The signing key: the shared HMAC secret in gateway mode, otherwise the
854
+ * generated RSA private key (lazily created via ensureKeys()).
855
+ */
856
+ signingKey() {
857
+ if (this.hmacSecret) return this.hmacSecret;
858
+ if (!this.privateKey) {
859
+ throw new Error("Private key not initialized");
860
+ }
861
+ return this.privateKey;
862
+ }
863
+ /**
864
+ * Initialize the key pair (called automatically on first use). No-op in HS256
865
+ * mode — there is no asymmetric keypair, just the shared secret.
866
+ */
867
+ async ensureKeys() {
868
+ if (this.isHmac) return;
869
+ if (this.privateKey && this.publicKey) return;
870
+ const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
871
+ extractable: true
872
+ });
873
+ this.privateKey = privateKey;
874
+ this.publicKey = publicKey;
875
+ this.jwk = await (0, import_jose.exportJWK)(publicKey);
876
+ this.jwk.kid = this.keyId;
877
+ this.jwk.use = "sig";
878
+ this.jwk.alg = "RS256";
879
+ }
880
+ /**
881
+ * Create a JWT token with the specified claims
882
+ */
883
+ async createTestToken(options) {
884
+ await this.ensureKeys();
885
+ const nowMs = Date.now();
886
+ const iat = Math.floor(nowMs / 1e3);
887
+ const exp = options.exp ?? 3600;
888
+ const payload = {
889
+ iss: options.iss ?? this.issuer,
890
+ sub: options.sub,
891
+ aud: options.aud ?? this.audience,
892
+ iat,
893
+ exp: Math.ceil(nowMs / 1e3) + exp,
894
+ scope: options.scopes?.join(" "),
895
+ ...options.claims
896
+ };
897
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
898
+ const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
899
+ return token;
900
+ }
901
+ /**
902
+ * Create an admin token with full access
903
+ */
904
+ async createAdminToken(sub = "admin-001") {
905
+ return this.createTestToken({
906
+ sub,
907
+ scopes: ["admin:*", "read", "write", "delete"],
908
+ claims: {
909
+ email: "admin@test.local",
910
+ name: "Test Admin",
911
+ role: "admin"
912
+ }
913
+ });
914
+ }
915
+ /**
916
+ * Create a regular user token
917
+ */
918
+ async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
919
+ return this.createTestToken({
920
+ sub,
921
+ scopes,
922
+ claims: {
923
+ email: "user@test.local",
924
+ name: "Test User",
925
+ role: "user"
926
+ }
927
+ });
928
+ }
929
+ /**
930
+ * Create an anonymous user token
931
+ */
932
+ async createAnonymousToken(expiresIn) {
933
+ return this.createTestToken({
934
+ sub: `anon:${Date.now()}`,
935
+ scopes: ["anonymous"],
936
+ exp: expiresIn,
937
+ claims: {
938
+ name: "Anonymous",
939
+ role: "anonymous"
940
+ }
941
+ });
942
+ }
943
+ /**
944
+ * Create an expired token (for testing token expiration)
945
+ */
946
+ async createExpiredToken(options) {
947
+ await this.ensureKeys();
948
+ const now = Math.floor(Date.now() / 1e3);
949
+ const payload = {
950
+ iss: this.issuer,
951
+ sub: options.sub,
952
+ aud: this.audience,
953
+ iat: now - 7200,
954
+ // 2 hours ago
955
+ exp: now - 3600
956
+ // Expired 1 hour ago
957
+ };
958
+ const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
959
+ const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
960
+ return token;
961
+ }
962
+ /**
963
+ * Create a token with an invalid signature (for testing signature validation)
964
+ */
965
+ createTokenWithInvalidSignature(options) {
966
+ const now = Math.floor(Date.now() / 1e3);
967
+ const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
968
+ const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
969
+ const payload = Buffer.from(
970
+ JSON.stringify({
971
+ iss: this.issuer,
972
+ sub: options.sub,
973
+ aud: this.audience,
974
+ iat: now,
975
+ exp: now + 3600
976
+ })
977
+ ).toString("base64url");
978
+ const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
979
+ return `${header}.${payload}.${signature}`;
980
+ }
981
+ /**
982
+ * Get the public JWKS for verifying tokens.
983
+ *
984
+ * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
985
+ * the shared secret, not a published public key, so there is no JWKS to serve.
986
+ */
987
+ async getPublicJwks() {
988
+ if (this.isHmac) {
989
+ throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
990
+ }
991
+ await this.ensureKeys();
992
+ if (!this.jwk) {
993
+ throw new Error("JWK not initialized");
994
+ }
995
+ return {
996
+ keys: [this.jwk]
997
+ };
998
+ }
999
+ /**
1000
+ * Get the issuer URL
1001
+ */
1002
+ getIssuer() {
1003
+ return this.issuer;
1004
+ }
1005
+ /**
1006
+ * Get the audience
1007
+ */
1008
+ getAudience() {
1009
+ return this.audience;
1010
+ }
1011
+ };
1012
+
837
1013
  // libs/testing/src/interceptor/mock-registry.ts
838
1014
  var DefaultMockRegistry = class {
839
1015
  mocks = [];
@@ -1106,8 +1282,479 @@ function sleep3(ms) {
1106
1282
  return new Promise((resolve) => setTimeout(resolve, ms));
1107
1283
  }
1108
1284
 
1109
- // libs/testing/src/transport/streamable-http.transport.ts
1285
+ // libs/testing/src/transport/error-utils.ts
1286
+ function errorName(err) {
1287
+ if (typeof err !== "object" || err === null) return void 0;
1288
+ const name = err.name;
1289
+ return typeof name === "string" ? name : void 0;
1290
+ }
1291
+ function isAbortError(err) {
1292
+ return errorName(err) === "AbortError";
1293
+ }
1294
+ function errorMessage(err, fallback = "Unknown error") {
1295
+ if (typeof err === "string" && err) return err;
1296
+ if (typeof err === "object" && err !== null) {
1297
+ const message = err.message;
1298
+ if (typeof message === "string" && message) return message;
1299
+ }
1300
+ return fallback;
1301
+ }
1302
+ var interceptedErrors = /* @__PURE__ */ new WeakSet();
1303
+ function markInterceptedError(err) {
1304
+ interceptedErrors.add(err);
1305
+ return err;
1306
+ }
1307
+ function isInterceptedError(err) {
1308
+ return typeof err === "object" && err !== null && interceptedErrors.has(err);
1309
+ }
1310
+
1311
+ // libs/testing/src/transport/sse.transport.ts
1110
1312
  var DEFAULT_TIMEOUT = 3e4;
1313
+ var SseTransport = class {
1314
+ baseUrl;
1315
+ entryPath;
1316
+ timeout;
1317
+ authHeaders;
1318
+ publicMode;
1319
+ debug;
1320
+ clientInfo;
1321
+ notificationHandler;
1322
+ interceptors;
1323
+ elicitationHandler;
1324
+ state = "disconnected";
1325
+ authToken;
1326
+ sessionId;
1327
+ messageEndpoint;
1328
+ stream;
1329
+ pending = /* @__PURE__ */ new Map();
1330
+ connectionCount = 0;
1331
+ reconnectCount = 0;
1332
+ lastRequestHeaders = {};
1333
+ constructor(config) {
1334
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
1335
+ this.entryPath = config.entryPath;
1336
+ this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
1337
+ this.authHeaders = config.auth?.headers ?? {};
1338
+ this.authToken = config.auth?.token;
1339
+ this.publicMode = config.publicMode ?? false;
1340
+ this.debug = config.debug ?? false;
1341
+ this.clientInfo = config.clientInfo;
1342
+ this.notificationHandler = config.notificationHandler;
1343
+ this.interceptors = config.interceptors;
1344
+ this.elicitationHandler = config.elicitationHandler;
1345
+ }
1346
+ async connect() {
1347
+ this.state = "connecting";
1348
+ this.connectionCount++;
1349
+ try {
1350
+ if (!this.publicMode && !this.authToken) {
1351
+ await this.requestAnonymousToken();
1352
+ }
1353
+ await this.openStream();
1354
+ this.state = "connected";
1355
+ this.log(`Connected; messages go to ${this.messageEndpoint}`);
1356
+ } catch (error) {
1357
+ this.state = "error";
1358
+ this.stream?.abort();
1359
+ this.stream = void 0;
1360
+ throw error;
1361
+ }
1362
+ }
1363
+ async request(message) {
1364
+ this.ensureConnected();
1365
+ const startTime = Date.now();
1366
+ if (this.interceptors) {
1367
+ const intercepted = await this.interceptors.processRequest(message, {
1368
+ timestamp: /* @__PURE__ */ new Date(),
1369
+ transport: "sse",
1370
+ sessionId: this.sessionId
1371
+ });
1372
+ switch (intercepted.type) {
1373
+ case "mock":
1374
+ return await this.interceptors.processResponse(
1375
+ message,
1376
+ intercepted.response,
1377
+ Date.now() - startTime
1378
+ );
1379
+ case "error":
1380
+ throw markInterceptedError(intercepted.error);
1381
+ case "continue":
1382
+ message = intercepted.request;
1383
+ break;
1384
+ }
1385
+ }
1386
+ let response;
1387
+ if (message.id === void 0) {
1388
+ await this.post(JSON.stringify(message));
1389
+ response = { jsonrpc: "2.0", id: null, result: void 0 };
1390
+ } else {
1391
+ response = await this.postAndAwait(message.id, JSON.stringify(message));
1392
+ }
1393
+ if (this.interceptors) {
1394
+ response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
1395
+ }
1396
+ return response;
1397
+ }
1398
+ async notify(message) {
1399
+ this.ensureConnected();
1400
+ const result = await this.post(JSON.stringify(message));
1401
+ if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
1402
+ }
1403
+ async sendRaw(data) {
1404
+ this.ensureConnected();
1405
+ let id;
1406
+ try {
1407
+ const parsed = JSON.parse(data);
1408
+ if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
1409
+ } catch {
1410
+ }
1411
+ if (id !== void 0) return this.postAndAwait(id, data);
1412
+ const result = await this.post(data);
1413
+ if (!result.ok) {
1414
+ return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
1415
+ }
1416
+ return { jsonrpc: "2.0", id: null, result: void 0 };
1417
+ }
1418
+ async close() {
1419
+ this.closeStream("Transport closed");
1420
+ this.state = "disconnected";
1421
+ this.sessionId = void 0;
1422
+ this.messageEndpoint = void 0;
1423
+ this.log("SSE transport closed");
1424
+ }
1425
+ isConnected() {
1426
+ return this.state === "connected";
1427
+ }
1428
+ getState() {
1429
+ return this.state;
1430
+ }
1431
+ getSessionId() {
1432
+ return this.sessionId;
1433
+ }
1434
+ setAuthToken(token) {
1435
+ this.authToken = token;
1436
+ }
1437
+ setTimeout(ms) {
1438
+ this.timeout = ms;
1439
+ }
1440
+ getMessageEndpoint() {
1441
+ return this.messageEndpoint;
1442
+ }
1443
+ getConnectionCount() {
1444
+ return this.connectionCount;
1445
+ }
1446
+ getReconnectCount() {
1447
+ return this.reconnectCount;
1448
+ }
1449
+ getLastRequestHeaders() {
1450
+ return { ...this.lastRequestHeaders };
1451
+ }
1452
+ setInterceptors(interceptors2) {
1453
+ this.interceptors = interceptors2;
1454
+ }
1455
+ getInterceptors() {
1456
+ return this.interceptors;
1457
+ }
1458
+ setElicitationHandler(handler) {
1459
+ this.elicitationHandler = handler;
1460
+ }
1461
+ async simulateDisconnect() {
1462
+ this.closeStream("Simulated disconnect");
1463
+ this.state = "disconnected";
1464
+ this.sessionId = void 0;
1465
+ this.messageEndpoint = void 0;
1466
+ }
1467
+ async waitForReconnect(timeoutMs) {
1468
+ this.reconnectCount++;
1469
+ let timer;
1470
+ try {
1471
+ await Promise.race([
1472
+ this.connect(),
1473
+ new Promise((_, reject) => {
1474
+ timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
1475
+ })
1476
+ ]);
1477
+ } finally {
1478
+ clearTimeout(timer);
1479
+ }
1480
+ }
1481
+ // ═══════════════════════════════════════════════════════════════════
1482
+ // STREAM
1483
+ // ═══════════════════════════════════════════════════════════════════
1484
+ /** Open `GET <entry>/sse` and wait for the `endpoint` event. */
1485
+ async openStream() {
1486
+ const controller = new AbortController();
1487
+ this.stream = controller;
1488
+ const url = this.sseUrl();
1489
+ const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
1490
+ this.log(`GET ${url}`);
1491
+ const connectTimer = setTimeout(() => controller.abort(), this.timeout);
1492
+ let response;
1493
+ try {
1494
+ response = await fetch(url, {
1495
+ method: "GET",
1496
+ headers: { ...headers, Accept: "text/event-stream" },
1497
+ signal: controller.signal
1498
+ });
1499
+ } catch (error) {
1500
+ clearTimeout(connectTimer);
1501
+ throw new Error(
1502
+ isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
1503
+ { cause: error }
1504
+ );
1505
+ }
1506
+ const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
1507
+ if (!response.ok || !response.body || !isEventStream) {
1508
+ clearTimeout(connectTimer);
1509
+ const body = await response.text().catch(() => "");
1510
+ throw new Error(
1511
+ `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 } } })).`
1512
+ );
1513
+ }
1514
+ const endpoint = new Promise((resolve, reject) => {
1515
+ const reader = response.body?.getReader();
1516
+ if (!reader) {
1517
+ reject(new Error(`SSE connection to ${url} has no body`));
1518
+ return;
1519
+ }
1520
+ void this.readStream(controller, reader, resolve, reject);
1521
+ });
1522
+ try {
1523
+ const data = await endpoint;
1524
+ const messageUrl = new URL(data, url);
1525
+ this.messageEndpoint = messageUrl.toString();
1526
+ this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
1527
+ } finally {
1528
+ clearTimeout(connectTimer);
1529
+ }
1530
+ }
1531
+ async readStream(controller, reader, onEndpoint, onEndpointError) {
1532
+ const decoder = new TextDecoder();
1533
+ let buffer = "";
1534
+ let endpointSeen = false;
1535
+ try {
1536
+ for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
1537
+ const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
1538
+ buffer = remainder;
1539
+ for (const text of completeEvents) {
1540
+ const event = parseSSEEvent(text);
1541
+ if (!event) continue;
1542
+ if (event.type === "endpoint") {
1543
+ if (!endpointSeen) {
1544
+ endpointSeen = true;
1545
+ onEndpoint(event.data);
1546
+ }
1547
+ continue;
1548
+ }
1549
+ await this.handleMessage(event.data);
1550
+ }
1551
+ }
1552
+ } catch (error) {
1553
+ if (!controller.signal.aborted) this.log("SSE stream failed:", error);
1554
+ } finally {
1555
+ reader.releaseLock();
1556
+ }
1557
+ if (!endpointSeen) {
1558
+ onEndpointError(
1559
+ new Error(
1560
+ 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`
1561
+ )
1562
+ );
1563
+ }
1564
+ if (this.stream === controller) {
1565
+ this.stream = void 0;
1566
+ this.rejectPending("SSE stream closed by the server");
1567
+ if (this.state === "connected") this.state = "disconnected";
1568
+ }
1569
+ }
1570
+ async handleMessage(data) {
1571
+ let message;
1572
+ try {
1573
+ message = JSON.parse(data);
1574
+ } catch {
1575
+ this.log("Ignoring SSE event that is not JSON:", data);
1576
+ return;
1577
+ }
1578
+ if (!message || typeof message !== "object") return;
1579
+ if (typeof message.method === "string") {
1580
+ const request = message;
1581
+ if (request.id === void 0 || request.id === null) {
1582
+ this.notificationHandler?.(request);
1583
+ return;
1584
+ }
1585
+ await this.answerServerRequest(request);
1586
+ return;
1587
+ }
1588
+ if (message.id !== void 0 && message.id !== null) {
1589
+ const pending = this.pending.get(message.id);
1590
+ if (pending) {
1591
+ clearTimeout(pending.timer);
1592
+ this.pending.delete(message.id);
1593
+ pending.resolve(message);
1594
+ }
1595
+ }
1596
+ }
1597
+ /** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
1598
+ async answerServerRequest(request) {
1599
+ const id = request.id;
1600
+ let reply;
1601
+ if (request.method === "elicitation/create") {
1602
+ reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
1603
+ } else if (request.method === "ping") {
1604
+ reply = { jsonrpc: "2.0", id, result: {} };
1605
+ } else {
1606
+ reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
1607
+ }
1608
+ const result = await this.post(JSON.stringify(reply));
1609
+ if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
1610
+ }
1611
+ async elicit(params) {
1612
+ if (!this.elicitationHandler) return { action: "decline" };
1613
+ try {
1614
+ return await this.elicitationHandler(params);
1615
+ } catch (error) {
1616
+ this.log("Elicitation handler error:", error);
1617
+ return { action: "cancel" };
1618
+ }
1619
+ }
1620
+ closeStream(reason) {
1621
+ const controller = this.stream;
1622
+ this.stream = void 0;
1623
+ controller?.abort();
1624
+ this.rejectPending(reason);
1625
+ }
1626
+ rejectPending(reason) {
1627
+ for (const [id, pending] of this.pending) {
1628
+ clearTimeout(pending.timer);
1629
+ pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
1630
+ }
1631
+ this.pending.clear();
1632
+ }
1633
+ // ═══════════════════════════════════════════════════════════════════
1634
+ // POSTING
1635
+ // ═══════════════════════════════════════════════════════════════════
1636
+ /** POST a message and resolve with the response the stream carries for `id`. */
1637
+ async postAndAwait(id, body) {
1638
+ const answer = new Promise((resolve) => {
1639
+ const timer = setTimeout(() => {
1640
+ this.pending.delete(id);
1641
+ resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
1642
+ }, this.timeout);
1643
+ this.pending.set(id, { resolve, timer });
1644
+ });
1645
+ const result = await this.post(body);
1646
+ if (!result.ok) {
1647
+ const pending = this.pending.get(id);
1648
+ if (pending) {
1649
+ clearTimeout(pending.timer);
1650
+ this.pending.delete(id);
1651
+ }
1652
+ return {
1653
+ jsonrpc: "2.0",
1654
+ id,
1655
+ error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
1656
+ };
1657
+ }
1658
+ return answer;
1659
+ }
1660
+ async post(body) {
1661
+ const endpoint = this.messageEndpoint;
1662
+ if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
1663
+ const headers = this.buildHeaders();
1664
+ this.lastRequestHeaders = headers;
1665
+ this.log(`POST ${endpoint}`, body);
1666
+ const controller = new AbortController();
1667
+ const timer = setTimeout(() => controller.abort(), this.timeout);
1668
+ try {
1669
+ const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
1670
+ const text = await response.text();
1671
+ return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
1672
+ } catch (error) {
1673
+ if (isAbortError(error)) {
1674
+ return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
1675
+ }
1676
+ throw error;
1677
+ } finally {
1678
+ clearTimeout(timer);
1679
+ }
1680
+ }
1681
+ // ═══════════════════════════════════════════════════════════════════
1682
+ // HELPERS
1683
+ // ═══════════════════════════════════════════════════════════════════
1684
+ async requestAnonymousToken() {
1685
+ const url = new URL(this.baseUrl);
1686
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
1687
+ url.search = "";
1688
+ const tokenUrl = url.toString();
1689
+ try {
1690
+ const response = await fetch(tokenUrl, {
1691
+ method: "POST",
1692
+ headers: { "Content-Type": "application/json" },
1693
+ body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
1694
+ });
1695
+ if (!response.ok) {
1696
+ this.log(`No anonymous token (HTTP ${response.status})`);
1697
+ return;
1698
+ }
1699
+ const token = await response.json();
1700
+ if (token.access_token) this.authToken = token.access_token;
1701
+ } catch (error) {
1702
+ this.log("Error requesting anonymous token:", error);
1703
+ }
1704
+ }
1705
+ /** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
1706
+ sseUrl() {
1707
+ const url = new URL(this.baseUrl);
1708
+ const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
1709
+ const basePath = url.pathname.replace(/\/+$/, "");
1710
+ url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
1711
+ return url.toString();
1712
+ }
1713
+ buildHeaders() {
1714
+ const headers = {
1715
+ "Content-Type": "application/json",
1716
+ Accept: "application/json, text/event-stream"
1717
+ };
1718
+ if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
1719
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1720
+ if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
1721
+ Object.assign(headers, this.authHeaders);
1722
+ return headers;
1723
+ }
1724
+ ensureConnected() {
1725
+ if (this.state !== "connected") {
1726
+ throw new Error("Transport not connected. Call connect() first.");
1727
+ }
1728
+ }
1729
+ log(message, data) {
1730
+ if (this.debug) {
1731
+ console.log(`[SSE] ${message}`, data ?? "");
1732
+ }
1733
+ }
1734
+ };
1735
+ function splitSSEEvents(buffer) {
1736
+ const endsWithCarriageReturn = buffer.endsWith("\r");
1737
+ const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1738
+ const completeEvents = normalized.split("\n\n");
1739
+ const remainder = completeEvents.pop() ?? "";
1740
+ return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
1741
+ }
1742
+ function parseSSEEvent(text) {
1743
+ let type = "message";
1744
+ const data = [];
1745
+ for (const line of text.split("\n")) {
1746
+ if (line.startsWith(":")) continue;
1747
+ const colon = line.indexOf(":");
1748
+ const field = colon === -1 ? line : line.slice(0, colon);
1749
+ const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
1750
+ if (field === "event") type = value;
1751
+ else if (field === "data") data.push(value);
1752
+ }
1753
+ return data.length > 0 ? { type, data: data.join("\n") } : void 0;
1754
+ }
1755
+
1756
+ // libs/testing/src/transport/streamable-http.transport.ts
1757
+ var DEFAULT_TIMEOUT2 = 3e4;
1111
1758
  var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
1112
1759
  var StreamableHttpTransport = class {
1113
1760
  config;
@@ -1129,7 +1776,7 @@ var StreamableHttpTransport = class {
1129
1776
  baseUrl: config.baseUrl.replace(/\/$/, ""),
1130
1777
  // Remove trailing slash
1131
1778
  entryPath: config.entryPath,
1132
- timeout: config.timeout ?? DEFAULT_TIMEOUT,
1779
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
1133
1780
  auth: config.auth ?? {},
1134
1781
  publicMode: config.publicMode ?? false,
1135
1782
  debug: config.debug ?? false,
@@ -1214,7 +1861,7 @@ var StreamableHttpTransport = class {
1214
1861
  return mockResponse2;
1215
1862
  }
1216
1863
  case "error":
1217
- throw interceptResult.error;
1864
+ throw markInterceptedError(interceptResult.error);
1218
1865
  case "continue":
1219
1866
  message = interceptResult.request;
1220
1867
  break;
@@ -1275,7 +1922,7 @@ var StreamableHttpTransport = class {
1275
1922
  return jsonResponse;
1276
1923
  } catch (error) {
1277
1924
  clearTimeout(timeoutId);
1278
- if (error instanceof Error && error.name === "AbortError") {
1925
+ if (isAbortError(error)) {
1279
1926
  return {
1280
1927
  jsonrpc: "2.0",
1281
1928
  id: message.id ?? null,
@@ -1314,7 +1961,7 @@ var StreamableHttpTransport = class {
1314
1961
  }
1315
1962
  } catch (error) {
1316
1963
  clearTimeout(timeoutId);
1317
- if (error instanceof Error && error.name !== "AbortError") {
1964
+ if (!isAbortError(error)) {
1318
1965
  throw error;
1319
1966
  }
1320
1967
  }
@@ -1355,7 +2002,7 @@ var StreamableHttpTransport = class {
1355
2002
  error: {
1356
2003
  code: -32700,
1357
2004
  message: "Parse error",
1358
- data: error instanceof Error ? error.message : "Unknown error"
2005
+ data: errorMessage(error)
1359
2006
  }
1360
2007
  };
1361
2008
  }
@@ -1469,7 +2116,7 @@ var StreamableHttpTransport = class {
1469
2116
  let buffer = "";
1470
2117
  try {
1471
2118
  for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
1472
- const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
2119
+ const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
1473
2120
  buffer = remainder;
1474
2121
  for (const eventText of completeEvents) {
1475
2122
  for (const event of this.parseSSEEvents(eventText, void 0).events) {
@@ -1725,7 +2372,7 @@ var StreamableHttpTransport = class {
1725
2372
  if (this.config.clientInfo) {
1726
2373
  headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
1727
2374
  }
1728
- if (this.authToken && !this.publicMode) {
2375
+ if (this.authToken) {
1729
2376
  headers["Authorization"] = `Bearer ${this.authToken}`;
1730
2377
  }
1731
2378
  if (this.sessionId) {
@@ -1803,7 +2450,7 @@ var StreamableHttpTransport = class {
1803
2450
  };
1804
2451
  }
1805
2452
  };
1806
- function splitSSEEvents(buffer) {
2453
+ function splitSSEEvents2(buffer) {
1807
2454
  const endsWithCarriageReturn = buffer.endsWith("\r");
1808
2455
  const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
1809
2456
  const completeEvents = normalized.split("\n\n");
@@ -2041,7 +2688,7 @@ var McpTestClientBuilder = class {
2041
2688
  };
2042
2689
 
2043
2690
  // libs/testing/src/client/mcp-test-client.ts
2044
- var DEFAULT_TIMEOUT2 = 3e4;
2691
+ var DEFAULT_TIMEOUT3 = 3e4;
2045
2692
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
2046
2693
  var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
2047
2694
  var DEFAULT_CLIENT_INFO = {
@@ -2049,6 +2696,15 @@ var DEFAULT_CLIENT_INFO = {
2049
2696
  version: "0.4.0"
2050
2697
  };
2051
2698
  var MAX_LIST_PAGES = 1e3;
2699
+ function isAuthFailure(error) {
2700
+ return typeof error?.message === "string" && /^HTTP (401|403)\b/.test(error.message);
2701
+ }
2702
+ function describeInitializeError(error) {
2703
+ const message = error?.message ?? "Unknown error";
2704
+ const data = error?.data;
2705
+ const detail = typeof data === "string" ? data.trim() : "";
2706
+ return detail ? `${message} - ${detail.slice(0, 500)}` : message;
2707
+ }
2052
2708
  function extractEntryPaths(data) {
2053
2709
  let payload = data;
2054
2710
  if (typeof payload === "string") {
@@ -2102,7 +2758,7 @@ var McpTestClient = class {
2102
2758
  transport: config.transport ?? "streamable-http",
2103
2759
  auth: config.auth ?? {},
2104
2760
  publicMode: config.publicMode ?? false,
2105
- timeout: config.timeout ?? DEFAULT_TIMEOUT2,
2761
+ timeout: config.timeout ?? DEFAULT_TIMEOUT3,
2106
2762
  debug: config.debug ?? false,
2107
2763
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
2108
2764
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
@@ -2135,19 +2791,22 @@ var McpTestClient = class {
2135
2791
  async connect() {
2136
2792
  this.log("debug", `Connecting to ${this.config.baseUrl}...`);
2137
2793
  this.triedDiscoveredEntryPath = false;
2794
+ this.initResult = null;
2138
2795
  this.transport = this.createTransport();
2139
2796
  await this.transport.connect();
2140
2797
  const maxRetries = 3;
2141
2798
  const retryDelayMs = 500;
2142
2799
  let lastError;
2143
2800
  let reportedEntryPaths;
2801
+ let attemptsMade = 0;
2144
2802
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
2803
+ attemptsMade = attempt;
2145
2804
  const initResponse = await this.initialize();
2146
2805
  if (initResponse.success && initResponse.data) {
2147
2806
  this.initResult = initResponse.data;
2148
2807
  break;
2149
2808
  }
2150
- lastError = initResponse.error?.message ?? "Unknown error";
2809
+ lastError = describeInitializeError(initResponse.error);
2151
2810
  reportedEntryPaths = extractEntryPaths(initResponse.error?.data) ?? reportedEntryPaths;
2152
2811
  if (reportedEntryPaths?.length === 1 && !this.triedDiscoveredEntryPath) {
2153
2812
  this.triedDiscoveredEntryPath = true;
@@ -2159,6 +2818,7 @@ var McpTestClient = class {
2159
2818
  await this.transport.connect();
2160
2819
  continue;
2161
2820
  }
2821
+ if (isAuthFailure(initResponse.error)) break;
2162
2822
  if (attempt < maxRetries) {
2163
2823
  this.log("debug", `MCP init attempt ${attempt} failed (${lastError}), retrying in ${retryDelayMs}ms...`);
2164
2824
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
@@ -2167,7 +2827,7 @@ var McpTestClient = class {
2167
2827
  if (!this.initResult) {
2168
2828
  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.` : "";
2169
2829
  throw new Error(
2170
- `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${maxRetries} attempts: ${lastError}.${served}`
2830
+ `Failed to initialize MCP connection to ${this.config.baseUrl}${this.mcpPathLabel()} after ${attemptsMade} attempt${attemptsMade === 1 ? "" : "s"}: ${lastError}.${served}`
2171
2831
  );
2172
2832
  }
2173
2833
  this._sessionId = this.transport.getSessionId();
@@ -2237,18 +2897,47 @@ var McpTestClient = class {
2237
2897
  return this._authState;
2238
2898
  }
2239
2899
  /**
2240
- * Authenticate with a token
2900
+ * Authenticate with a token.
2901
+ *
2902
+ * When connected, this opens a NEW session with the token (the previous session was created
2903
+ * for the previous identity) and rejects if the server refuses it — for example because the
2904
+ * token is expired or badly signed. On rejection the client keeps its previous identity and
2905
+ * session. The token is kept in the client config, so `reconnect()` keeps using it.
2241
2906
  */
2242
2907
  async authenticate(token) {
2908
+ if (typeof token !== "string" || token.trim() === "") {
2909
+ throw new Error("authenticate() requires a non-empty token");
2910
+ }
2911
+ const previousAuthState = this._authState;
2912
+ const previousConfigAuth = this.config.auth;
2913
+ this.config.auth = { ...previousConfigAuth, token };
2243
2914
  this._authState = {
2244
2915
  isAnonymous: false,
2245
2916
  token,
2246
2917
  scopes: this.parseScopesFromToken(token),
2247
2918
  user: this.parseUserFromToken(token)
2248
2919
  };
2249
- if (this.transport) {
2250
- this.transport.setAuthToken(token);
2920
+ const previousTransport = this.transport;
2921
+ if (!previousTransport) {
2922
+ this.log("debug", "Authentication updated (not connected yet)");
2923
+ return;
2251
2924
  }
2925
+ const previousInit = this.initResult;
2926
+ const previousSessionId = this._sessionId;
2927
+ const previousSessionInfo = this._sessionInfo;
2928
+ try {
2929
+ await this.connect();
2930
+ } catch (err) {
2931
+ await this.transport?.close().catch(() => void 0);
2932
+ this.transport = previousTransport;
2933
+ this.initResult = previousInit;
2934
+ this._sessionId = previousSessionId;
2935
+ this._sessionInfo = previousSessionInfo;
2936
+ this.config.auth = previousConfigAuth;
2937
+ this._authState = previousAuthState;
2938
+ throw err;
2939
+ }
2940
+ await previousTransport.close().catch(() => void 0);
2252
2941
  this.log("debug", "Authentication updated");
2253
2942
  }
2254
2943
  // ═══════════════════════════════════════════════════════════════════
@@ -2834,8 +3523,19 @@ var McpTestClient = class {
2834
3523
  elicitationHandler: this._elicitationHandler,
2835
3524
  notificationHandler: this.recordNotification
2836
3525
  });
2837
- case "sse":
2838
- throw new Error("SSE transport not yet implemented");
3526
+ case "sse":
3527
+ return new SseTransport({
3528
+ baseUrl,
3529
+ entryPath: this.config.entryPath,
3530
+ timeout: this.config.timeout,
3531
+ auth: this.config.auth,
3532
+ publicMode: this.config.publicMode,
3533
+ debug: this.config.debug,
3534
+ interceptors: this._interceptors,
3535
+ clientInfo: this.config.clientInfo,
3536
+ elicitationHandler: this._elicitationHandler,
3537
+ notificationHandler: this.recordNotification
3538
+ });
2839
3539
  default:
2840
3540
  throw new Error(`Unknown transport type: ${this.config.transport}`);
2841
3541
  }
@@ -2872,10 +3572,11 @@ var McpTestClient = class {
2872
3572
  requestId: id
2873
3573
  };
2874
3574
  } catch (err) {
3575
+ if (isInterceptedError(err)) throw err;
2875
3576
  const durationMs = Date.now() - start;
2876
3577
  const error = {
2877
3578
  code: -32603,
2878
- message: err instanceof Error ? err.message : "Unknown error"
3579
+ message: errorMessage(err)
2879
3580
  };
2880
3581
  return {
2881
3582
  success: false,
@@ -3102,190 +3803,6 @@ var ProgressCollector = class {
3102
3803
  }
3103
3804
  };
3104
3805
 
3105
- // libs/testing/src/auth/token-factory.ts
3106
- var import_jose = require("jose");
3107
- var TestTokenFactory = class {
3108
- issuer;
3109
- audience;
3110
- privateKey = null;
3111
- publicKey = null;
3112
- jwk = null;
3113
- keyId;
3114
- /** HS256 shared secret (gateway mode). Null → RS256 + JWKS (default). */
3115
- hmacSecret;
3116
- constructor(options = {}) {
3117
- this.issuer = options.issuer ?? "https://test.frontmcp.local";
3118
- this.audience = options.audience ?? "frontmcp-test";
3119
- this.keyId = `test-key-${Date.now()}`;
3120
- this.hmacSecret = options.hmacSecret ? new TextEncoder().encode(options.hmacSecret) : null;
3121
- }
3122
- /** True when this factory signs HS256 with a shared secret (gateway mode). */
3123
- get isHmac() {
3124
- return this.hmacSecret !== null;
3125
- }
3126
- /** The JWS `alg` this factory signs with. */
3127
- get alg() {
3128
- return this.isHmac ? "HS256" : "RS256";
3129
- }
3130
- /**
3131
- * The signing key: the shared HMAC secret in gateway mode, otherwise the
3132
- * generated RSA private key (lazily created via ensureKeys()).
3133
- */
3134
- signingKey() {
3135
- if (this.hmacSecret) return this.hmacSecret;
3136
- if (!this.privateKey) {
3137
- throw new Error("Private key not initialized");
3138
- }
3139
- return this.privateKey;
3140
- }
3141
- /**
3142
- * Initialize the key pair (called automatically on first use). No-op in HS256
3143
- * mode — there is no asymmetric keypair, just the shared secret.
3144
- */
3145
- async ensureKeys() {
3146
- if (this.isHmac) return;
3147
- if (this.privateKey && this.publicKey) return;
3148
- const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
3149
- extractable: true
3150
- });
3151
- this.privateKey = privateKey;
3152
- this.publicKey = publicKey;
3153
- this.jwk = await (0, import_jose.exportJWK)(publicKey);
3154
- this.jwk.kid = this.keyId;
3155
- this.jwk.use = "sig";
3156
- this.jwk.alg = "RS256";
3157
- }
3158
- /**
3159
- * Create a JWT token with the specified claims
3160
- */
3161
- async createTestToken(options) {
3162
- await this.ensureKeys();
3163
- const now = Math.floor(Date.now() / 1e3);
3164
- const exp = options.exp ?? 3600;
3165
- const payload = {
3166
- iss: options.iss ?? this.issuer,
3167
- sub: options.sub,
3168
- aud: options.aud ?? this.audience,
3169
- iat: now,
3170
- exp: now + exp,
3171
- scope: options.scopes?.join(" "),
3172
- ...options.claims
3173
- };
3174
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3175
- const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3176
- return token;
3177
- }
3178
- /**
3179
- * Create an admin token with full access
3180
- */
3181
- async createAdminToken(sub = "admin-001") {
3182
- return this.createTestToken({
3183
- sub,
3184
- scopes: ["admin:*", "read", "write", "delete"],
3185
- claims: {
3186
- email: "admin@test.local",
3187
- name: "Test Admin",
3188
- role: "admin"
3189
- }
3190
- });
3191
- }
3192
- /**
3193
- * Create a regular user token
3194
- */
3195
- async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
3196
- return this.createTestToken({
3197
- sub,
3198
- scopes,
3199
- claims: {
3200
- email: "user@test.local",
3201
- name: "Test User",
3202
- role: "user"
3203
- }
3204
- });
3205
- }
3206
- /**
3207
- * Create an anonymous user token
3208
- */
3209
- async createAnonymousToken() {
3210
- return this.createTestToken({
3211
- sub: `anon:${Date.now()}`,
3212
- scopes: ["anonymous"],
3213
- claims: {
3214
- name: "Anonymous",
3215
- role: "anonymous"
3216
- }
3217
- });
3218
- }
3219
- /**
3220
- * Create an expired token (for testing token expiration)
3221
- */
3222
- async createExpiredToken(options) {
3223
- await this.ensureKeys();
3224
- const now = Math.floor(Date.now() / 1e3);
3225
- const payload = {
3226
- iss: this.issuer,
3227
- sub: options.sub,
3228
- aud: this.audience,
3229
- iat: now - 7200,
3230
- // 2 hours ago
3231
- exp: now - 3600
3232
- // Expired 1 hour ago
3233
- };
3234
- const header = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3235
- const token = await new import_jose.SignJWT(payload).setProtectedHeader(header).sign(this.signingKey());
3236
- return token;
3237
- }
3238
- /**
3239
- * Create a token with an invalid signature (for testing signature validation)
3240
- */
3241
- createTokenWithInvalidSignature(options) {
3242
- const now = Math.floor(Date.now() / 1e3);
3243
- const headerObj = this.isHmac ? { alg: this.alg, typ: "JWT" } : { alg: this.alg, kid: this.keyId };
3244
- const header = Buffer.from(JSON.stringify(headerObj)).toString("base64url");
3245
- const payload = Buffer.from(
3246
- JSON.stringify({
3247
- iss: this.issuer,
3248
- sub: options.sub,
3249
- aud: this.audience,
3250
- iat: now,
3251
- exp: now + 3600
3252
- })
3253
- ).toString("base64url");
3254
- const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
3255
- return `${header}.${payload}.${signature}`;
3256
- }
3257
- /**
3258
- * Get the public JWKS for verifying tokens.
3259
- *
3260
- * Only meaningful in RS256 mode — HS256 (gateway) tokens are verified against
3261
- * the shared secret, not a published public key, so there is no JWKS to serve.
3262
- */
3263
- async getPublicJwks() {
3264
- if (this.isHmac) {
3265
- throw new Error("getPublicJwks() is not available in HS256 mode (gateway tokens use a shared secret)");
3266
- }
3267
- await this.ensureKeys();
3268
- if (!this.jwk) {
3269
- throw new Error("JWK not initialized");
3270
- }
3271
- return {
3272
- keys: [this.jwk]
3273
- };
3274
- }
3275
- /**
3276
- * Get the issuer URL
3277
- */
3278
- getIssuer() {
3279
- return this.issuer;
3280
- }
3281
- /**
3282
- * Get the audience
3283
- */
3284
- getAudience() {
3285
- return this.audience;
3286
- }
3287
- };
3288
-
3289
3806
  // libs/testing/src/server/test-server.ts
3290
3807
  var import_child_process = require("child_process");
3291
3808
  var import_utils = require("@frontmcp/utils");
@@ -3358,6 +3875,8 @@ var E2E_PORT_RANGES = {
3358
3875
  "demo-e2e-distributed": { start: 50440, size: 20 },
3359
3876
  // Protocol revision E2E tests (50460-50479)
3360
3877
  "demo-e2e-protocol-20260728": { start: 50460, size: 20 },
3878
+ // Testing library E2E tests (50480-50499)
3879
+ "demo-e2e-testing": { start: 50480, size: 20 },
3361
3880
  // Mock servers and utilities (50900-50999)
3362
3881
  "mock-oauth": { start: 50900, size: 10 },
3363
3882
  "mock-api": { start: 50910, size: 10 },
@@ -3366,6 +3885,66 @@ var E2E_PORT_RANGES = {
3366
3885
  default: { start: 51e3, size: 100 }
3367
3886
  };
3368
3887
  var reservedPorts = /* @__PURE__ */ new Map();
3888
+ var lockedPorts = /* @__PURE__ */ new Set();
3889
+ var LOCK_MAX_AGE_MS = 10 * 60 * 1e3;
3890
+ function lockDir() {
3891
+ try {
3892
+ const os = require("node:os");
3893
+ const path = require("node:path");
3894
+ return path.join(os.tmpdir(), "frontmcp-testing-ports");
3895
+ } catch {
3896
+ return void 0;
3897
+ }
3898
+ }
3899
+ function pidIsAlive(pid) {
3900
+ try {
3901
+ process.kill(pid, 0);
3902
+ return true;
3903
+ } catch (error) {
3904
+ return error.code === "EPERM";
3905
+ }
3906
+ }
3907
+ function acquirePortLock(port) {
3908
+ if (lockedPorts.has(port)) return false;
3909
+ let fs;
3910
+ let file;
3911
+ try {
3912
+ fs = require("node:fs");
3913
+ const dir = lockDir();
3914
+ if (!dir) return true;
3915
+ fs.mkdirSync(dir, { recursive: true });
3916
+ file = require("node:path").join(dir, `${port}.lock`);
3917
+ } catch {
3918
+ return true;
3919
+ }
3920
+ const payload = JSON.stringify({ pid: process.pid, at: Date.now() });
3921
+ for (let attempt = 0; attempt < 2; attempt++) {
3922
+ try {
3923
+ fs.writeFileSync(file, payload, { flag: "wx" });
3924
+ lockedPorts.add(port);
3925
+ return true;
3926
+ } catch (error) {
3927
+ if (error.code !== "EEXIST") return true;
3928
+ try {
3929
+ const held = JSON.parse(fs.readFileSync(file, "utf8"));
3930
+ const fresh = typeof held.at === "number" && Date.now() - held.at < LOCK_MAX_AGE_MS;
3931
+ if (fresh && typeof held.pid === "number" && pidIsAlive(held.pid)) return false;
3932
+ fs.unlinkSync(file);
3933
+ } catch {
3934
+ }
3935
+ }
3936
+ }
3937
+ return false;
3938
+ }
3939
+ function releasePortLock(port) {
3940
+ if (!lockedPorts.delete(port)) return;
3941
+ try {
3942
+ const fs = require("node:fs");
3943
+ const dir = lockDir();
3944
+ if (dir) fs.unlinkSync(require("node:path").join(dir, `${port}.lock`));
3945
+ } catch {
3946
+ }
3947
+ }
3369
3948
  var projectPortIndex = /* @__PURE__ */ new Map();
3370
3949
  function getPortRange(project) {
3371
3950
  const key = project;
@@ -3376,15 +3955,22 @@ function getPortRange(project) {
3376
3955
  }
3377
3956
  async function reservePort(project, preferredPort) {
3378
3957
  const range = getPortRange(project);
3379
- if (preferredPort !== void 0) {
3958
+ const handle = (port) => ({
3959
+ port,
3960
+ // Frees the socket only, so the server about to be spawned can bind the port; the lock file stays
3961
+ releaseSocket: async () => {
3962
+ await releasePort(port);
3963
+ },
3964
+ release: async () => {
3965
+ await releasePort(port);
3966
+ releasePortLock(port);
3967
+ }
3968
+ });
3969
+ const wantsSpecificPort = preferredPort !== void 0 && Number.isInteger(preferredPort) && preferredPort > 0;
3970
+ if (wantsSpecificPort) {
3380
3971
  const reservation = await tryReservePort(preferredPort, project);
3381
3972
  if (reservation) {
3382
- return {
3383
- port: preferredPort,
3384
- release: async () => {
3385
- await releasePort(preferredPort);
3386
- }
3387
- };
3973
+ return handle(preferredPort);
3388
3974
  }
3389
3975
  console.warn(`[PortRegistry] Preferred port ${preferredPort} not available for ${project}, allocating from range`);
3390
3976
  }
@@ -3393,30 +3979,19 @@ async function reservePort(project, preferredPort) {
3393
3979
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
3394
3980
  const port = range.start + index % range.size;
3395
3981
  index = (index + 1) % range.size;
3396
- if (reservedPorts.has(port)) {
3982
+ if (reservedPorts.has(port) || lockedPorts.has(port)) {
3397
3983
  continue;
3398
3984
  }
3399
3985
  const reservation = await tryReservePort(port, project);
3400
3986
  if (reservation) {
3401
3987
  projectPortIndex.set(project, index);
3402
- return {
3403
- port,
3404
- release: async () => {
3405
- await releasePort(port);
3406
- }
3407
- };
3988
+ return handle(port);
3408
3989
  }
3409
3990
  }
3410
- const dynamicPort = await findAvailablePortInRange(51e3, 52e3);
3411
- if (dynamicPort) {
3412
- const reservation = await tryReservePort(dynamicPort, project);
3413
- if (reservation) {
3414
- return {
3415
- port: dynamicPort,
3416
- release: async () => {
3417
- await releasePort(dynamicPort);
3418
- }
3419
- };
3991
+ for (let port = 51e3; port < 52e3; port++) {
3992
+ if (reservedPorts.has(port) || lockedPorts.has(port)) continue;
3993
+ if (await tryReservePort(port, project)) {
3994
+ return handle(port);
3420
3995
  }
3421
3996
  }
3422
3997
  throw new Error(
@@ -3424,9 +3999,13 @@ async function reservePort(project, preferredPort) {
3424
3999
  );
3425
4000
  }
3426
4001
  async function tryReservePort(port, project) {
4002
+ if (!acquirePortLock(port)) {
4003
+ return false;
4004
+ }
3427
4005
  return new Promise((resolve) => {
3428
4006
  const server = (0, import_net.createServer)();
3429
4007
  server.once("error", () => {
4008
+ releasePortLock(port);
3430
4009
  resolve(false);
3431
4010
  });
3432
4011
  server.listen(port, () => {
@@ -3452,31 +4031,6 @@ async function releasePort(port) {
3452
4031
  });
3453
4032
  });
3454
4033
  }
3455
- async function findAvailablePortInRange(start, end) {
3456
- for (let port = start; port < end; port++) {
3457
- if (reservedPorts.has(port)) {
3458
- continue;
3459
- }
3460
- const available = await isPortAvailable(port);
3461
- if (available) {
3462
- return port;
3463
- }
3464
- }
3465
- return null;
3466
- }
3467
- async function isPortAvailable(port) {
3468
- return new Promise((resolve) => {
3469
- const server = (0, import_net.createServer)();
3470
- server.once("error", () => {
3471
- resolve(false);
3472
- });
3473
- server.listen(port, () => {
3474
- server.close(() => {
3475
- resolve(true);
3476
- });
3477
- });
3478
- });
3479
- }
3480
4034
 
3481
4035
  // libs/testing/src/server/test-server.ts
3482
4036
  var DEBUG_SERVER = process.env["DEBUG_SERVER"] === "1" || process.env["DEBUG"] === "1";
@@ -3485,8 +4039,11 @@ var TestServer = class _TestServer {
3485
4039
  options;
3486
4040
  _info;
3487
4041
  logs = [];
4042
+ /** Frees the port reservation (socket and cross-process lock); held until stop() */
3488
4043
  portRelease = null;
3489
- constructor(options, port, portRelease) {
4044
+ /** Frees only the reservation socket, just before the server is spawned */
4045
+ portReleaseSocket = null;
4046
+ constructor(options, port, portRelease, portReleaseSocket) {
3490
4047
  this.options = {
3491
4048
  port,
3492
4049
  project: options.project,
@@ -3498,6 +4055,7 @@ var TestServer = class _TestServer {
3498
4055
  debug: options.debug ?? DEBUG_SERVER
3499
4056
  };
3500
4057
  this.portRelease = portRelease ?? null;
4058
+ this.portReleaseSocket = portReleaseSocket ?? portRelease ?? null;
3501
4059
  this._info = {
3502
4060
  baseUrl: `http://localhost:${port}`,
3503
4061
  port
@@ -3510,8 +4068,8 @@ var TestServer = class _TestServer {
3510
4068
  const project = options.project ?? "default";
3511
4069
  const maxAttempts = 3;
3512
4070
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3513
- const { port, release } = await reservePort(project, options.port);
3514
- const server = new _TestServer(options, port, release);
4071
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
4072
+ const server = new _TestServer(options, port, release, releaseSocket);
3515
4073
  try {
3516
4074
  await server.startProcess();
3517
4075
  return server;
@@ -3551,7 +4109,7 @@ var TestServer = class _TestServer {
3551
4109
  }
3552
4110
  const maxAttempts = 3;
3553
4111
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3554
- const { port, release } = await reservePort(project, options.port);
4112
+ const { port, release, releaseSocket } = await reservePort(project, options.port);
3555
4113
  const serverOptions = {
3556
4114
  ...options,
3557
4115
  port,
@@ -3559,7 +4117,7 @@ var TestServer = class _TestServer {
3559
4117
  command: `npx nx serve ${project} --port ${port}`,
3560
4118
  cwd: options.cwd ?? process.cwd()
3561
4119
  };
3562
- const server = new _TestServer(serverOptions, port, release);
4120
+ const server = new _TestServer(serverOptions, port, release, releaseSocket);
3563
4121
  try {
3564
4122
  await server.startProcess();
3565
4123
  return server;
@@ -3617,10 +4175,17 @@ var TestServer = class _TestServer {
3617
4175
  * Stop the test server
3618
4176
  */
3619
4177
  async stop() {
3620
- if (this.portRelease) {
3621
- await this.portRelease();
3622
- this.portRelease = null;
4178
+ try {
4179
+ await this.stopProcess();
4180
+ } finally {
4181
+ if (this.portRelease) {
4182
+ await this.portRelease();
4183
+ this.portRelease = null;
4184
+ }
4185
+ this.portReleaseSocket = null;
3623
4186
  }
4187
+ }
4188
+ async stopProcess() {
3624
4189
  if (this.process) {
3625
4190
  this.log("Stopping server...");
3626
4191
  if (this.process.exitCode !== null || this.process.signalCode !== null) {
@@ -3693,7 +4258,7 @@ var TestServer = class _TestServer {
3693
4258
  * Restart the server
3694
4259
  */
3695
4260
  async restart() {
3696
- await this.stop();
4261
+ await this.stopProcess();
3697
4262
  await this.startProcess();
3698
4263
  }
3699
4264
  /**
@@ -3723,9 +4288,9 @@ var TestServer = class _TestServer {
3723
4288
  PORT: String(this.options.port)
3724
4289
  };
3725
4290
  const runtimeEnv = withWorkspaceProtocolFallback(env, this.options.cwd);
3726
- if (this.portRelease) {
3727
- await this.portRelease();
3728
- this.portRelease = null;
4291
+ if (this.portReleaseSocket) {
4292
+ await this.portReleaseSocket();
4293
+ this.portReleaseSocket = null;
3729
4294
  await sleep4(300);
3730
4295
  }
3731
4296
  this.process = (0, import_child_process.spawn)(this.options.command, [], {
@@ -3735,9 +4300,8 @@ var TestServer = class _TestServer {
3735
4300
  detached: true,
3736
4301
  stdio: ["pipe", "pipe", "pipe"]
3737
4302
  });
3738
- if (this.process.pid !== void 0) {
3739
- this._info.pid = this.process.pid;
3740
- }
4303
+ this._info.pid = this.process.pid;
4304
+ this._info.shellPid = this.process.pid;
3741
4305
  let processExited = false;
3742
4306
  let exitCode = null;
3743
4307
  let exitError = null;
@@ -3840,6 +4404,8 @@ ${allLogs}`);
3840
4404
  });
3841
4405
  if (response.ok || response.status === 404) {
3842
4406
  this.log(`Server is ready after ${healthCheckAttempts} health check attempts`);
4407
+ const listeningPid = await findListeningPid(this.options.port);
4408
+ if (listeningPid !== void 0) this._info.pid = listeningPid;
3843
4409
  return;
3844
4410
  }
3845
4411
  lastHealthCheckError = `HTTP ${response.status}: ${response.statusText}`;
@@ -3884,6 +4450,19 @@ TIP: Set DEBUG_SERVER=1 or DEBUG=1 environment variable for verbose output`
3884
4450
  function sleep4(ms) {
3885
4451
  return new Promise((resolve) => setTimeout(resolve, ms));
3886
4452
  }
4453
+ function findListeningPid(port) {
4454
+ if (process.platform === "win32") return Promise.resolve(void 0);
4455
+ return new Promise((resolve) => {
4456
+ (0, import_child_process.execFile)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { timeout: 2e3 }, (error, stdout) => {
4457
+ if (error) {
4458
+ resolve(void 0);
4459
+ return;
4460
+ }
4461
+ const pid = parseInt(String(stdout).split("\n")[0] ?? "", 10);
4462
+ resolve(Number.isInteger(pid) && pid > 0 ? pid : void 0);
4463
+ });
4464
+ });
4465
+ }
3887
4466
  function withWorkspaceProtocolFallback(env, cwd) {
3888
4467
  if (findInstalledProtocolPackageDir(cwd)) {
3889
4468
  return env;
@@ -4031,7 +4610,8 @@ Server entry: ${currentConfig.server}
4031
4610
  Project: ${currentConfig.project ?? "default"}
4032
4611
  Command: ${serverCommand}
4033
4612
 
4034
- Error: ${errMsg}`
4613
+ Error: ${errMsg}`,
4614
+ { cause: error }
4035
4615
  );
4036
4616
  }
4037
4617
  } else {
@@ -4221,16 +4801,19 @@ function todo(name) {
4221
4801
  }
4222
4802
  var perfTest = perfTestWithFixtures;
4223
4803
  perfTest.use = use;
4224
- perfTest.describe = describe;
4225
- perfTest.beforeAll = beforeAll;
4226
- perfTest.beforeEach = beforeEach;
4227
- perfTest.afterEach = afterEach;
4228
- perfTest.afterAll = afterAll;
4804
+ for (const hook of ["describe", "beforeAll", "beforeEach", "afterEach", "afterAll"]) {
4805
+ Object.defineProperty(perfTest, hook, {
4806
+ get: () => globalThis[hook],
4807
+ enumerable: true,
4808
+ configurable: true
4809
+ });
4810
+ }
4229
4811
  perfTest.skip = skip;
4230
4812
  perfTest.only = only;
4231
4813
  perfTest.todo = todo;
4232
4814
 
4233
4815
  // libs/testing/src/perf/baseline-store.ts
4816
+ var import_utils2 = require("@frontmcp/utils");
4234
4817
  var BASELINE_START_MARKER = "<!-- PERF_BASELINE_START -->";
4235
4818
  var BASELINE_END_MARKER = "<!-- PERF_BASELINE_END -->";
4236
4819
  var DEFAULT_BASELINE_PATH = "perf-results/baseline.json";
@@ -4245,11 +4828,10 @@ var BaselineStore = class {
4245
4828
  */
4246
4829
  async load() {
4247
4830
  try {
4248
- const { readFile, fileExists } = await import("@frontmcp/utils");
4249
- if (!await fileExists(this.baselinePath)) {
4831
+ if (!await (0, import_utils2.fileExists)(this.baselinePath)) {
4250
4832
  return null;
4251
4833
  }
4252
- const content = await readFile(this.baselinePath);
4834
+ const content = await (0, import_utils2.readFile)(this.baselinePath);
4253
4835
  this.baseline = JSON.parse(content);
4254
4836
  return this.baseline;
4255
4837
  } catch {
@@ -4260,10 +4842,9 @@ var BaselineStore = class {
4260
4842
  * Save baseline to local file.
4261
4843
  */
4262
4844
  async save(baseline) {
4263
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4264
4845
  const dir = this.baselinePath.substring(0, this.baselinePath.lastIndexOf("/"));
4265
- await ensureDir(dir);
4266
- await writeFile(this.baselinePath, JSON.stringify(baseline, null, 2));
4846
+ await (0, import_utils2.ensureDir)(dir);
4847
+ await (0, import_utils2.writeFile)(this.baselinePath, JSON.stringify(baseline, null, 2));
4267
4848
  this.baseline = baseline;
4268
4849
  }
4269
4850
  /**
@@ -4539,6 +5120,7 @@ function resetRegressionDetector() {
4539
5120
  }
4540
5121
 
4541
5122
  // libs/testing/src/perf/report-generator.ts
5123
+ var import_utils3 = require("@frontmcp/utils");
4542
5124
  var ReportGenerator = class {
4543
5125
  detector;
4544
5126
  constructor() {
@@ -4794,14 +5376,13 @@ var ReportGenerator = class {
4794
5376
  }
4795
5377
  };
4796
5378
  async function saveReports(measurements, outputDir, baseline, gitInfo) {
4797
- const { writeFile, ensureDir } = await import("@frontmcp/utils");
4798
- await ensureDir(outputDir);
5379
+ await (0, import_utils3.ensureDir)(outputDir);
4799
5380
  const generator = new ReportGenerator();
4800
5381
  const report = generator.generateReport(measurements, baseline, gitInfo);
4801
5382
  const jsonPath = `${outputDir}/report.json`;
4802
5383
  const markdownPath = `${outputDir}/report.md`;
4803
- await writeFile(jsonPath, generator.generateJsonReport(report));
4804
- await writeFile(markdownPath, generator.generateMarkdownReport(report));
5384
+ await (0, import_utils3.writeFile)(jsonPath, generator.generateJsonReport(report));
5385
+ await (0, import_utils3.writeFile)(markdownPath, generator.generateMarkdownReport(report));
4805
5386
  return { jsonPath, markdownPath };
4806
5387
  }
4807
5388
  function createReportGenerator() {