@frontmcp/testing 1.8.7 → 1.9.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/mcp-test-client.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +524 -31
- package/esm/index.mjs +525 -31
- package/esm/matchers/index.mjs +3 -0
- package/esm/package.json +8 -6
- package/esm/perf/index.mjs +466 -7
- package/esm/perf/perf-setup.mjs +3 -0
- package/esm/playwright/index.mjs +3 -0
- package/esm/setup.mjs +3 -0
- package/fixtures/fixture-types.d.ts +19 -6
- package/fixtures/fixture-types.d.ts.map +1 -1
- package/fixtures/index.d.ts +1 -1
- package/fixtures/index.d.ts.map +1 -1
- package/fixtures/index.js +521 -31
- package/fixtures/test-fixture.d.ts.map +1 -1
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js +523 -31
- package/package.json +8 -6
- package/perf/index.js +463 -7
- package/transport/index.d.ts +1 -0
- package/transport/index.d.ts.map +1 -1
- package/transport/sse.transport.d.ts +80 -0
- package/transport/sse.transport.d.ts.map +1 -0
package/esm/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
1
4
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
5
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
6
|
}) : x)(function(x) {
|
|
@@ -367,8 +370,453 @@ function isInterceptedError(err) {
|
|
|
367
370
|
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
368
371
|
}
|
|
369
372
|
|
|
370
|
-
// libs/testing/src/transport/
|
|
373
|
+
// libs/testing/src/transport/sse.transport.ts
|
|
371
374
|
var DEFAULT_TIMEOUT = 3e4;
|
|
375
|
+
var SseTransport = class {
|
|
376
|
+
baseUrl;
|
|
377
|
+
entryPath;
|
|
378
|
+
timeout;
|
|
379
|
+
authHeaders;
|
|
380
|
+
publicMode;
|
|
381
|
+
debug;
|
|
382
|
+
clientInfo;
|
|
383
|
+
notificationHandler;
|
|
384
|
+
interceptors;
|
|
385
|
+
elicitationHandler;
|
|
386
|
+
state = "disconnected";
|
|
387
|
+
authToken;
|
|
388
|
+
sessionId;
|
|
389
|
+
messageEndpoint;
|
|
390
|
+
stream;
|
|
391
|
+
pending = /* @__PURE__ */ new Map();
|
|
392
|
+
connectionCount = 0;
|
|
393
|
+
reconnectCount = 0;
|
|
394
|
+
lastRequestHeaders = {};
|
|
395
|
+
constructor(config) {
|
|
396
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
397
|
+
this.entryPath = config.entryPath;
|
|
398
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
399
|
+
this.authHeaders = config.auth?.headers ?? {};
|
|
400
|
+
this.authToken = config.auth?.token;
|
|
401
|
+
this.publicMode = config.publicMode ?? false;
|
|
402
|
+
this.debug = config.debug ?? false;
|
|
403
|
+
this.clientInfo = config.clientInfo;
|
|
404
|
+
this.notificationHandler = config.notificationHandler;
|
|
405
|
+
this.interceptors = config.interceptors;
|
|
406
|
+
this.elicitationHandler = config.elicitationHandler;
|
|
407
|
+
}
|
|
408
|
+
async connect() {
|
|
409
|
+
this.state = "connecting";
|
|
410
|
+
this.connectionCount++;
|
|
411
|
+
try {
|
|
412
|
+
if (!this.publicMode && !this.authToken) {
|
|
413
|
+
await this.requestAnonymousToken();
|
|
414
|
+
}
|
|
415
|
+
await this.openStream();
|
|
416
|
+
this.state = "connected";
|
|
417
|
+
this.log(`Connected; messages go to ${this.messageEndpoint}`);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
this.state = "error";
|
|
420
|
+
this.stream?.abort();
|
|
421
|
+
this.stream = void 0;
|
|
422
|
+
throw error;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
async request(message) {
|
|
426
|
+
this.ensureConnected();
|
|
427
|
+
const startTime = Date.now();
|
|
428
|
+
if (this.interceptors) {
|
|
429
|
+
const intercepted = await this.interceptors.processRequest(message, {
|
|
430
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
431
|
+
transport: "sse",
|
|
432
|
+
sessionId: this.sessionId
|
|
433
|
+
});
|
|
434
|
+
switch (intercepted.type) {
|
|
435
|
+
case "mock":
|
|
436
|
+
return await this.interceptors.processResponse(
|
|
437
|
+
message,
|
|
438
|
+
intercepted.response,
|
|
439
|
+
Date.now() - startTime
|
|
440
|
+
);
|
|
441
|
+
case "error":
|
|
442
|
+
throw markInterceptedError(intercepted.error);
|
|
443
|
+
case "continue":
|
|
444
|
+
message = intercepted.request;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
let response;
|
|
449
|
+
if (message.id === void 0) {
|
|
450
|
+
await this.post(JSON.stringify(message));
|
|
451
|
+
response = { jsonrpc: "2.0", id: null, result: void 0 };
|
|
452
|
+
} else {
|
|
453
|
+
response = await this.postAndAwait(message.id, JSON.stringify(message));
|
|
454
|
+
}
|
|
455
|
+
if (this.interceptors) {
|
|
456
|
+
response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
|
|
457
|
+
}
|
|
458
|
+
return response;
|
|
459
|
+
}
|
|
460
|
+
async notify(message) {
|
|
461
|
+
this.ensureConnected();
|
|
462
|
+
const result = await this.post(JSON.stringify(message));
|
|
463
|
+
if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
|
|
464
|
+
}
|
|
465
|
+
async sendRaw(data) {
|
|
466
|
+
this.ensureConnected();
|
|
467
|
+
let id;
|
|
468
|
+
try {
|
|
469
|
+
const parsed = JSON.parse(data);
|
|
470
|
+
if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
|
|
471
|
+
} catch {
|
|
472
|
+
}
|
|
473
|
+
if (id !== void 0) return this.postAndAwait(id, data);
|
|
474
|
+
const result = await this.post(data);
|
|
475
|
+
if (!result.ok) {
|
|
476
|
+
return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
|
|
477
|
+
}
|
|
478
|
+
return { jsonrpc: "2.0", id: null, result: void 0 };
|
|
479
|
+
}
|
|
480
|
+
async close() {
|
|
481
|
+
this.closeStream("Transport closed");
|
|
482
|
+
this.state = "disconnected";
|
|
483
|
+
this.sessionId = void 0;
|
|
484
|
+
this.messageEndpoint = void 0;
|
|
485
|
+
this.log("SSE transport closed");
|
|
486
|
+
}
|
|
487
|
+
isConnected() {
|
|
488
|
+
return this.state === "connected";
|
|
489
|
+
}
|
|
490
|
+
getState() {
|
|
491
|
+
return this.state;
|
|
492
|
+
}
|
|
493
|
+
getSessionId() {
|
|
494
|
+
return this.sessionId;
|
|
495
|
+
}
|
|
496
|
+
setAuthToken(token) {
|
|
497
|
+
this.authToken = token;
|
|
498
|
+
}
|
|
499
|
+
setTimeout(ms) {
|
|
500
|
+
this.timeout = ms;
|
|
501
|
+
}
|
|
502
|
+
getMessageEndpoint() {
|
|
503
|
+
return this.messageEndpoint;
|
|
504
|
+
}
|
|
505
|
+
getConnectionCount() {
|
|
506
|
+
return this.connectionCount;
|
|
507
|
+
}
|
|
508
|
+
getReconnectCount() {
|
|
509
|
+
return this.reconnectCount;
|
|
510
|
+
}
|
|
511
|
+
getLastRequestHeaders() {
|
|
512
|
+
return { ...this.lastRequestHeaders };
|
|
513
|
+
}
|
|
514
|
+
setInterceptors(interceptors2) {
|
|
515
|
+
this.interceptors = interceptors2;
|
|
516
|
+
}
|
|
517
|
+
getInterceptors() {
|
|
518
|
+
return this.interceptors;
|
|
519
|
+
}
|
|
520
|
+
setElicitationHandler(handler) {
|
|
521
|
+
this.elicitationHandler = handler;
|
|
522
|
+
}
|
|
523
|
+
async simulateDisconnect() {
|
|
524
|
+
this.closeStream("Simulated disconnect");
|
|
525
|
+
this.state = "disconnected";
|
|
526
|
+
this.sessionId = void 0;
|
|
527
|
+
this.messageEndpoint = void 0;
|
|
528
|
+
}
|
|
529
|
+
async waitForReconnect(timeoutMs) {
|
|
530
|
+
this.reconnectCount++;
|
|
531
|
+
let timer;
|
|
532
|
+
try {
|
|
533
|
+
await Promise.race([
|
|
534
|
+
this.connect(),
|
|
535
|
+
new Promise((_, reject) => {
|
|
536
|
+
timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
|
|
537
|
+
})
|
|
538
|
+
]);
|
|
539
|
+
} finally {
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
544
|
+
// STREAM
|
|
545
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
546
|
+
/** Open `GET <entry>/sse` and wait for the `endpoint` event. */
|
|
547
|
+
async openStream() {
|
|
548
|
+
const controller = new AbortController();
|
|
549
|
+
this.stream = controller;
|
|
550
|
+
const url = this.sseUrl();
|
|
551
|
+
const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
|
|
552
|
+
this.log(`GET ${url}`);
|
|
553
|
+
const connectTimer = setTimeout(() => controller.abort(), this.timeout);
|
|
554
|
+
let response;
|
|
555
|
+
try {
|
|
556
|
+
response = await fetch(url, {
|
|
557
|
+
method: "GET",
|
|
558
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
559
|
+
signal: controller.signal
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
clearTimeout(connectTimer);
|
|
563
|
+
throw new Error(
|
|
564
|
+
isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
|
|
565
|
+
{ cause: error }
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
569
|
+
if (!response.ok || !response.body || !isEventStream) {
|
|
570
|
+
clearTimeout(connectTimer);
|
|
571
|
+
const body = await response.text().catch(() => "");
|
|
572
|
+
throw new Error(
|
|
573
|
+
`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 } } })).`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
const endpoint = new Promise((resolve, reject) => {
|
|
577
|
+
const reader = response.body?.getReader();
|
|
578
|
+
if (!reader) {
|
|
579
|
+
reject(new Error(`SSE connection to ${url} has no body`));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
void this.readStream(controller, reader, resolve, reject);
|
|
583
|
+
});
|
|
584
|
+
try {
|
|
585
|
+
const data = await endpoint;
|
|
586
|
+
const messageUrl = new URL(data, url);
|
|
587
|
+
this.messageEndpoint = messageUrl.toString();
|
|
588
|
+
this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
|
|
589
|
+
} finally {
|
|
590
|
+
clearTimeout(connectTimer);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async readStream(controller, reader, onEndpoint, onEndpointError) {
|
|
594
|
+
const decoder = new TextDecoder();
|
|
595
|
+
let buffer = "";
|
|
596
|
+
let endpointSeen = false;
|
|
597
|
+
try {
|
|
598
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
599
|
+
const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
600
|
+
buffer = remainder;
|
|
601
|
+
for (const text of completeEvents) {
|
|
602
|
+
const event = parseSSEEvent(text);
|
|
603
|
+
if (!event) continue;
|
|
604
|
+
if (event.type === "endpoint") {
|
|
605
|
+
if (!endpointSeen) {
|
|
606
|
+
endpointSeen = true;
|
|
607
|
+
onEndpoint(event.data);
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
await this.handleMessage(event.data);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (!controller.signal.aborted) this.log("SSE stream failed:", error);
|
|
616
|
+
} finally {
|
|
617
|
+
reader.releaseLock();
|
|
618
|
+
}
|
|
619
|
+
if (!endpointSeen) {
|
|
620
|
+
onEndpointError(
|
|
621
|
+
new Error(
|
|
622
|
+
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`
|
|
623
|
+
)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
if (this.stream === controller) {
|
|
627
|
+
this.stream = void 0;
|
|
628
|
+
this.rejectPending("SSE stream closed by the server");
|
|
629
|
+
if (this.state === "connected") this.state = "disconnected";
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
async handleMessage(data) {
|
|
633
|
+
let message;
|
|
634
|
+
try {
|
|
635
|
+
message = JSON.parse(data);
|
|
636
|
+
} catch {
|
|
637
|
+
this.log("Ignoring SSE event that is not JSON:", data);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (!message || typeof message !== "object") return;
|
|
641
|
+
if (typeof message.method === "string") {
|
|
642
|
+
const request = message;
|
|
643
|
+
if (request.id === void 0 || request.id === null) {
|
|
644
|
+
this.notificationHandler?.(request);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
await this.answerServerRequest(request);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
if (message.id !== void 0 && message.id !== null) {
|
|
651
|
+
const pending = this.pending.get(message.id);
|
|
652
|
+
if (pending) {
|
|
653
|
+
clearTimeout(pending.timer);
|
|
654
|
+
this.pending.delete(message.id);
|
|
655
|
+
pending.resolve(message);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
|
|
660
|
+
async answerServerRequest(request) {
|
|
661
|
+
const id = request.id;
|
|
662
|
+
let reply;
|
|
663
|
+
if (request.method === "elicitation/create") {
|
|
664
|
+
reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
|
|
665
|
+
} else if (request.method === "ping") {
|
|
666
|
+
reply = { jsonrpc: "2.0", id, result: {} };
|
|
667
|
+
} else {
|
|
668
|
+
reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
|
|
669
|
+
}
|
|
670
|
+
const result = await this.post(JSON.stringify(reply));
|
|
671
|
+
if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
|
|
672
|
+
}
|
|
673
|
+
async elicit(params) {
|
|
674
|
+
if (!this.elicitationHandler) return { action: "decline" };
|
|
675
|
+
try {
|
|
676
|
+
return await this.elicitationHandler(params);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
this.log("Elicitation handler error:", error);
|
|
679
|
+
return { action: "cancel" };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
closeStream(reason) {
|
|
683
|
+
const controller = this.stream;
|
|
684
|
+
this.stream = void 0;
|
|
685
|
+
controller?.abort();
|
|
686
|
+
this.rejectPending(reason);
|
|
687
|
+
}
|
|
688
|
+
rejectPending(reason) {
|
|
689
|
+
for (const [id, pending] of this.pending) {
|
|
690
|
+
clearTimeout(pending.timer);
|
|
691
|
+
pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
|
|
692
|
+
}
|
|
693
|
+
this.pending.clear();
|
|
694
|
+
}
|
|
695
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
696
|
+
// POSTING
|
|
697
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
698
|
+
/** POST a message and resolve with the response the stream carries for `id`. */
|
|
699
|
+
async postAndAwait(id, body) {
|
|
700
|
+
const answer = new Promise((resolve) => {
|
|
701
|
+
const timer = setTimeout(() => {
|
|
702
|
+
this.pending.delete(id);
|
|
703
|
+
resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
|
|
704
|
+
}, this.timeout);
|
|
705
|
+
this.pending.set(id, { resolve, timer });
|
|
706
|
+
});
|
|
707
|
+
const result = await this.post(body);
|
|
708
|
+
if (!result.ok) {
|
|
709
|
+
const pending = this.pending.get(id);
|
|
710
|
+
if (pending) {
|
|
711
|
+
clearTimeout(pending.timer);
|
|
712
|
+
this.pending.delete(id);
|
|
713
|
+
}
|
|
714
|
+
return {
|
|
715
|
+
jsonrpc: "2.0",
|
|
716
|
+
id,
|
|
717
|
+
error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
return answer;
|
|
721
|
+
}
|
|
722
|
+
async post(body) {
|
|
723
|
+
const endpoint = this.messageEndpoint;
|
|
724
|
+
if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
|
|
725
|
+
const headers = this.buildHeaders();
|
|
726
|
+
this.lastRequestHeaders = headers;
|
|
727
|
+
this.log(`POST ${endpoint}`, body);
|
|
728
|
+
const controller = new AbortController();
|
|
729
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
730
|
+
try {
|
|
731
|
+
const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
|
|
732
|
+
const text = await response.text();
|
|
733
|
+
return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (isAbortError(error)) {
|
|
736
|
+
return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
|
|
737
|
+
}
|
|
738
|
+
throw error;
|
|
739
|
+
} finally {
|
|
740
|
+
clearTimeout(timer);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
744
|
+
// HELPERS
|
|
745
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
746
|
+
async requestAnonymousToken() {
|
|
747
|
+
const url = new URL(this.baseUrl);
|
|
748
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
|
|
749
|
+
url.search = "";
|
|
750
|
+
const tokenUrl = url.toString();
|
|
751
|
+
try {
|
|
752
|
+
const response = await fetch(tokenUrl, {
|
|
753
|
+
method: "POST",
|
|
754
|
+
headers: { "Content-Type": "application/json" },
|
|
755
|
+
body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
|
|
756
|
+
});
|
|
757
|
+
if (!response.ok) {
|
|
758
|
+
this.log(`No anonymous token (HTTP ${response.status})`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const token = await response.json();
|
|
762
|
+
if (token.access_token) this.authToken = token.access_token;
|
|
763
|
+
} catch (error) {
|
|
764
|
+
this.log("Error requesting anonymous token:", error);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
/** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
|
|
768
|
+
sseUrl() {
|
|
769
|
+
const url = new URL(this.baseUrl);
|
|
770
|
+
const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
771
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
772
|
+
url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
|
|
773
|
+
return url.toString();
|
|
774
|
+
}
|
|
775
|
+
buildHeaders() {
|
|
776
|
+
const headers = {
|
|
777
|
+
"Content-Type": "application/json",
|
|
778
|
+
Accept: "application/json, text/event-stream"
|
|
779
|
+
};
|
|
780
|
+
if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
|
|
781
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
782
|
+
if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
|
|
783
|
+
Object.assign(headers, this.authHeaders);
|
|
784
|
+
return headers;
|
|
785
|
+
}
|
|
786
|
+
ensureConnected() {
|
|
787
|
+
if (this.state !== "connected") {
|
|
788
|
+
throw new Error("Transport not connected. Call connect() first.");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
log(message, data) {
|
|
792
|
+
if (this.debug) {
|
|
793
|
+
console.log(`[SSE] ${message}`, data ?? "");
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
function splitSSEEvents(buffer) {
|
|
798
|
+
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
799
|
+
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
800
|
+
const completeEvents = normalized.split("\n\n");
|
|
801
|
+
const remainder = completeEvents.pop() ?? "";
|
|
802
|
+
return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
|
|
803
|
+
}
|
|
804
|
+
function parseSSEEvent(text) {
|
|
805
|
+
let type = "message";
|
|
806
|
+
const data = [];
|
|
807
|
+
for (const line of text.split("\n")) {
|
|
808
|
+
if (line.startsWith(":")) continue;
|
|
809
|
+
const colon = line.indexOf(":");
|
|
810
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
811
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
812
|
+
if (field === "event") type = value;
|
|
813
|
+
else if (field === "data") data.push(value);
|
|
814
|
+
}
|
|
815
|
+
return data.length > 0 ? { type, data: data.join("\n") } : void 0;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// libs/testing/src/transport/streamable-http.transport.ts
|
|
819
|
+
var DEFAULT_TIMEOUT2 = 3e4;
|
|
372
820
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
373
821
|
var StreamableHttpTransport = class {
|
|
374
822
|
config;
|
|
@@ -390,7 +838,7 @@ var StreamableHttpTransport = class {
|
|
|
390
838
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
391
839
|
// Remove trailing slash
|
|
392
840
|
entryPath: config.entryPath,
|
|
393
|
-
timeout: config.timeout ??
|
|
841
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT2,
|
|
394
842
|
auth: config.auth ?? {},
|
|
395
843
|
publicMode: config.publicMode ?? false,
|
|
396
844
|
debug: config.debug ?? false,
|
|
@@ -730,7 +1178,7 @@ var StreamableHttpTransport = class {
|
|
|
730
1178
|
let buffer = "";
|
|
731
1179
|
try {
|
|
732
1180
|
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
733
|
-
const { completeEvents, remainder } =
|
|
1181
|
+
const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
734
1182
|
buffer = remainder;
|
|
735
1183
|
for (const eventText of completeEvents) {
|
|
736
1184
|
for (const event of this.parseSSEEvents(eventText, void 0).events) {
|
|
@@ -1064,7 +1512,7 @@ var StreamableHttpTransport = class {
|
|
|
1064
1512
|
};
|
|
1065
1513
|
}
|
|
1066
1514
|
};
|
|
1067
|
-
function
|
|
1515
|
+
function splitSSEEvents2(buffer) {
|
|
1068
1516
|
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
1069
1517
|
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
1070
1518
|
const completeEvents = normalized.split("\n\n");
|
|
@@ -1321,7 +1769,7 @@ var McpTestClientBuilder = class {
|
|
|
1321
1769
|
};
|
|
1322
1770
|
|
|
1323
1771
|
// libs/testing/src/client/mcp-test-client.ts
|
|
1324
|
-
var
|
|
1772
|
+
var DEFAULT_TIMEOUT3 = 3e4;
|
|
1325
1773
|
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
1326
1774
|
var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
1327
1775
|
var DEFAULT_CLIENT_INFO = {
|
|
@@ -1391,7 +1839,7 @@ var McpTestClient = class {
|
|
|
1391
1839
|
transport: config.transport ?? "streamable-http",
|
|
1392
1840
|
auth: config.auth ?? {},
|
|
1393
1841
|
publicMode: config.publicMode ?? false,
|
|
1394
|
-
timeout: config.timeout ??
|
|
1842
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT3,
|
|
1395
1843
|
debug: config.debug ?? false,
|
|
1396
1844
|
protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
|
|
1397
1845
|
clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
|
|
@@ -2157,7 +2605,18 @@ var McpTestClient = class {
|
|
|
2157
2605
|
notificationHandler: this.recordNotification
|
|
2158
2606
|
});
|
|
2159
2607
|
case "sse":
|
|
2160
|
-
|
|
2608
|
+
return new SseTransport({
|
|
2609
|
+
baseUrl,
|
|
2610
|
+
entryPath: this.config.entryPath,
|
|
2611
|
+
timeout: this.config.timeout,
|
|
2612
|
+
auth: this.config.auth,
|
|
2613
|
+
publicMode: this.config.publicMode,
|
|
2614
|
+
debug: this.config.debug,
|
|
2615
|
+
interceptors: this._interceptors,
|
|
2616
|
+
clientInfo: this.config.clientInfo,
|
|
2617
|
+
elicitationHandler: this._elicitationHandler,
|
|
2618
|
+
notificationHandler: this.recordNotification
|
|
2619
|
+
});
|
|
2161
2620
|
default:
|
|
2162
2621
|
throw new Error(`Unknown transport type: ${this.config.transport}`);
|
|
2163
2622
|
}
|
|
@@ -5104,7 +5563,19 @@ function jestGlobals() {
|
|
|
5104
5563
|
);
|
|
5105
5564
|
}
|
|
5106
5565
|
}
|
|
5107
|
-
|
|
5566
|
+
function newScope(parent) {
|
|
5567
|
+
return {
|
|
5568
|
+
skipped: parent?.skipped ?? false,
|
|
5569
|
+
reason: parent?.reason,
|
|
5570
|
+
config: {},
|
|
5571
|
+
envs: [],
|
|
5572
|
+
configured: false,
|
|
5573
|
+
cleanupRegistered: false,
|
|
5574
|
+
beforeEachHooks: [],
|
|
5575
|
+
afterEachHooks: []
|
|
5576
|
+
};
|
|
5577
|
+
}
|
|
5578
|
+
var scopes = [newScope()];
|
|
5108
5579
|
function currentScope() {
|
|
5109
5580
|
return scopes[scopes.length - 1];
|
|
5110
5581
|
}
|
|
@@ -5112,9 +5583,8 @@ var serverEntries = /* @__PURE__ */ new Map();
|
|
|
5112
5583
|
function resolveConfig(chain) {
|
|
5113
5584
|
const merged = {};
|
|
5114
5585
|
for (const scope of chain) {
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
if (env) merged.env = { ...merged.env, ...env };
|
|
5586
|
+
Object.assign(merged, scope.config);
|
|
5587
|
+
for (const env of scope.envs) merged.env = { ...merged.env, ...env };
|
|
5118
5588
|
}
|
|
5119
5589
|
return merged;
|
|
5120
5590
|
}
|
|
@@ -5367,17 +5837,39 @@ function resolveServerCommand(server) {
|
|
|
5367
5837
|
function runWithFixtures(fn, chain) {
|
|
5368
5838
|
return async (...extra) => {
|
|
5369
5839
|
const fixtures = await buildFixtures(chain);
|
|
5370
|
-
let
|
|
5840
|
+
let failure;
|
|
5371
5841
|
try {
|
|
5842
|
+
for (const scope of chain) {
|
|
5843
|
+
for (const hook of scope.beforeEachHooks) await hook(fixtures);
|
|
5844
|
+
}
|
|
5372
5845
|
await fn(fixtures, ...extra);
|
|
5373
5846
|
} catch (error) {
|
|
5374
|
-
|
|
5375
|
-
throw error;
|
|
5376
|
-
} finally {
|
|
5377
|
-
await cleanupTestFixtures(fixtures, testFailed);
|
|
5847
|
+
failure = { error };
|
|
5378
5848
|
}
|
|
5849
|
+
try {
|
|
5850
|
+
await runAfterEachHooks(chain, fixtures);
|
|
5851
|
+
} catch (error) {
|
|
5852
|
+
failure ??= { error };
|
|
5853
|
+
}
|
|
5854
|
+
await cleanupTestFixtures(fixtures, failure !== void 0);
|
|
5855
|
+
if (failure) throw failure.error;
|
|
5379
5856
|
};
|
|
5380
5857
|
}
|
|
5858
|
+
async function runAfterEachHooks(chain, fixtures) {
|
|
5859
|
+
let firstError;
|
|
5860
|
+
let failed = false;
|
|
5861
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
5862
|
+
for (const hook of chain[i].afterEachHooks) {
|
|
5863
|
+
try {
|
|
5864
|
+
await hook(fixtures);
|
|
5865
|
+
} catch (error) {
|
|
5866
|
+
if (!failed) firstError = error;
|
|
5867
|
+
failed = true;
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
if (failed) throw firstError;
|
|
5872
|
+
}
|
|
5381
5873
|
function skipTitle(name, scope) {
|
|
5382
5874
|
return scope.reason ? `${name} (skipped: ${scope.reason})` : name;
|
|
5383
5875
|
}
|
|
@@ -5392,11 +5884,9 @@ function testWithFixtures(name, fn) {
|
|
|
5392
5884
|
}
|
|
5393
5885
|
function use(config) {
|
|
5394
5886
|
const scope = currentScope();
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
env: config.env ? { ...scope.config.env, ...config.env } : scope.config.env
|
|
5399
|
-
};
|
|
5887
|
+
const { env, ...rest } = config;
|
|
5888
|
+
scope.config = { ...scope.config, ...rest };
|
|
5889
|
+
if (env) scope.envs.push(env);
|
|
5400
5890
|
scope.configured = true;
|
|
5401
5891
|
if (!scope.cleanupRegistered) {
|
|
5402
5892
|
scope.cleanupRegistered = true;
|
|
@@ -5447,14 +5937,7 @@ function each(table, ...tagged) {
|
|
|
5447
5937
|
}
|
|
5448
5938
|
function withScope(fn) {
|
|
5449
5939
|
return (...args) => {
|
|
5450
|
-
|
|
5451
|
-
scopes.push({
|
|
5452
|
-
skipped: parent.skipped,
|
|
5453
|
-
reason: parent.reason,
|
|
5454
|
-
config: {},
|
|
5455
|
-
configured: false,
|
|
5456
|
-
cleanupRegistered: false
|
|
5457
|
-
});
|
|
5940
|
+
scopes.push(newScope(currentScope()));
|
|
5458
5941
|
try {
|
|
5459
5942
|
fn(...args);
|
|
5460
5943
|
} finally {
|
|
@@ -5492,12 +5975,22 @@ var describeWithScope = Object.assign(
|
|
|
5492
5975
|
each: scopedEach(() => jestGlobals().describe.each)
|
|
5493
5976
|
}
|
|
5494
5977
|
);
|
|
5978
|
+
function eachHook(kind) {
|
|
5979
|
+
return (fn, timeout) => {
|
|
5980
|
+
if (typeof fn === "function" && fn.length > 0) {
|
|
5981
|
+
const scope = currentScope();
|
|
5982
|
+
(kind === "beforeEach" ? scope.beforeEachHooks : scope.afterEachHooks).push(fn);
|
|
5983
|
+
return;
|
|
5984
|
+
}
|
|
5985
|
+
jestGlobals()[kind](fn, timeout);
|
|
5986
|
+
};
|
|
5987
|
+
}
|
|
5495
5988
|
var test = testWithFixtures;
|
|
5496
5989
|
test.use = use;
|
|
5497
5990
|
test.describe = describeWithScope;
|
|
5498
5991
|
test.beforeAll = ((...args) => jestGlobals().beforeAll(...args));
|
|
5499
|
-
test.beforeEach = (
|
|
5500
|
-
test.afterEach = (
|
|
5992
|
+
test.beforeEach = eachHook("beforeEach");
|
|
5993
|
+
test.afterEach = eachHook("afterEach");
|
|
5501
5994
|
test.afterAll = ((...args) => jestGlobals().afterAll(...args));
|
|
5502
5995
|
test.skip = skip;
|
|
5503
5996
|
test.only = only;
|
|
@@ -8626,6 +9119,7 @@ export {
|
|
|
8626
9119
|
RegressionDetector,
|
|
8627
9120
|
ReportGenerator,
|
|
8628
9121
|
ServerStartError,
|
|
9122
|
+
SseTransport,
|
|
8629
9123
|
StreamableHttpTransport,
|
|
8630
9124
|
TestClientError,
|
|
8631
9125
|
TestServer,
|
package/esm/matchers/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
var __frontmcpModule = typeof process !== "undefined" && typeof process.getBuiltinModule === "function" ? process.getBuiltinModule("module") : undefined;
|
|
2
|
+
const require = __frontmcpModule ? __frontmcpModule.createRequire(import.meta.url || 'file:///') : undefined;
|
|
3
|
+
// __frontmcpCreateRequire
|
|
1
4
|
// libs/testing/src/platform/platform-types.ts
|
|
2
5
|
function getPlatformMimeType(platform) {
|
|
3
6
|
return "text/html;profile=mcp-app";
|