@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/perf/index.js
CHANGED
|
@@ -1308,8 +1308,453 @@ function isInterceptedError(err) {
|
|
|
1308
1308
|
return typeof err === "object" && err !== null && interceptedErrors.has(err);
|
|
1309
1309
|
}
|
|
1310
1310
|
|
|
1311
|
-
// libs/testing/src/transport/
|
|
1311
|
+
// libs/testing/src/transport/sse.transport.ts
|
|
1312
1312
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1313
|
+
var SseTransport = class {
|
|
1314
|
+
baseUrl;
|
|
1315
|
+
entryPath;
|
|
1316
|
+
timeout;
|
|
1317
|
+
authHeaders;
|
|
1318
|
+
publicMode;
|
|
1319
|
+
debug;
|
|
1320
|
+
clientInfo;
|
|
1321
|
+
notificationHandler;
|
|
1322
|
+
interceptors;
|
|
1323
|
+
elicitationHandler;
|
|
1324
|
+
state = "disconnected";
|
|
1325
|
+
authToken;
|
|
1326
|
+
sessionId;
|
|
1327
|
+
messageEndpoint;
|
|
1328
|
+
stream;
|
|
1329
|
+
pending = /* @__PURE__ */ new Map();
|
|
1330
|
+
connectionCount = 0;
|
|
1331
|
+
reconnectCount = 0;
|
|
1332
|
+
lastRequestHeaders = {};
|
|
1333
|
+
constructor(config) {
|
|
1334
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
1335
|
+
this.entryPath = config.entryPath;
|
|
1336
|
+
this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
|
|
1337
|
+
this.authHeaders = config.auth?.headers ?? {};
|
|
1338
|
+
this.authToken = config.auth?.token;
|
|
1339
|
+
this.publicMode = config.publicMode ?? false;
|
|
1340
|
+
this.debug = config.debug ?? false;
|
|
1341
|
+
this.clientInfo = config.clientInfo;
|
|
1342
|
+
this.notificationHandler = config.notificationHandler;
|
|
1343
|
+
this.interceptors = config.interceptors;
|
|
1344
|
+
this.elicitationHandler = config.elicitationHandler;
|
|
1345
|
+
}
|
|
1346
|
+
async connect() {
|
|
1347
|
+
this.state = "connecting";
|
|
1348
|
+
this.connectionCount++;
|
|
1349
|
+
try {
|
|
1350
|
+
if (!this.publicMode && !this.authToken) {
|
|
1351
|
+
await this.requestAnonymousToken();
|
|
1352
|
+
}
|
|
1353
|
+
await this.openStream();
|
|
1354
|
+
this.state = "connected";
|
|
1355
|
+
this.log(`Connected; messages go to ${this.messageEndpoint}`);
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
this.state = "error";
|
|
1358
|
+
this.stream?.abort();
|
|
1359
|
+
this.stream = void 0;
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async request(message) {
|
|
1364
|
+
this.ensureConnected();
|
|
1365
|
+
const startTime = Date.now();
|
|
1366
|
+
if (this.interceptors) {
|
|
1367
|
+
const intercepted = await this.interceptors.processRequest(message, {
|
|
1368
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1369
|
+
transport: "sse",
|
|
1370
|
+
sessionId: this.sessionId
|
|
1371
|
+
});
|
|
1372
|
+
switch (intercepted.type) {
|
|
1373
|
+
case "mock":
|
|
1374
|
+
return await this.interceptors.processResponse(
|
|
1375
|
+
message,
|
|
1376
|
+
intercepted.response,
|
|
1377
|
+
Date.now() - startTime
|
|
1378
|
+
);
|
|
1379
|
+
case "error":
|
|
1380
|
+
throw markInterceptedError(intercepted.error);
|
|
1381
|
+
case "continue":
|
|
1382
|
+
message = intercepted.request;
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
let response;
|
|
1387
|
+
if (message.id === void 0) {
|
|
1388
|
+
await this.post(JSON.stringify(message));
|
|
1389
|
+
response = { jsonrpc: "2.0", id: null, result: void 0 };
|
|
1390
|
+
} else {
|
|
1391
|
+
response = await this.postAndAwait(message.id, JSON.stringify(message));
|
|
1392
|
+
}
|
|
1393
|
+
if (this.interceptors) {
|
|
1394
|
+
response = await this.interceptors.processResponse(message, response, Date.now() - startTime);
|
|
1395
|
+
}
|
|
1396
|
+
return response;
|
|
1397
|
+
}
|
|
1398
|
+
async notify(message) {
|
|
1399
|
+
this.ensureConnected();
|
|
1400
|
+
const result = await this.post(JSON.stringify(message));
|
|
1401
|
+
if (!result.ok) this.log(`HTTP ${result.status} on notification: ${result.body}`);
|
|
1402
|
+
}
|
|
1403
|
+
async sendRaw(data) {
|
|
1404
|
+
this.ensureConnected();
|
|
1405
|
+
let id;
|
|
1406
|
+
try {
|
|
1407
|
+
const parsed = JSON.parse(data);
|
|
1408
|
+
if (typeof parsed.id === "string" || typeof parsed.id === "number") id = parsed.id;
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
if (id !== void 0) return this.postAndAwait(id, data);
|
|
1412
|
+
const result = await this.post(data);
|
|
1413
|
+
if (!result.ok) {
|
|
1414
|
+
return { jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error", data: result.body } };
|
|
1415
|
+
}
|
|
1416
|
+
return { jsonrpc: "2.0", id: null, result: void 0 };
|
|
1417
|
+
}
|
|
1418
|
+
async close() {
|
|
1419
|
+
this.closeStream("Transport closed");
|
|
1420
|
+
this.state = "disconnected";
|
|
1421
|
+
this.sessionId = void 0;
|
|
1422
|
+
this.messageEndpoint = void 0;
|
|
1423
|
+
this.log("SSE transport closed");
|
|
1424
|
+
}
|
|
1425
|
+
isConnected() {
|
|
1426
|
+
return this.state === "connected";
|
|
1427
|
+
}
|
|
1428
|
+
getState() {
|
|
1429
|
+
return this.state;
|
|
1430
|
+
}
|
|
1431
|
+
getSessionId() {
|
|
1432
|
+
return this.sessionId;
|
|
1433
|
+
}
|
|
1434
|
+
setAuthToken(token) {
|
|
1435
|
+
this.authToken = token;
|
|
1436
|
+
}
|
|
1437
|
+
setTimeout(ms) {
|
|
1438
|
+
this.timeout = ms;
|
|
1439
|
+
}
|
|
1440
|
+
getMessageEndpoint() {
|
|
1441
|
+
return this.messageEndpoint;
|
|
1442
|
+
}
|
|
1443
|
+
getConnectionCount() {
|
|
1444
|
+
return this.connectionCount;
|
|
1445
|
+
}
|
|
1446
|
+
getReconnectCount() {
|
|
1447
|
+
return this.reconnectCount;
|
|
1448
|
+
}
|
|
1449
|
+
getLastRequestHeaders() {
|
|
1450
|
+
return { ...this.lastRequestHeaders };
|
|
1451
|
+
}
|
|
1452
|
+
setInterceptors(interceptors2) {
|
|
1453
|
+
this.interceptors = interceptors2;
|
|
1454
|
+
}
|
|
1455
|
+
getInterceptors() {
|
|
1456
|
+
return this.interceptors;
|
|
1457
|
+
}
|
|
1458
|
+
setElicitationHandler(handler) {
|
|
1459
|
+
this.elicitationHandler = handler;
|
|
1460
|
+
}
|
|
1461
|
+
async simulateDisconnect() {
|
|
1462
|
+
this.closeStream("Simulated disconnect");
|
|
1463
|
+
this.state = "disconnected";
|
|
1464
|
+
this.sessionId = void 0;
|
|
1465
|
+
this.messageEndpoint = void 0;
|
|
1466
|
+
}
|
|
1467
|
+
async waitForReconnect(timeoutMs) {
|
|
1468
|
+
this.reconnectCount++;
|
|
1469
|
+
let timer;
|
|
1470
|
+
try {
|
|
1471
|
+
await Promise.race([
|
|
1472
|
+
this.connect(),
|
|
1473
|
+
new Promise((_, reject) => {
|
|
1474
|
+
timer = setTimeout(() => reject(new Error("Timeout waiting for reconnection")), timeoutMs);
|
|
1475
|
+
})
|
|
1476
|
+
]);
|
|
1477
|
+
} finally {
|
|
1478
|
+
clearTimeout(timer);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1482
|
+
// STREAM
|
|
1483
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1484
|
+
/** Open `GET <entry>/sse` and wait for the `endpoint` event. */
|
|
1485
|
+
async openStream() {
|
|
1486
|
+
const controller = new AbortController();
|
|
1487
|
+
this.stream = controller;
|
|
1488
|
+
const url = this.sseUrl();
|
|
1489
|
+
const { "Content-Type": _contentType, ...headers } = this.buildHeaders();
|
|
1490
|
+
this.log(`GET ${url}`);
|
|
1491
|
+
const connectTimer = setTimeout(() => controller.abort(), this.timeout);
|
|
1492
|
+
let response;
|
|
1493
|
+
try {
|
|
1494
|
+
response = await fetch(url, {
|
|
1495
|
+
method: "GET",
|
|
1496
|
+
headers: { ...headers, Accept: "text/event-stream" },
|
|
1497
|
+
signal: controller.signal
|
|
1498
|
+
});
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
clearTimeout(connectTimer);
|
|
1501
|
+
throw new Error(
|
|
1502
|
+
isAbortError(error) ? `SSE connection to ${url} timed out after ${this.timeout}ms` : `SSE connection to ${url} failed: ${errorMessage(error)}`,
|
|
1503
|
+
{ cause: error }
|
|
1504
|
+
);
|
|
1505
|
+
}
|
|
1506
|
+
const isEventStream = response.headers.get("content-type")?.includes("text/event-stream") ?? false;
|
|
1507
|
+
if (!response.ok || !response.body || !isEventStream) {
|
|
1508
|
+
clearTimeout(connectTimer);
|
|
1509
|
+
const body = await response.text().catch(() => "");
|
|
1510
|
+
throw new Error(
|
|
1511
|
+
`SSE connection to ${url} failed: HTTP ${response.status} ${response.statusText}${body ? ` \u2014 ${body}` : ""}. The legacy HTTP+SSE transport needs a server that serves it (@FrontMcp({ transport: { protocol: { legacy: true } } })).`
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
const endpoint = new Promise((resolve, reject) => {
|
|
1515
|
+
const reader = response.body?.getReader();
|
|
1516
|
+
if (!reader) {
|
|
1517
|
+
reject(new Error(`SSE connection to ${url} has no body`));
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
void this.readStream(controller, reader, resolve, reject);
|
|
1521
|
+
});
|
|
1522
|
+
try {
|
|
1523
|
+
const data = await endpoint;
|
|
1524
|
+
const messageUrl = new URL(data, url);
|
|
1525
|
+
this.messageEndpoint = messageUrl.toString();
|
|
1526
|
+
this.sessionId = messageUrl.searchParams.get("sessionId") ?? void 0;
|
|
1527
|
+
} finally {
|
|
1528
|
+
clearTimeout(connectTimer);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
async readStream(controller, reader, onEndpoint, onEndpointError) {
|
|
1532
|
+
const decoder = new TextDecoder();
|
|
1533
|
+
let buffer = "";
|
|
1534
|
+
let endpointSeen = false;
|
|
1535
|
+
try {
|
|
1536
|
+
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
1537
|
+
const { completeEvents, remainder } = splitSSEEvents(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
1538
|
+
buffer = remainder;
|
|
1539
|
+
for (const text of completeEvents) {
|
|
1540
|
+
const event = parseSSEEvent(text);
|
|
1541
|
+
if (!event) continue;
|
|
1542
|
+
if (event.type === "endpoint") {
|
|
1543
|
+
if (!endpointSeen) {
|
|
1544
|
+
endpointSeen = true;
|
|
1545
|
+
onEndpoint(event.data);
|
|
1546
|
+
}
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
await this.handleMessage(event.data);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
if (!controller.signal.aborted) this.log("SSE stream failed:", error);
|
|
1554
|
+
} finally {
|
|
1555
|
+
reader.releaseLock();
|
|
1556
|
+
}
|
|
1557
|
+
if (!endpointSeen) {
|
|
1558
|
+
onEndpointError(
|
|
1559
|
+
new Error(
|
|
1560
|
+
controller.signal.aborted ? `SSE stream at ${this.sseUrl()} sent no endpoint event within ${this.timeout}ms` : `SSE stream at ${this.sseUrl()} ended before the server sent its endpoint`
|
|
1561
|
+
)
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
if (this.stream === controller) {
|
|
1565
|
+
this.stream = void 0;
|
|
1566
|
+
this.rejectPending("SSE stream closed by the server");
|
|
1567
|
+
if (this.state === "connected") this.state = "disconnected";
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
async handleMessage(data) {
|
|
1571
|
+
let message;
|
|
1572
|
+
try {
|
|
1573
|
+
message = JSON.parse(data);
|
|
1574
|
+
} catch {
|
|
1575
|
+
this.log("Ignoring SSE event that is not JSON:", data);
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (!message || typeof message !== "object") return;
|
|
1579
|
+
if (typeof message.method === "string") {
|
|
1580
|
+
const request = message;
|
|
1581
|
+
if (request.id === void 0 || request.id === null) {
|
|
1582
|
+
this.notificationHandler?.(request);
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
await this.answerServerRequest(request);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (message.id !== void 0 && message.id !== null) {
|
|
1589
|
+
const pending = this.pending.get(message.id);
|
|
1590
|
+
if (pending) {
|
|
1591
|
+
clearTimeout(pending.timer);
|
|
1592
|
+
this.pending.delete(message.id);
|
|
1593
|
+
pending.resolve(message);
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
/** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
|
|
1598
|
+
async answerServerRequest(request) {
|
|
1599
|
+
const id = request.id;
|
|
1600
|
+
let reply;
|
|
1601
|
+
if (request.method === "elicitation/create") {
|
|
1602
|
+
reply = { jsonrpc: "2.0", id, result: await this.elicit(request.params) };
|
|
1603
|
+
} else if (request.method === "ping") {
|
|
1604
|
+
reply = { jsonrpc: "2.0", id, result: {} };
|
|
1605
|
+
} else {
|
|
1606
|
+
reply = { jsonrpc: "2.0", id, error: { code: -32601, message: `Method not found: ${request.method}` } };
|
|
1607
|
+
}
|
|
1608
|
+
const result = await this.post(JSON.stringify(reply));
|
|
1609
|
+
if (!result.ok) this.log(`HTTP ${result.status} answering ${request.method}: ${result.body}`);
|
|
1610
|
+
}
|
|
1611
|
+
async elicit(params) {
|
|
1612
|
+
if (!this.elicitationHandler) return { action: "decline" };
|
|
1613
|
+
try {
|
|
1614
|
+
return await this.elicitationHandler(params);
|
|
1615
|
+
} catch (error) {
|
|
1616
|
+
this.log("Elicitation handler error:", error);
|
|
1617
|
+
return { action: "cancel" };
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
closeStream(reason) {
|
|
1621
|
+
const controller = this.stream;
|
|
1622
|
+
this.stream = void 0;
|
|
1623
|
+
controller?.abort();
|
|
1624
|
+
this.rejectPending(reason);
|
|
1625
|
+
}
|
|
1626
|
+
rejectPending(reason) {
|
|
1627
|
+
for (const [id, pending] of this.pending) {
|
|
1628
|
+
clearTimeout(pending.timer);
|
|
1629
|
+
pending.resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: reason } });
|
|
1630
|
+
}
|
|
1631
|
+
this.pending.clear();
|
|
1632
|
+
}
|
|
1633
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1634
|
+
// POSTING
|
|
1635
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1636
|
+
/** POST a message and resolve with the response the stream carries for `id`. */
|
|
1637
|
+
async postAndAwait(id, body) {
|
|
1638
|
+
const answer = new Promise((resolve) => {
|
|
1639
|
+
const timer = setTimeout(() => {
|
|
1640
|
+
this.pending.delete(id);
|
|
1641
|
+
resolve({ jsonrpc: "2.0", id, error: { code: -32e3, message: `Request timeout after ${this.timeout}ms` } });
|
|
1642
|
+
}, this.timeout);
|
|
1643
|
+
this.pending.set(id, { resolve, timer });
|
|
1644
|
+
});
|
|
1645
|
+
const result = await this.post(body);
|
|
1646
|
+
if (!result.ok) {
|
|
1647
|
+
const pending = this.pending.get(id);
|
|
1648
|
+
if (pending) {
|
|
1649
|
+
clearTimeout(pending.timer);
|
|
1650
|
+
this.pending.delete(id);
|
|
1651
|
+
}
|
|
1652
|
+
return {
|
|
1653
|
+
jsonrpc: "2.0",
|
|
1654
|
+
id,
|
|
1655
|
+
error: { code: -32e3, message: `HTTP ${result.status}: ${result.statusText}`, data: result.body }
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
return answer;
|
|
1659
|
+
}
|
|
1660
|
+
async post(body) {
|
|
1661
|
+
const endpoint = this.messageEndpoint;
|
|
1662
|
+
if (!endpoint) throw new Error("SSE transport has no message endpoint. Call connect() first.");
|
|
1663
|
+
const headers = this.buildHeaders();
|
|
1664
|
+
this.lastRequestHeaders = headers;
|
|
1665
|
+
this.log(`POST ${endpoint}`, body);
|
|
1666
|
+
const controller = new AbortController();
|
|
1667
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
1668
|
+
try {
|
|
1669
|
+
const response = await fetch(endpoint, { method: "POST", headers, body, signal: controller.signal });
|
|
1670
|
+
const text = await response.text();
|
|
1671
|
+
return { ok: response.ok, status: response.status, statusText: response.statusText, body: text };
|
|
1672
|
+
} catch (error) {
|
|
1673
|
+
if (isAbortError(error)) {
|
|
1674
|
+
return { ok: false, status: 0, statusText: `timeout after ${this.timeout}ms`, body: "" };
|
|
1675
|
+
}
|
|
1676
|
+
throw error;
|
|
1677
|
+
} finally {
|
|
1678
|
+
clearTimeout(timer);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1682
|
+
// HELPERS
|
|
1683
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1684
|
+
async requestAnonymousToken() {
|
|
1685
|
+
const url = new URL(this.baseUrl);
|
|
1686
|
+
url.pathname = `${url.pathname.replace(/\/+$/, "")}/oauth/token`;
|
|
1687
|
+
url.search = "";
|
|
1688
|
+
const tokenUrl = url.toString();
|
|
1689
|
+
try {
|
|
1690
|
+
const response = await fetch(tokenUrl, {
|
|
1691
|
+
method: "POST",
|
|
1692
|
+
headers: { "Content-Type": "application/json" },
|
|
1693
|
+
body: JSON.stringify({ grant_type: "anonymous", client_id: crypto.randomUUID(), resource: this.baseUrl })
|
|
1694
|
+
});
|
|
1695
|
+
if (!response.ok) {
|
|
1696
|
+
this.log(`No anonymous token (HTTP ${response.status})`);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const token = await response.json();
|
|
1700
|
+
if (token.access_token) this.authToken = token.access_token;
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
this.log("Error requesting anonymous token:", error);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
/** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
|
|
1706
|
+
sseUrl() {
|
|
1707
|
+
const url = new URL(this.baseUrl);
|
|
1708
|
+
const entry = (this.entryPath ?? "").replace(/^\/+|\/+$/g, "");
|
|
1709
|
+
const basePath = url.pathname.replace(/\/+$/, "");
|
|
1710
|
+
url.pathname = entry ? `${basePath}/${entry}/sse` : `${basePath}/sse`;
|
|
1711
|
+
return url.toString();
|
|
1712
|
+
}
|
|
1713
|
+
buildHeaders() {
|
|
1714
|
+
const headers = {
|
|
1715
|
+
"Content-Type": "application/json",
|
|
1716
|
+
Accept: "application/json, text/event-stream"
|
|
1717
|
+
};
|
|
1718
|
+
if (this.clientInfo) headers["User-Agent"] = `${this.clientInfo.name}/${this.clientInfo.version}`;
|
|
1719
|
+
if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
1720
|
+
if (this.sessionId) headers["mcp-session-id"] = this.sessionId;
|
|
1721
|
+
Object.assign(headers, this.authHeaders);
|
|
1722
|
+
return headers;
|
|
1723
|
+
}
|
|
1724
|
+
ensureConnected() {
|
|
1725
|
+
if (this.state !== "connected") {
|
|
1726
|
+
throw new Error("Transport not connected. Call connect() first.");
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
log(message, data) {
|
|
1730
|
+
if (this.debug) {
|
|
1731
|
+
console.log(`[SSE] ${message}`, data ?? "");
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
};
|
|
1735
|
+
function splitSSEEvents(buffer) {
|
|
1736
|
+
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
1737
|
+
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
1738
|
+
const completeEvents = normalized.split("\n\n");
|
|
1739
|
+
const remainder = completeEvents.pop() ?? "";
|
|
1740
|
+
return { completeEvents, remainder: endsWithCarriageReturn ? `${remainder}\r` : remainder };
|
|
1741
|
+
}
|
|
1742
|
+
function parseSSEEvent(text) {
|
|
1743
|
+
let type = "message";
|
|
1744
|
+
const data = [];
|
|
1745
|
+
for (const line of text.split("\n")) {
|
|
1746
|
+
if (line.startsWith(":")) continue;
|
|
1747
|
+
const colon = line.indexOf(":");
|
|
1748
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
1749
|
+
const value = colon === -1 ? "" : line.slice(colon + 1).replace(/^ /, "");
|
|
1750
|
+
if (field === "event") type = value;
|
|
1751
|
+
else if (field === "data") data.push(value);
|
|
1752
|
+
}
|
|
1753
|
+
return data.length > 0 ? { type, data: data.join("\n") } : void 0;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// libs/testing/src/transport/streamable-http.transport.ts
|
|
1757
|
+
var DEFAULT_TIMEOUT2 = 3e4;
|
|
1313
1758
|
var NOTIFICATION_STREAM_REOPEN_DELAY_MS = 250;
|
|
1314
1759
|
var StreamableHttpTransport = class {
|
|
1315
1760
|
config;
|
|
@@ -1331,7 +1776,7 @@ var StreamableHttpTransport = class {
|
|
|
1331
1776
|
baseUrl: config.baseUrl.replace(/\/$/, ""),
|
|
1332
1777
|
// Remove trailing slash
|
|
1333
1778
|
entryPath: config.entryPath,
|
|
1334
|
-
timeout: config.timeout ??
|
|
1779
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT2,
|
|
1335
1780
|
auth: config.auth ?? {},
|
|
1336
1781
|
publicMode: config.publicMode ?? false,
|
|
1337
1782
|
debug: config.debug ?? false,
|
|
@@ -1671,7 +2116,7 @@ var StreamableHttpTransport = class {
|
|
|
1671
2116
|
let buffer = "";
|
|
1672
2117
|
try {
|
|
1673
2118
|
for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
|
|
1674
|
-
const { completeEvents, remainder } =
|
|
2119
|
+
const { completeEvents, remainder } = splitSSEEvents2(buffer + decoder.decode(chunk.value, { stream: true }));
|
|
1675
2120
|
buffer = remainder;
|
|
1676
2121
|
for (const eventText of completeEvents) {
|
|
1677
2122
|
for (const event of this.parseSSEEvents(eventText, void 0).events) {
|
|
@@ -2005,7 +2450,7 @@ var StreamableHttpTransport = class {
|
|
|
2005
2450
|
};
|
|
2006
2451
|
}
|
|
2007
2452
|
};
|
|
2008
|
-
function
|
|
2453
|
+
function splitSSEEvents2(buffer) {
|
|
2009
2454
|
const endsWithCarriageReturn = buffer.endsWith("\r");
|
|
2010
2455
|
const normalized = (endsWithCarriageReturn ? buffer.slice(0, -1) : buffer).replace(/\r\n?/g, "\n");
|
|
2011
2456
|
const completeEvents = normalized.split("\n\n");
|
|
@@ -2243,7 +2688,7 @@ var McpTestClientBuilder = class {
|
|
|
2243
2688
|
};
|
|
2244
2689
|
|
|
2245
2690
|
// libs/testing/src/client/mcp-test-client.ts
|
|
2246
|
-
var
|
|
2691
|
+
var DEFAULT_TIMEOUT3 = 3e4;
|
|
2247
2692
|
var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
2248
2693
|
var UNSUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28"];
|
|
2249
2694
|
var DEFAULT_CLIENT_INFO = {
|
|
@@ -2313,7 +2758,7 @@ var McpTestClient = class {
|
|
|
2313
2758
|
transport: config.transport ?? "streamable-http",
|
|
2314
2759
|
auth: config.auth ?? {},
|
|
2315
2760
|
publicMode: config.publicMode ?? false,
|
|
2316
|
-
timeout: config.timeout ??
|
|
2761
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT3,
|
|
2317
2762
|
debug: config.debug ?? false,
|
|
2318
2763
|
protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
|
|
2319
2764
|
clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
|
|
@@ -3079,7 +3524,18 @@ var McpTestClient = class {
|
|
|
3079
3524
|
notificationHandler: this.recordNotification
|
|
3080
3525
|
});
|
|
3081
3526
|
case "sse":
|
|
3082
|
-
|
|
3527
|
+
return new SseTransport({
|
|
3528
|
+
baseUrl,
|
|
3529
|
+
entryPath: this.config.entryPath,
|
|
3530
|
+
timeout: this.config.timeout,
|
|
3531
|
+
auth: this.config.auth,
|
|
3532
|
+
publicMode: this.config.publicMode,
|
|
3533
|
+
debug: this.config.debug,
|
|
3534
|
+
interceptors: this._interceptors,
|
|
3535
|
+
clientInfo: this.config.clientInfo,
|
|
3536
|
+
elicitationHandler: this._elicitationHandler,
|
|
3537
|
+
notificationHandler: this.recordNotification
|
|
3538
|
+
});
|
|
3083
3539
|
default:
|
|
3084
3540
|
throw new Error(`Unknown transport type: ${this.config.transport}`);
|
|
3085
3541
|
}
|
package/transport/index.d.ts
CHANGED
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export type { McpTransport, TransportConfig, TransportState, JsonRpcRequest, JsonRpcResponse, NotificationHandler, } from './transport.interface';
|
|
6
6
|
export { StreamableHttpTransport } from './streamable-http.transport';
|
|
7
|
+
export { SseTransport } from './sse.transport';
|
|
7
8
|
//# sourceMappingURL=index.d.ts.map
|
package/transport/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transport/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EACV,YAAY,EACZ,eAAe,EACf,cAAc,EACd,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/transport/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,YAAY,EACV,YAAY,EACZ,eAAe,EACf,cAAc,EACd,cAAc,EACd,eAAe,EACf,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file sse.transport.ts
|
|
3
|
+
* @description Legacy HTTP+SSE transport (MCP 2024-11-05) for the MCP Test Client
|
|
4
|
+
*
|
|
5
|
+
* The client opens a long-lived `GET <entry>/sse` stream. The server's first event, `endpoint`,
|
|
6
|
+
* names the URL to POST JSON-RPC messages to (`<entry>/message?sessionId=…`). Every POST is
|
|
7
|
+
* answered `202 Accepted`; the JSON-RPC response arrives later as a `message` event on the
|
|
8
|
+
* stream, as do notifications and server→client requests (e.g. `elicitation/create`).
|
|
9
|
+
*/
|
|
10
|
+
import type { ElicitationHandler } from '../client/mcp-test-client.types';
|
|
11
|
+
import type { InterceptorChain } from '../interceptor';
|
|
12
|
+
import type { JsonRpcRequest, JsonRpcResponse, McpTransport, TransportConfig, TransportState } from './transport.interface';
|
|
13
|
+
/**
|
|
14
|
+
* Legacy HTTP+SSE transport.
|
|
15
|
+
*
|
|
16
|
+
* Needs a server that serves the legacy SSE endpoints (`transport.protocol` with `legacy: true`,
|
|
17
|
+
* or `sse: true`). Requests are correlated with their responses by JSON-RPC id.
|
|
18
|
+
*/
|
|
19
|
+
export declare class SseTransport implements McpTransport {
|
|
20
|
+
private readonly baseUrl;
|
|
21
|
+
private readonly entryPath?;
|
|
22
|
+
private timeout;
|
|
23
|
+
private readonly authHeaders;
|
|
24
|
+
private readonly publicMode;
|
|
25
|
+
private readonly debug;
|
|
26
|
+
private readonly clientInfo?;
|
|
27
|
+
private readonly notificationHandler?;
|
|
28
|
+
private interceptors?;
|
|
29
|
+
private elicitationHandler?;
|
|
30
|
+
private state;
|
|
31
|
+
private authToken;
|
|
32
|
+
private sessionId;
|
|
33
|
+
private messageEndpoint;
|
|
34
|
+
private stream;
|
|
35
|
+
private readonly pending;
|
|
36
|
+
private connectionCount;
|
|
37
|
+
private reconnectCount;
|
|
38
|
+
private lastRequestHeaders;
|
|
39
|
+
constructor(config: TransportConfig);
|
|
40
|
+
connect(): Promise<void>;
|
|
41
|
+
request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & {
|
|
42
|
+
result?: T;
|
|
43
|
+
}>;
|
|
44
|
+
notify(message: JsonRpcRequest): Promise<void>;
|
|
45
|
+
sendRaw(data: string): Promise<JsonRpcResponse>;
|
|
46
|
+
close(): Promise<void>;
|
|
47
|
+
isConnected(): boolean;
|
|
48
|
+
getState(): TransportState;
|
|
49
|
+
getSessionId(): string | undefined;
|
|
50
|
+
setAuthToken(token: string): void;
|
|
51
|
+
setTimeout(ms: number): void;
|
|
52
|
+
getMessageEndpoint(): string | undefined;
|
|
53
|
+
getConnectionCount(): number;
|
|
54
|
+
getReconnectCount(): number;
|
|
55
|
+
getLastRequestHeaders(): Record<string, string>;
|
|
56
|
+
setInterceptors(interceptors: InterceptorChain): void;
|
|
57
|
+
getInterceptors(): InterceptorChain | undefined;
|
|
58
|
+
setElicitationHandler(handler: ElicitationHandler | undefined): void;
|
|
59
|
+
simulateDisconnect(): Promise<void>;
|
|
60
|
+
waitForReconnect(timeoutMs: number): Promise<void>;
|
|
61
|
+
/** Open `GET <entry>/sse` and wait for the `endpoint` event. */
|
|
62
|
+
private openStream;
|
|
63
|
+
private readStream;
|
|
64
|
+
private handleMessage;
|
|
65
|
+
/** Server→client requests: elicitation goes to the handler, `ping` is answered, the rest refused. */
|
|
66
|
+
private answerServerRequest;
|
|
67
|
+
private elicit;
|
|
68
|
+
private closeStream;
|
|
69
|
+
private rejectPending;
|
|
70
|
+
/** POST a message and resolve with the response the stream carries for `id`. */
|
|
71
|
+
private postAndAwait;
|
|
72
|
+
private post;
|
|
73
|
+
private requestAnonymousToken;
|
|
74
|
+
/** `<baseUrl><entryPath>/sse`, built from URL parts so query params on `baseUrl` survive. */
|
|
75
|
+
private sseUrl;
|
|
76
|
+
private buildHeaders;
|
|
77
|
+
private ensureConnected;
|
|
78
|
+
private log;
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=sse.transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.transport.d.ts","sourceRoot":"","sources":["../../src/transport/sse.transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAwC,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AAChH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EAEZ,eAAe,EACf,cAAc,EACf,MAAM,uBAAuB,CAAC;AAc/B;;;;;GAKG;AACH,qBAAa,YAAa,YAAW,YAAY;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAS;IACpC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAyB;IACrD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAU;IAChC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAa;IACzC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,CAAsB;IAC3D,OAAO,CAAC,YAAY,CAAC,CAAmB;IACxC,OAAO,CAAC,kBAAkB,CAAC,CAAqB;IAEhD,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8C;IACtE,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,kBAAkB,CAA8B;gBAE5C,MAAM,EAAE,eAAe;IAc7B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAkBxB,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,GAAG;QAAE,MAAM,CAAC,EAAE,CAAC,CAAA;KAAE,CAAC;IAuCxF,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAkB/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ5B,WAAW,IAAI,OAAO;IAItB,QAAQ,IAAI,cAAc;IAI1B,YAAY,IAAI,MAAM,GAAG,SAAS;IAIlC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAIjC,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAI5B,kBAAkB,IAAI,MAAM,GAAG,SAAS;IAIxC,kBAAkB,IAAI,MAAM;IAI5B,iBAAiB,IAAI,MAAM;IAI3B,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAI/C,eAAe,CAAC,YAAY,EAAE,gBAAgB,GAAG,IAAI;IAIrD,eAAe,IAAI,gBAAgB,GAAG,SAAS;IAI/C,qBAAqB,CAAC,OAAO,EAAE,kBAAkB,GAAG,SAAS,GAAG,IAAI;IAI9D,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOnC,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBxD,gEAAgE;YAClD,UAAU;YAsDV,UAAU;YAgDV,aAAa;IA8B3B,qGAAqG;YACvF,mBAAmB;YAcnB,MAAM;IAYpB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,aAAa;IAYrB,gFAAgF;YAClE,YAAY;YAyBZ,IAAI;YA2BJ,qBAAqB;IAuBnC,6FAA6F;IAC7F,OAAO,CAAC,MAAM;IAQd,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,GAAG;CAKZ"}
|