@frontmcp/testing 1.8.0 → 1.8.2
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/index.d.ts +1 -1
- package/client/index.d.ts.map +1 -1
- package/client/mcp-test-client.builder.d.ts +2 -1
- package/client/mcp-test-client.builder.d.ts.map +1 -1
- package/client/mcp-test-client.d.ts +6 -2
- package/client/mcp-test-client.d.ts.map +1 -1
- package/client/mcp-test-client.types.d.ts +8 -1
- package/client/mcp-test-client.types.d.ts.map +1 -1
- package/esm/fixtures/index.mjs +127 -6
- package/esm/index.mjs +132 -12
- package/esm/matchers/index.mjs +5 -6
- package/esm/package.json +6 -6
- package/esm/perf/index.mjs +127 -6
- package/esm/setup.mjs +5 -6
- package/fixtures/index.js +127 -6
- package/index.d.ts +1 -1
- package/index.d.ts.map +1 -1
- package/index.js +132 -12
- package/matchers/index.js +5 -6
- package/matchers/matcher-types.d.ts +3 -2
- package/matchers/matcher-types.d.ts.map +1 -1
- package/matchers/mcp-matchers.d.ts +1 -1
- package/matchers/mcp-matchers.d.ts.map +1 -1
- package/package.json +6 -6
- package/perf/index.js +127 -6
- package/setup.js +5 -6
- package/transport/index.d.ts +1 -1
- package/transport/index.d.ts.map +1 -1
- package/transport/streamable-http.transport.d.ts +10 -0
- package/transport/streamable-http.transport.d.ts.map +1 -1
- package/transport/transport.interface.d.ts +9 -0
- package/transport/transport.interface.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -343,6 +343,7 @@ var interceptors = {
|
|
|
343
343
|
|
|
344
344
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
345
345
|
var DEFAULT_TIMEOUT = 3e4;
|
|
346
|
+
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
346
347
|
var StreamableHttpTransport = class {
|
|
347
348
|
config;
|
|
348
349
|
state = "disconnected";
|
|
@@ -354,6 +355,10 @@ var StreamableHttpTransport = class {
|
|
|
354
355
|
interceptors;
|
|
355
356
|
publicMode;
|
|
356
357
|
elicitationHandler;
|
|
358
|
+
notificationHandler;
|
|
359
|
+
notificationStream;
|
|
360
|
+
notificationProtocolVersion = "";
|
|
361
|
+
notificationStreamReopenTimer;
|
|
357
362
|
constructor(config) {
|
|
358
363
|
this.config = {
|
|
359
364
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
@@ -370,6 +375,7 @@ var StreamableHttpTransport = class {
|
|
|
370
375
|
this.interceptors = config.interceptors;
|
|
371
376
|
this.publicMode = config.publicMode ?? false;
|
|
372
377
|
this.elicitationHandler = config.elicitationHandler;
|
|
378
|
+
this.notificationHandler = config.notificationHandler;
|
|
373
379
|
}
|
|
374
380
|
async connect() {
|
|
375
381
|
this.state = "connecting";
|
|
@@ -589,7 +595,12 @@ var StreamableHttpTransport = class {
|
|
|
589
595
|
};
|
|
590
596
|
}
|
|
591
597
|
}
|
|
598
|
+
async openNotificationStream(protocolVersion) {
|
|
599
|
+
this.notificationProtocolVersion = protocolVersion;
|
|
600
|
+
await this.connectNotificationStream();
|
|
601
|
+
}
|
|
592
602
|
async close() {
|
|
603
|
+
this.closeNotificationStream();
|
|
593
604
|
this.state = "disconnected";
|
|
594
605
|
this.sessionId = void 0;
|
|
595
606
|
this.log("StreamableHTTP transport closed");
|
|
@@ -628,6 +639,7 @@ var StreamableHttpTransport = class {
|
|
|
628
639
|
return { ...this.lastRequestHeaders };
|
|
629
640
|
}
|
|
630
641
|
async simulateDisconnect() {
|
|
642
|
+
this.closeNotificationStream();
|
|
631
643
|
this.state = "disconnected";
|
|
632
644
|
this.sessionId = void 0;
|
|
633
645
|
}
|
|
@@ -646,6 +658,72 @@ var StreamableHttpTransport = class {
|
|
|
646
658
|
// ═══════════════════════════════════════════════════════════════════
|
|
647
659
|
// PRIVATE HELPERS
|
|
648
660
|
// ═══════════════════════════════════════════════════════════════════
|
|
661
|
+
closeNotificationStream() {
|
|
662
|
+
clearTimeout(this.notificationStreamReopenTimer);
|
|
663
|
+
this.notificationStream?.abort();
|
|
664
|
+
this.notificationStream = void 0;
|
|
665
|
+
}
|
|
666
|
+
/** Opens the stream without holding up the caller past the request timeout; close() aborts it at any point. */
|
|
667
|
+
async connectNotificationStream() {
|
|
668
|
+
if (!this.sessionId || this.notificationStream) return;
|
|
669
|
+
const controller = new AbortController();
|
|
670
|
+
this.notificationStream = controller;
|
|
671
|
+
const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
|
|
672
|
+
const established = fetch(this.mcpUrl(), {
|
|
673
|
+
method: "GET",
|
|
674
|
+
headers: { ...headers, Accept: "text/event-stream", "MCP-Protocol-Version": this.notificationProtocolVersion },
|
|
675
|
+
signal: controller.signal
|
|
676
|
+
}).then(
|
|
677
|
+
async (response) => {
|
|
678
|
+
const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
679
|
+
if (!response.ok || !response.body || !isEventStream) {
|
|
680
|
+
await response.body?.cancel();
|
|
681
|
+
this.log(`Server offers no notification stream (HTTP ${response.status})`);
|
|
682
|
+
this.releaseNotificationStream(controller);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
void this.readNotificationStream(controller, response.body.getReader());
|
|
686
|
+
},
|
|
687
|
+
(error) => {
|
|
688
|
+
this.log("Failed to open the notification stream:", error);
|
|
689
|
+
this.releaseNotificationStream(controller);
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
let establishTimer;
|
|
693
|
+
const establishDeadline = new Promise((resolve) => {
|
|
694
|
+
establishTimer = setTimeout(resolve, this.config.timeout);
|
|
695
|
+
});
|
|
696
|
+
await Promise.race([established, establishDeadline]);
|
|
697
|
+
clearTimeout(establishTimer);
|
|
698
|
+
}
|
|
699
|
+
releaseNotificationStream(controller) {
|
|
700
|
+
if (this.notificationStream === controller) this.notificationStream = void 0;
|
|
701
|
+
}
|
|
702
|
+
async readNotificationStream(controller, reader) {
|
|
703
|
+
const decoder = new TextDecoder();
|
|
704
|
+
let buffer = "";
|
|
705
|
+
try {
|
|
706
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
707
|
+
const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
708
|
+
buffer = remainder;
|
|
709
|
+
for (const eventText of completeEvents) {
|
|
710
|
+
for (const event of this.parseSSEEvents(eventText, void 0).events) {
|
|
711
|
+
await this.handleSSEEvent(event);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
} catch (error) {
|
|
716
|
+
this.log("Notification stream ended:", error);
|
|
717
|
+
} finally {
|
|
718
|
+
reader.releaseLock();
|
|
719
|
+
}
|
|
720
|
+
if (controller.signal.aborted || this.notificationStream !== controller) return;
|
|
721
|
+
this.releaseNotificationStream(controller);
|
|
722
|
+
this.notificationStreamReopenTimer = setTimeout(
|
|
723
|
+
() => void this.connectNotificationStream(),
|
|
724
|
+
NOTIFICATION_STREAM_REOPEN_DELAY_MS
|
|
725
|
+
);
|
|
726
|
+
}
|
|
649
727
|
/**
|
|
650
728
|
* Handle SSE response with elicitation support.
|
|
651
729
|
*
|
|
@@ -783,6 +861,13 @@ var StreamableHttpTransport = class {
|
|
|
783
861
|
response: { jsonrpc: "2.0", id: null, result: void 0 }
|
|
784
862
|
};
|
|
785
863
|
}
|
|
864
|
+
if ("method" in parsed && parsed.id === void 0) {
|
|
865
|
+
this.notificationHandler?.(parsed);
|
|
866
|
+
return {
|
|
867
|
+
isFinal: false,
|
|
868
|
+
response: { jsonrpc: "2.0", id: null, result: void 0 }
|
|
869
|
+
};
|
|
870
|
+
}
|
|
786
871
|
if ("result" in parsed || "error" in parsed) {
|
|
787
872
|
return { isFinal: true, response: parsed };
|
|
788
873
|
}
|
|
@@ -953,6 +1038,13 @@ var StreamableHttpTransport = class {
|
|
|
953
1038
|
};
|
|
954
1039
|
}
|
|
955
1040
|
};
|
|
1041
|
+
function splitSSEEvents(buffer) {
|
|
1042
|
+
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
1043
|
+
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
1044
|
+
const completeEvents = normalized.split("\n\n");
|
|
1045
|
+
const remainder = completeEvents.pop() ?? "";
|
|
1046
|
+
return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
|
|
1047
|
+
}
|
|
956
1048
|
|
|
957
1049
|
// libs/testing/src/platform/platform-client-info.ts
|
|
958
1050
|
var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
|
|
@@ -1108,7 +1200,8 @@ var McpTestClientBuilder = class {
|
|
|
1108
1200
|
return this;
|
|
1109
1201
|
}
|
|
1110
1202
|
/**
|
|
1111
|
-
* Set the MCP protocol version to request
|
|
1203
|
+
* Set the MCP protocol version to request (default '2025-06-18').
|
|
1204
|
+
* '2026-07-28' has no initialize handshake, so `build()` throws for it.
|
|
1112
1205
|
*/
|
|
1113
1206
|
withProtocolVersion(version) {
|
|
1114
1207
|
this.config.protocolVersion = version;
|
|
@@ -1204,6 +1297,7 @@ var McpTestClientBuilder = class {
|
|
|
1204
1297
|
// libs/testing/src/client/mcp-test-client.ts
|
|
1205
1298
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
1206
1299
|
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
1300
|
+
var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
1207
1301
|
var DEFAULT_CLIENT_INFO = {
|
|
1208
1302
|
name: "@frontmcp/testing",
|
|
1209
1303
|
version: "0.4.0"
|
|
@@ -1240,6 +1334,8 @@ var McpTestClient = class {
|
|
|
1240
1334
|
_traces = [];
|
|
1241
1335
|
_notifications = [];
|
|
1242
1336
|
_progressUpdates = [];
|
|
1337
|
+
collectingProgress = false;
|
|
1338
|
+
progressTokenCounter = 0;
|
|
1243
1339
|
// Interceptor chain
|
|
1244
1340
|
_interceptors;
|
|
1245
1341
|
// Elicitation handler for server→client elicit requests
|
|
@@ -1248,6 +1344,11 @@ var McpTestClient = class {
|
|
|
1248
1344
|
// CONSTRUCTOR & FACTORY
|
|
1249
1345
|
// ═══════════════════════════════════════════════════════════════════
|
|
1250
1346
|
constructor(config) {
|
|
1347
|
+
if (config.protocolVersion && UNSUPPORTED_PROTOCOL_VERSIONS.includes(config.protocolVersion)) {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`McpTestClient does not support protocol version ${config.protocolVersion}: it has no initialize handshake. Use ${DEFAULT_PROTOCOL_VERSION} or earlier.`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1251
1352
|
this.config = {
|
|
1252
1353
|
baseUrl: config.baseUrl,
|
|
1253
1354
|
entryPath: config.entryPath,
|
|
@@ -1333,6 +1434,7 @@ var McpTestClient = class {
|
|
|
1333
1434
|
jsonrpc: "2.0",
|
|
1334
1435
|
method: "notifications/initialized"
|
|
1335
1436
|
});
|
|
1437
|
+
await this.transport.openNotificationStream?.(this.initResult.protocolVersion);
|
|
1336
1438
|
this.log("info", `Connected to ${this.initResult.serverInfo?.name ?? "MCP Server"}`);
|
|
1337
1439
|
return this.initResult;
|
|
1338
1440
|
}
|
|
@@ -1443,8 +1545,8 @@ var McpTestClient = class {
|
|
|
1443
1545
|
/**
|
|
1444
1546
|
* Call a tool by name with arguments
|
|
1445
1547
|
*/
|
|
1446
|
-
call: async (name, args) => {
|
|
1447
|
-
const response = await this.callTool(name, args);
|
|
1548
|
+
call: async (name, args, options = {}) => {
|
|
1549
|
+
const response = await this.callTool(name, args, this.progressTokenFor(options));
|
|
1448
1550
|
return this.wrapToolResult(response);
|
|
1449
1551
|
}
|
|
1450
1552
|
};
|
|
@@ -1586,6 +1688,7 @@ var McpTestClient = class {
|
|
|
1586
1688
|
* Collect progress notifications specifically
|
|
1587
1689
|
*/
|
|
1588
1690
|
collectProgress: () => {
|
|
1691
|
+
this.collectingProgress = true;
|
|
1589
1692
|
return new ProgressCollector(this._progressUpdates);
|
|
1590
1693
|
},
|
|
1591
1694
|
/**
|
|
@@ -1891,12 +1994,29 @@ var McpTestClient = class {
|
|
|
1891
1994
|
async listTools() {
|
|
1892
1995
|
return this.request("tools/list", {});
|
|
1893
1996
|
}
|
|
1894
|
-
async callTool(name, args) {
|
|
1997
|
+
async callTool(name, args, progressToken) {
|
|
1895
1998
|
return this.request("tools/call", {
|
|
1896
1999
|
name,
|
|
1897
|
-
arguments: args ?? {}
|
|
2000
|
+
arguments: args ?? {},
|
|
2001
|
+
...progressToken !== void 0 && { _meta: { progressToken } }
|
|
1898
2002
|
});
|
|
1899
2003
|
}
|
|
2004
|
+
progressTokenFor(options) {
|
|
2005
|
+
if (options.progressToken !== void 0) return options.progressToken;
|
|
2006
|
+
return this.collectingProgress ? `progress-${++this.progressTokenCounter}` : void 0;
|
|
2007
|
+
}
|
|
2008
|
+
recordNotification = (notification) => {
|
|
2009
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
2010
|
+
this._notifications.push({ method: notification.method, params: notification.params, timestamp });
|
|
2011
|
+
if (notification.method !== "notifications/progress") return;
|
|
2012
|
+
const { progress, total, progressToken } = notification.params ?? {};
|
|
2013
|
+
this._progressUpdates.push({
|
|
2014
|
+
progress: Number(progress),
|
|
2015
|
+
total: typeof total === "number" ? total : void 0,
|
|
2016
|
+
progressToken: typeof progressToken === "string" || typeof progressToken === "number" ? progressToken : void 0,
|
|
2017
|
+
timestamp
|
|
2018
|
+
});
|
|
2019
|
+
};
|
|
1900
2020
|
async listResources() {
|
|
1901
2021
|
return this.request("resources/list", {});
|
|
1902
2022
|
}
|
|
@@ -1943,7 +2063,8 @@ var McpTestClient = class {
|
|
|
1943
2063
|
debug: this.config.debug,
|
|
1944
2064
|
interceptors: this._interceptors,
|
|
1945
2065
|
clientInfo: this.config.clientInfo,
|
|
1946
|
-
elicitationHandler: this._elicitationHandler
|
|
2066
|
+
elicitationHandler: this._elicitationHandler,
|
|
2067
|
+
notificationHandler: this.recordNotification
|
|
1947
2068
|
});
|
|
1948
2069
|
case "sse":
|
|
1949
2070
|
throw new Error("SSE transport not yet implemented");
|
|
@@ -5486,18 +5607,17 @@ var toBeError = function(received, expectedCode) {
|
|
|
5486
5607
|
message: () => `Expected a result wrapper object with isError property`
|
|
5487
5608
|
};
|
|
5488
5609
|
}
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
}
|
|
5610
|
+
const receivedCode = typeof expectedCode === "string" ? result.raw?._meta?.["code"] : result.error?.code;
|
|
5611
|
+
const codeMatches = expectedCode === void 0 || receivedCode === expectedCode;
|
|
5612
|
+
const pass = result.isError && codeMatches;
|
|
5493
5613
|
return {
|
|
5494
5614
|
pass,
|
|
5495
5615
|
message: () => {
|
|
5496
5616
|
if (!result.isError) {
|
|
5497
5617
|
return "Expected result to be an error, but it was successful";
|
|
5498
5618
|
}
|
|
5499
|
-
if (
|
|
5500
|
-
return `Expected error code ${expectedCode}, but got ${
|
|
5619
|
+
if (!codeMatches) {
|
|
5620
|
+
return `Expected error code ${expectedCode}, but got ${String(receivedCode)}`;
|
|
5501
5621
|
}
|
|
5502
5622
|
return "Expected result not to be an error";
|
|
5503
5623
|
}
|
package/esm/matchers/index.mjs
CHANGED
|
@@ -363,18 +363,17 @@ var toBeError = function(received, expectedCode) {
|
|
|
363
363
|
message: () => `Expected a result wrapper object with isError property`
|
|
364
364
|
};
|
|
365
365
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
}
|
|
366
|
+
const receivedCode = typeof expectedCode === "string" ? result.raw?._meta?.["code"] : result.error?.code;
|
|
367
|
+
const codeMatches = expectedCode === void 0 || receivedCode === expectedCode;
|
|
368
|
+
const pass = result.isError && codeMatches;
|
|
370
369
|
return {
|
|
371
370
|
pass,
|
|
372
371
|
message: () => {
|
|
373
372
|
if (!result.isError) {
|
|
374
373
|
return "Expected result to be an error, but it was successful";
|
|
375
374
|
}
|
|
376
|
-
if (
|
|
377
|
-
return `Expected error code ${expectedCode}, but got ${
|
|
375
|
+
if (!codeMatches) {
|
|
376
|
+
return `Expected error code ${expectedCode}, but got ${String(receivedCode)}`;
|
|
378
377
|
}
|
|
379
378
|
return "Expected result not to be an error";
|
|
380
379
|
}
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/testing",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "E2E testing framework for FrontMCP servers - MCP client, auth mocks, Playwright integration",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
"./esm": null
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
|
-
"@frontmcp/lazy-zod": "1.8.
|
|
91
|
-
"@frontmcp/sdk": "1.8.
|
|
92
|
-
"@frontmcp/ui": "1.8.
|
|
90
|
+
"@frontmcp/lazy-zod": "1.8.2",
|
|
91
|
+
"@frontmcp/sdk": "1.8.2",
|
|
92
|
+
"@frontmcp/ui": "1.8.2",
|
|
93
93
|
"@jest/globals": "^29.0.0 || ^30.0.0",
|
|
94
94
|
"@playwright/test": "^1.40.0",
|
|
95
95
|
"jest": "^29.0.0 || ^30.0.0"
|
|
@@ -112,8 +112,8 @@
|
|
|
112
112
|
"node": ">=24.0.0"
|
|
113
113
|
},
|
|
114
114
|
"dependencies": {
|
|
115
|
-
"@frontmcp/protocol": "1.8.
|
|
116
|
-
"@frontmcp/utils": "1.8.
|
|
115
|
+
"@frontmcp/protocol": "1.8.2",
|
|
116
|
+
"@frontmcp/utils": "1.8.2",
|
|
117
117
|
"jose": "^6.0.11",
|
|
118
118
|
"tslib": "^2.3.0"
|
|
119
119
|
},
|
package/esm/perf/index.mjs
CHANGED
|
@@ -1046,6 +1046,7 @@ function sleep3(ms) {
|
|
|
1046
1046
|
|
|
1047
1047
|
// libs/testing/src/transport/streamable-http.transport.ts
|
|
1048
1048
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1049
|
+
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
1049
1050
|
var StreamableHttpTransport = class {
|
|
1050
1051
|
config;
|
|
1051
1052
|
state = "disconnected";
|
|
@@ -1057,6 +1058,10 @@ var StreamableHttpTransport = class {
|
|
|
1057
1058
|
interceptors;
|
|
1058
1059
|
publicMode;
|
|
1059
1060
|
elicitationHandler;
|
|
1061
|
+
notificationHandler;
|
|
1062
|
+
notificationStream;
|
|
1063
|
+
notificationProtocolVersion = "";
|
|
1064
|
+
notificationStreamReopenTimer;
|
|
1060
1065
|
constructor(config) {
|
|
1061
1066
|
this.config = {
|
|
1062
1067
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
@@ -1073,6 +1078,7 @@ var StreamableHttpTransport = class {
|
|
|
1073
1078
|
this.interceptors = config.interceptors;
|
|
1074
1079
|
this.publicMode = config.publicMode ?? false;
|
|
1075
1080
|
this.elicitationHandler = config.elicitationHandler;
|
|
1081
|
+
this.notificationHandler = config.notificationHandler;
|
|
1076
1082
|
}
|
|
1077
1083
|
async connect() {
|
|
1078
1084
|
this.state = "connecting";
|
|
@@ -1292,7 +1298,12 @@ var StreamableHttpTransport = class {
|
|
|
1292
1298
|
};
|
|
1293
1299
|
}
|
|
1294
1300
|
}
|
|
1301
|
+
async openNotificationStream(protocolVersion) {
|
|
1302
|
+
this.notificationProtocolVersion = protocolVersion;
|
|
1303
|
+
await this.connectNotificationStream();
|
|
1304
|
+
}
|
|
1295
1305
|
async close() {
|
|
1306
|
+
this.closeNotificationStream();
|
|
1296
1307
|
this.state = "disconnected";
|
|
1297
1308
|
this.sessionId = void 0;
|
|
1298
1309
|
this.log("StreamableHTTP transport closed");
|
|
@@ -1331,6 +1342,7 @@ var StreamableHttpTransport = class {
|
|
|
1331
1342
|
return { ...this.lastRequestHeaders };
|
|
1332
1343
|
}
|
|
1333
1344
|
async simulateDisconnect() {
|
|
1345
|
+
this.closeNotificationStream();
|
|
1334
1346
|
this.state = "disconnected";
|
|
1335
1347
|
this.sessionId = void 0;
|
|
1336
1348
|
}
|
|
@@ -1349,6 +1361,72 @@ var StreamableHttpTransport = class {
|
|
|
1349
1361
|
// ═══════════════════════════════════════════════════════════════════
|
|
1350
1362
|
// PRIVATE HELPERS
|
|
1351
1363
|
// ═══════════════════════════════════════════════════════════════════
|
|
1364
|
+
closeNotificationStream() {
|
|
1365
|
+
clearTimeout(this.notificationStreamReopenTimer);
|
|
1366
|
+
this.notificationStream?.abort();
|
|
1367
|
+
this.notificationStream = void 0;
|
|
1368
|
+
}
|
|
1369
|
+
/** Opens the stream without holding up the caller past the request timeout; close() aborts it at any point. */
|
|
1370
|
+
async connectNotificationStream() {
|
|
1371
|
+
if (!this.sessionId || this.notificationStream) return;
|
|
1372
|
+
const controller = new AbortController();
|
|
1373
|
+
this.notificationStream = controller;
|
|
1374
|
+
const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
|
|
1375
|
+
const established = fetch(this.mcpUrl(), {
|
|
1376
|
+
method: "GET",
|
|
1377
|
+
headers: { ...headers, Accept: "text/event-stream", "MCP-Protocol-Version": this.notificationProtocolVersion },
|
|
1378
|
+
signal: controller.signal
|
|
1379
|
+
}).then(
|
|
1380
|
+
async (response) => {
|
|
1381
|
+
const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
1382
|
+
if (!response.ok || !response.body || !isEventStream) {
|
|
1383
|
+
await response.body?.cancel();
|
|
1384
|
+
this.log(`Server offers no notification stream (HTTP ${response.status})`);
|
|
1385
|
+
this.releaseNotificationStream(controller);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
void this.readNotificationStream(controller, response.body.getReader());
|
|
1389
|
+
},
|
|
1390
|
+
(error) => {
|
|
1391
|
+
this.log("Failed to open the notification stream:", error);
|
|
1392
|
+
this.releaseNotificationStream(controller);
|
|
1393
|
+
}
|
|
1394
|
+
);
|
|
1395
|
+
let establishTimer;
|
|
1396
|
+
const establishDeadline = new Promise((resolve) => {
|
|
1397
|
+
establishTimer = setTimeout(resolve, this.config.timeout);
|
|
1398
|
+
});
|
|
1399
|
+
await Promise.race([established, establishDeadline]);
|
|
1400
|
+
clearTimeout(establishTimer);
|
|
1401
|
+
}
|
|
1402
|
+
releaseNotificationStream(controller) {
|
|
1403
|
+
if (this.notificationStream === controller) this.notificationStream = void 0;
|
|
1404
|
+
}
|
|
1405
|
+
async readNotificationStream(controller, reader) {
|
|
1406
|
+
const decoder = new TextDecoder();
|
|
1407
|
+
let buffer = "";
|
|
1408
|
+
try {
|
|
1409
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
1410
|
+
const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
1411
|
+
buffer = remainder;
|
|
1412
|
+
for (const eventText of completeEvents) {
|
|
1413
|
+
for (const event of this.parseSSEEvents(eventText, void 0).events) {
|
|
1414
|
+
await this.handleSSEEvent(event);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
this.log("Notification stream ended:", error);
|
|
1420
|
+
} finally {
|
|
1421
|
+
reader.releaseLock();
|
|
1422
|
+
}
|
|
1423
|
+
if (controller.signal.aborted || this.notificationStream !== controller) return;
|
|
1424
|
+
this.releaseNotificationStream(controller);
|
|
1425
|
+
this.notificationStreamReopenTimer = setTimeout(
|
|
1426
|
+
() => void this.connectNotificationStream(),
|
|
1427
|
+
NOTIFICATION_STREAM_REOPEN_DELAY_MS
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1352
1430
|
/**
|
|
1353
1431
|
* Handle SSE response with elicitation support.
|
|
1354
1432
|
*
|
|
@@ -1486,6 +1564,13 @@ var StreamableHttpTransport = class {
|
|
|
1486
1564
|
response: { jsonrpc: "2.0", id: null, result: void 0 }
|
|
1487
1565
|
};
|
|
1488
1566
|
}
|
|
1567
|
+
if ("method" in parsed && parsed.id === void 0) {
|
|
1568
|
+
this.notificationHandler?.(parsed);
|
|
1569
|
+
return {
|
|
1570
|
+
isFinal: false,
|
|
1571
|
+
response: { jsonrpc: "2.0", id: null, result: void 0 }
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1489
1574
|
if ("result" in parsed || "error" in parsed) {
|
|
1490
1575
|
return { isFinal: true, response: parsed };
|
|
1491
1576
|
}
|
|
@@ -1656,6 +1741,13 @@ var StreamableHttpTransport = class {
|
|
|
1656
1741
|
};
|
|
1657
1742
|
}
|
|
1658
1743
|
};
|
|
1744
|
+
function splitSSEEvents(buffer) {
|
|
1745
|
+
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
1746
|
+
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
1747
|
+
const completeEvents = normalized.split("\n\n");
|
|
1748
|
+
const remainder = completeEvents.pop() ?? "";
|
|
1749
|
+
return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
|
|
1750
|
+
}
|
|
1659
1751
|
|
|
1660
1752
|
// libs/testing/src/platform/platform-client-info.ts
|
|
1661
1753
|
var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
|
|
@@ -1792,7 +1884,8 @@ var McpTestClientBuilder = class {
|
|
|
1792
1884
|
return this;
|
|
1793
1885
|
}
|
|
1794
1886
|
/**
|
|
1795
|
-
* Set the MCP protocol version to request
|
|
1887
|
+
* Set the MCP protocol version to request (default '2025-06-18').
|
|
1888
|
+
* '2026-07-28' has no initialize handshake, so `build()` throws for it.
|
|
1796
1889
|
*/
|
|
1797
1890
|
withProtocolVersion(version) {
|
|
1798
1891
|
this.config.protocolVersion = version;
|
|
@@ -1888,6 +1981,7 @@ var McpTestClientBuilder = class {
|
|
|
1888
1981
|
// libs/testing/src/client/mcp-test-client.ts
|
|
1889
1982
|
var DEFAULT_TIMEOUT2 = 3e4;
|
|
1890
1983
|
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
1984
|
+
var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
1891
1985
|
var DEFAULT_CLIENT_INFO = {
|
|
1892
1986
|
name: "@frontmcp/testing",
|
|
1893
1987
|
version: "0.4.0"
|
|
@@ -1924,6 +2018,8 @@ var McpTestClient = class {
|
|
|
1924
2018
|
_traces = [];
|
|
1925
2019
|
_notifications = [];
|
|
1926
2020
|
_progressUpdates = [];
|
|
2021
|
+
collectingProgress = false;
|
|
2022
|
+
progressTokenCounter = 0;
|
|
1927
2023
|
// Interceptor chain
|
|
1928
2024
|
_interceptors;
|
|
1929
2025
|
// Elicitation handler for server→client elicit requests
|
|
@@ -1932,6 +2028,11 @@ var McpTestClient = class {
|
|
|
1932
2028
|
// CONSTRUCTOR & FACTORY
|
|
1933
2029
|
// ═══════════════════════════════════════════════════════════════════
|
|
1934
2030
|
constructor(config) {
|
|
2031
|
+
if (config.protocolVersion && UNSUPPORTED_PROTOCOL_VERSIONS.includes(config.protocolVersion)) {
|
|
2032
|
+
throw new Error(
|
|
2033
|
+
`McpTestClient does not support protocol version ${config.protocolVersion}: it has no initialize handshake. Use ${DEFAULT_PROTOCOL_VERSION} or earlier.`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
1935
2036
|
this.config = {
|
|
1936
2037
|
baseUrl: config.baseUrl,
|
|
1937
2038
|
entryPath: config.entryPath,
|
|
@@ -2017,6 +2118,7 @@ var McpTestClient = class {
|
|
|
2017
2118
|
jsonrpc: "2.0",
|
|
2018
2119
|
method: "notifications/initialized"
|
|
2019
2120
|
});
|
|
2121
|
+
await this.transport.openNotificationStream?.(this.initResult.protocolVersion);
|
|
2020
2122
|
this.log("info", `Connected to ${this.initResult.serverInfo?.name ?? "MCP Server"}`);
|
|
2021
2123
|
return this.initResult;
|
|
2022
2124
|
}
|
|
@@ -2127,8 +2229,8 @@ var McpTestClient = class {
|
|
|
2127
2229
|
/**
|
|
2128
2230
|
* Call a tool by name with arguments
|
|
2129
2231
|
*/
|
|
2130
|
-
call: async (name, args) => {
|
|
2131
|
-
const response = await this.callTool(name, args);
|
|
2232
|
+
call: async (name, args, options = {}) => {
|
|
2233
|
+
const response = await this.callTool(name, args, this.progressTokenFor(options));
|
|
2132
2234
|
return this.wrapToolResult(response);
|
|
2133
2235
|
}
|
|
2134
2236
|
};
|
|
@@ -2270,6 +2372,7 @@ var McpTestClient = class {
|
|
|
2270
2372
|
* Collect progress notifications specifically
|
|
2271
2373
|
*/
|
|
2272
2374
|
collectProgress: () => {
|
|
2375
|
+
this.collectingProgress = true;
|
|
2273
2376
|
return new ProgressCollector(this._progressUpdates);
|
|
2274
2377
|
},
|
|
2275
2378
|
/**
|
|
@@ -2575,12 +2678,29 @@ var McpTestClient = class {
|
|
|
2575
2678
|
async listTools() {
|
|
2576
2679
|
return this.request("tools/list", {});
|
|
2577
2680
|
}
|
|
2578
|
-
async callTool(name, args) {
|
|
2681
|
+
async callTool(name, args, progressToken) {
|
|
2579
2682
|
return this.request("tools/call", {
|
|
2580
2683
|
name,
|
|
2581
|
-
arguments: args ?? {}
|
|
2684
|
+
arguments: args ?? {},
|
|
2685
|
+
...progressToken !== void 0 && { _meta: { progressToken } }
|
|
2582
2686
|
});
|
|
2583
2687
|
}
|
|
2688
|
+
progressTokenFor(options) {
|
|
2689
|
+
if (options.progressToken !== void 0) return options.progressToken;
|
|
2690
|
+
return this.collectingProgress ? `progress-${++this.progressTokenCounter}` : void 0;
|
|
2691
|
+
}
|
|
2692
|
+
recordNotification = (notification) => {
|
|
2693
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
2694
|
+
this._notifications.push({ method: notification.method, params: notification.params, timestamp });
|
|
2695
|
+
if (notification.method !== "notifications/progress") return;
|
|
2696
|
+
const { progress, total, progressToken } = notification.params ?? {};
|
|
2697
|
+
this._progressUpdates.push({
|
|
2698
|
+
progress: Number(progress),
|
|
2699
|
+
total: typeof total === "number" ? total : void 0,
|
|
2700
|
+
progressToken: typeof progressToken === "string" || typeof progressToken === "number" ? progressToken : void 0,
|
|
2701
|
+
timestamp
|
|
2702
|
+
});
|
|
2703
|
+
};
|
|
2584
2704
|
async listResources() {
|
|
2585
2705
|
return this.request("resources/list", {});
|
|
2586
2706
|
}
|
|
@@ -2627,7 +2747,8 @@ var McpTestClient = class {
|
|
|
2627
2747
|
debug: this.config.debug,
|
|
2628
2748
|
interceptors: this._interceptors,
|
|
2629
2749
|
clientInfo: this.config.clientInfo,
|
|
2630
|
-
elicitationHandler: this._elicitationHandler
|
|
2750
|
+
elicitationHandler: this._elicitationHandler,
|
|
2751
|
+
notificationHandler: this.recordNotification
|
|
2631
2752
|
});
|
|
2632
2753
|
case "sse":
|
|
2633
2754
|
throw new Error("SSE transport not yet implemented");
|
package/esm/setup.mjs
CHANGED
|
@@ -397,18 +397,17 @@ var init_mcp_matchers = __esm({
|
|
|
397
397
|
message: () => `Expected a result wrapper object with isError property`
|
|
398
398
|
};
|
|
399
399
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
400
|
+
const receivedCode = typeof expectedCode === "string" ? result.raw?._meta?.["code"] : result.error?.code;
|
|
401
|
+
const codeMatches = expectedCode === void 0 || receivedCode === expectedCode;
|
|
402
|
+
const pass = result.isError && codeMatches;
|
|
404
403
|
return {
|
|
405
404
|
pass,
|
|
406
405
|
message: () => {
|
|
407
406
|
if (!result.isError) {
|
|
408
407
|
return "Expected result to be an error, but it was successful";
|
|
409
408
|
}
|
|
410
|
-
if (
|
|
411
|
-
return `Expected error code ${expectedCode}, but got ${
|
|
409
|
+
if (!codeMatches) {
|
|
410
|
+
return `Expected error code ${expectedCode}, but got ${String(receivedCode)}`;
|
|
412
411
|
}
|
|
413
412
|
return "Expected result not to be an error";
|
|
414
413
|
}
|