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