@roxybrowser/playwright 2.0.2-beta.1 → 2.0.2-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/browser.d.ts +33 -5
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +127 -15
  4. package/dist/browser.js.map +1 -1
  5. package/dist/browserType.d.ts.map +1 -1
  6. package/dist/browserType.js +2 -1
  7. package/dist/browserType.js.map +1 -1
  8. package/dist/mcp/backend/connect.d.ts +1 -0
  9. package/dist/mcp/backend/connect.d.ts.map +1 -1
  10. package/dist/mcp/backend/connect.js +4 -2
  11. package/dist/mcp/backend/connect.js.map +1 -1
  12. package/dist/mcp/backend/network.d.ts.map +1 -1
  13. package/dist/mcp/backend/network.js +30 -4
  14. package/dist/mcp/backend/network.js.map +1 -1
  15. package/dist/mcp/backend/response.d.ts +2 -0
  16. package/dist/mcp/backend/response.d.ts.map +1 -1
  17. package/dist/mcp/backend/response.js +52 -6
  18. package/dist/mcp/backend/response.js.map +1 -1
  19. package/dist/mcp/connectedBrowser.d.ts.map +1 -1
  20. package/dist/mcp/connectedBrowser.js +355 -25
  21. package/dist/mcp/connectedBrowser.js.map +1 -1
  22. package/dist/mcp/runtime.d.ts +3 -0
  23. package/dist/mcp/runtime.d.ts.map +1 -1
  24. package/dist/mcp/runtime.js +62 -8
  25. package/dist/mcp/runtime.js.map +1 -1
  26. package/dist/mcp/transports/inMemory.d.ts.map +1 -1
  27. package/dist/mcp/transports/inMemory.js +1 -0
  28. package/dist/mcp/transports/inMemory.js.map +1 -1
  29. package/dist/mcp/types.d.ts +5 -0
  30. package/dist/mcp/types.d.ts.map +1 -1
  31. package/dist/page.d.ts +2 -0
  32. package/dist/page.d.ts.map +1 -1
  33. package/dist/page.js +12 -0
  34. package/dist/page.js.map +1 -1
  35. package/dist/protocol/bidi/backend.d.ts +1 -1
  36. package/dist/protocol/bidi/backend.d.ts.map +1 -1
  37. package/dist/protocol/bidi/backend.js +6 -2
  38. package/dist/protocol/bidi/backend.js.map +1 -1
  39. package/dist/roxybrowser.bundle.js +516 -60
  40. package/dist/roxybrowser.bundle.js.map +1 -1
  41. package/dist/types/api.d.ts +21 -2
  42. package/dist/types/api.d.ts.map +1 -1
  43. package/package.json +1 -1
@@ -4,6 +4,7 @@ import * as cdpModule from "chrome-remote-interface";
4
4
  import { normalizeAriaSnapshotOptions, retryUntilReady } from "../ariaSnapshot.js";
5
5
  import { PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE as ARIA_SNAPSHOT_EVALUATE_SOURCE } from "../vendor/playwright/ariaSnapshotEvaluate.js";
6
6
  import { getBidiClientFactory } from "../protocol/bidi/client.js";
7
+ import { parseSerializedEvaluationResult, wrapWithSerializedEvaluationResult } from "../protocol/evaluationSerializer.js";
7
8
  import { McpToolError } from "./errors.js";
8
9
  import { ACTION_POINT_EVALUATE_SOURCE, ACTION_POINT_BY_SELECTOR_SOURCE } from "./snapshot.js";
9
10
  function delay(ms) {
@@ -351,7 +352,7 @@ async function evaluateBiDi(client, contextId, functionSource, arg) {
351
352
  ? `(${functionSource})()`
352
353
  : `(${functionSource})(${JSON.stringify(arg)})`;
353
354
  const response = (await client.scriptEvaluate({
354
- expression,
355
+ expression: wrapWithSerializedEvaluationResult(expression),
355
356
  target: {
356
357
  context: contextId
357
358
  },
@@ -361,7 +362,7 @@ async function evaluateBiDi(client, contextId, functionSource, arg) {
361
362
  if (response.type === "exception") {
362
363
  throw new Error(response.exceptionDetails?.text || "BiDi runtime evaluation failed.");
363
364
  }
364
- return response.result?.value;
365
+ return parseSerializedEvaluationResult(extractBiDiValue(response.result));
365
366
  }
366
367
  async function evaluateBiDiRef(client, contextId, functionSource, arg) {
367
368
  const expression = arg === undefined
@@ -389,6 +390,22 @@ async function evaluateBiDiRef(client, contextId, functionSource, arg) {
389
390
  ...(response.result?.value?.handle !== undefined ? { handle: response.result.value.handle } : {})
390
391
  };
391
392
  }
393
+ function extractBiDiValue(value) {
394
+ if (!value) {
395
+ return undefined;
396
+ }
397
+ if (value.type === "array" && Array.isArray(value.value)) {
398
+ return value.value.map((entry) => extractBiDiValue(entry));
399
+ }
400
+ if (value.type === "object" && Array.isArray(value.value)) {
401
+ const obj = {};
402
+ for (const [key, val] of value.value) {
403
+ obj[key] = extractBiDiValue(val);
404
+ }
405
+ return obj;
406
+ }
407
+ return value.value;
408
+ }
392
409
  function toAriaSnapshotPayload(request = {}) {
393
410
  return {
394
411
  options: normalizeAriaSnapshotOptions({
@@ -432,6 +449,7 @@ class CdpConnectedBrowserSession {
432
449
  pageConsoleStates = new Map();
433
450
  pageNetworkStates = new Map();
434
451
  pageDialogStates = new Map();
452
+ dialogWaiters = new Map();
435
453
  activeTabId;
436
454
  versionString = "Chromium/unknown";
437
455
  constructor(browserClient, connection) {
@@ -452,10 +470,7 @@ class CdpConnectedBrowserSession {
452
470
  });
453
471
  const session = new CdpConnectedBrowserSession(browserClient, connection);
454
472
  session.versionString = version.Browser;
455
- const tabs = await session.refreshTabs();
456
- if (tabs.length === 0) {
457
- await session.newTab();
458
- }
473
+ await session.refreshTabs();
459
474
  await session.getActivePageClient().catch(() => undefined);
460
475
  return session;
461
476
  }
@@ -519,6 +534,7 @@ class CdpConnectedBrowserSession {
519
534
  async click(target, options) {
520
535
  const pageClient = await this.getActivePageClient();
521
536
  const contextId = await this.getActiveUtilityContextId(pageClient);
537
+ const tabId = await this.getActiveTabId();
522
538
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
523
539
  const arg = "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
524
540
  const point = await evaluateCdp(pageClient, source, arg, contextId);
@@ -552,7 +568,7 @@ class CdpConnectedBrowserSession {
552
568
  modifiers: modifiersMask
553
569
  });
554
570
  await delay(options.clickHoldMs);
555
- await pageClient.Input.dispatchMouseEvent({
571
+ const releasePromise = pageClient.Input.dispatchMouseEvent({
556
572
  type: "mouseReleased",
557
573
  x: point.x,
558
574
  y: point.y,
@@ -560,6 +576,10 @@ class CdpConnectedBrowserSession {
560
576
  clickCount,
561
577
  modifiers: modifiersMask
562
578
  });
579
+ await Promise.race([
580
+ releasePromise,
581
+ this.waitForDialog(tabId, options.clickHoldMs + 1000)
582
+ ]);
563
583
  }
564
584
  }
565
585
  async drag(start, end, options) {
@@ -816,26 +836,105 @@ class CdpConnectedBrowserSession {
816
836
  }
817
837
  }
818
838
  async handleDialog(accept, promptText) {
819
- const tabId = await this.getActiveTabId();
839
+ const tabId = this.dialogTabId();
820
840
  if (!this.pageDialogStates.has(tabId)) {
821
841
  throw new McpToolError("no_dialog", "No dialog visible.");
822
842
  }
823
- const pageClient = await this.getActivePageClient();
843
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
824
844
  this.pageDialogStates.delete(tabId);
825
845
  await pageClient.Page.handleJavaScriptDialog({
826
846
  accept,
827
847
  ...(promptText !== undefined ? { promptText } : {})
828
848
  });
829
849
  }
850
+ async hasDialog() {
851
+ return this.pageDialogStates.size > 0;
852
+ }
853
+ waitForDialog(tabId, timeoutMs) {
854
+ if (this.pageDialogStates.has(tabId)) {
855
+ return Promise.resolve();
856
+ }
857
+ return new Promise((resolve) => {
858
+ const waiter = {
859
+ resolve: () => {
860
+ if (waiter.timer) {
861
+ clearTimeout(waiter.timer);
862
+ }
863
+ this.removeDialogWaiter(tabId, waiter);
864
+ resolve();
865
+ }
866
+ };
867
+ waiter.timer = setTimeout(() => waiter.resolve(), timeoutMs);
868
+ const waiters = this.dialogWaiters.get(tabId) ?? new Set();
869
+ waiters.add(waiter);
870
+ this.dialogWaiters.set(tabId, waiters);
871
+ });
872
+ }
873
+ resolveDialogWaiters(tabId) {
874
+ const waiters = this.dialogWaiters.get(tabId);
875
+ if (!waiters) {
876
+ return;
877
+ }
878
+ this.dialogWaiters.delete(tabId);
879
+ for (const waiter of waiters) {
880
+ waiter.resolve();
881
+ }
882
+ }
883
+ removeDialogWaiter(tabId, waiter) {
884
+ const waiters = this.dialogWaiters.get(tabId);
885
+ if (!waiters) {
886
+ return;
887
+ }
888
+ waiters.delete(waiter);
889
+ if (waiters.size === 0) {
890
+ this.dialogWaiters.delete(tabId);
891
+ }
892
+ }
830
893
  async networkRequests() {
831
894
  const tabId = await this.getActiveTabId();
895
+ await this.hydratePerformanceResourceRequests(tabId);
832
896
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
833
897
  }
834
898
  async networkRequest(index) {
835
899
  const tabId = await this.getActiveTabId();
900
+ await this.hydratePerformanceResourceRequests(tabId);
836
901
  const request = this.ensureNetworkState(tabId).requests[index - 1];
837
902
  return request ? cloneNetworkRequest(request) : undefined;
838
903
  }
904
+ async fetchResponseBody(index) {
905
+ const tabId = await this.getActiveTabId();
906
+ const state = this.ensureNetworkState(tabId);
907
+ const request = state.requests[index - 1];
908
+ if (!request || !request.requestId) {
909
+ return request?.responseBody;
910
+ }
911
+ if (!canReadResponseBody(request)) {
912
+ return undefined;
913
+ }
914
+ if (request.responseBody !== undefined) {
915
+ return request.responseBody;
916
+ }
917
+ await waitForLoadingDone(state, request.requestId, 5_000).catch(() => undefined);
918
+ if (request.responseBody !== undefined) {
919
+ return request.responseBody;
920
+ }
921
+ if (state.bodyRead.has(request.requestId)) {
922
+ return undefined;
923
+ }
924
+ state.bodyRead.add(request.requestId);
925
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
926
+ const clientNetwork = pageClient.Network;
927
+ if (!clientNetwork) {
928
+ return undefined;
929
+ }
930
+ const body = await clientNetwork.getResponseBody({ requestId: request.requestId }).catch(() => undefined);
931
+ if (body) {
932
+ request.responseBody = body.base64Encoded
933
+ ? Buffer.from(body.body, "base64").toString("utf8")
934
+ : body.body;
935
+ }
936
+ return request.responseBody;
937
+ }
839
938
  async runCodeUnsafe(code) {
840
939
  const pageClient = await this.getActivePageClient();
841
940
  const contextId = await this.getActiveUtilityContextId(pageClient);
@@ -905,6 +1004,16 @@ class CdpConnectedBrowserSession {
905
1004
  }
906
1005
  return activeTab.id;
907
1006
  }
1007
+ dialogTabId() {
1008
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) {
1009
+ return this.activeTabId;
1010
+ }
1011
+ const tabId = this.pageDialogStates.keys().next().value;
1012
+ if (!tabId) {
1013
+ throw new McpToolError("no_dialog", "No dialog visible.");
1014
+ }
1015
+ return tabId;
1016
+ }
908
1017
  async getActivePageClient() {
909
1018
  const tabs = await this.refreshTabs();
910
1019
  const activeTab = tabs.find((tab) => tab.active);
@@ -999,6 +1108,7 @@ class CdpConnectedBrowserSession {
999
1108
  ...(event.defaultPrompt !== undefined ? { defaultPrompt: event.defaultPrompt } : {}),
1000
1109
  ...(event.url !== undefined ? { url: event.url } : {})
1001
1110
  });
1111
+ this.resolveDialogWaiters(tabId);
1002
1112
  });
1003
1113
  this.installNetworkCollection(tabId, client);
1004
1114
  }
@@ -1046,13 +1156,15 @@ class CdpConnectedBrowserSession {
1046
1156
  client.Network.loadingFinished(async (event) => {
1047
1157
  const request = state.byRequestId.get(event.requestId);
1048
1158
  if (!request) {
1159
+ resolveLoadingDone(state, event.requestId, true);
1049
1160
  return;
1050
1161
  }
1051
1162
  const startedAt = state.startedAt.get(event.requestId);
1052
1163
  if (startedAt !== undefined && event.timestamp !== undefined) {
1053
1164
  request.durationMs = Math.round(event.timestamp * 1000 - startedAt);
1054
1165
  }
1055
- if (canReadResponseBody(request)) {
1166
+ if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
1167
+ state.bodyRead.add(event.requestId);
1056
1168
  const clientNetwork = client.Network;
1057
1169
  const body = await clientNetwork?.getResponseBody({ requestId: event.requestId }).catch(() => undefined);
1058
1170
  if (body) {
@@ -1061,10 +1173,12 @@ class CdpConnectedBrowserSession {
1061
1173
  : body.body;
1062
1174
  }
1063
1175
  }
1176
+ resolveLoadingDone(state, event.requestId, true);
1064
1177
  });
1065
1178
  client.Network.loadingFailed((event) => {
1066
1179
  const request = state.byRequestId.get(event.requestId);
1067
1180
  if (!request) {
1181
+ resolveLoadingDone(state, event.requestId, false);
1068
1182
  return;
1069
1183
  }
1070
1184
  request.failureText = event.errorText ?? "Unknown error";
@@ -1072,6 +1186,7 @@ class CdpConnectedBrowserSession {
1072
1186
  if (startedAt !== undefined && event.timestamp !== undefined) {
1073
1187
  request.durationMs = Math.round(event.timestamp * 1000 - startedAt);
1074
1188
  }
1189
+ resolveLoadingDone(state, event.requestId, false);
1075
1190
  });
1076
1191
  }
1077
1192
  ensureConsoleState(tabId) {
@@ -1093,7 +1208,10 @@ class CdpConnectedBrowserSession {
1093
1208
  state = {
1094
1209
  requests: [],
1095
1210
  byRequestId: new Map(),
1096
- startedAt: new Map()
1211
+ startedAt: new Map(),
1212
+ hydratedPerformanceResources: false,
1213
+ loadingDone: new Map(),
1214
+ bodyRead: new Set()
1097
1215
  };
1098
1216
  this.pageNetworkStates.set(tabId, state);
1099
1217
  }
@@ -1109,10 +1227,82 @@ class CdpConnectedBrowserSession {
1109
1227
  this.pageNetworkStates.set(tabId, {
1110
1228
  requests: [],
1111
1229
  byRequestId: new Map(),
1112
- startedAt: new Map()
1230
+ startedAt: new Map(),
1231
+ hydratedPerformanceResources: false,
1232
+ loadingDone: new Map(),
1233
+ bodyRead: new Set()
1113
1234
  });
1114
1235
  this.pageDialogStates.delete(tabId);
1115
1236
  }
1237
+ async hydratePerformanceResourceRequests(tabId) {
1238
+ const state = this.ensureNetworkState(tabId);
1239
+ if (state.hydratedPerformanceResources) {
1240
+ return;
1241
+ }
1242
+ state.hydratedPerformanceResources = true;
1243
+ const pageClient = await this.getActivePageClient();
1244
+ const contextId = await this.getActiveUtilityContextId(pageClient);
1245
+ const documentRequest = await evaluateCdp(pageClient, String.raw `() => {
1246
+ const navigation = performance.getEntriesByType("navigation")[0];
1247
+ return {
1248
+ url: String(location.href || ""),
1249
+ duration: navigation ? Math.round(Number(navigation.duration || 0)) : undefined
1250
+ };
1251
+ }`, undefined, contextId).catch(() => undefined);
1252
+ if (documentRequest?.url && !Array.from(state.byRequestId.values()).some((request) => request.url === documentRequest.url)) {
1253
+ const requestId = `performance:navigation:${documentRequest.url}`;
1254
+ const request = {
1255
+ index: state.requests.length + 1,
1256
+ requestId,
1257
+ method: "GET",
1258
+ url: documentRequest.url,
1259
+ resourceType: "document",
1260
+ requestHeaders: {},
1261
+ status: 200,
1262
+ statusText: "OK",
1263
+ ...(documentRequest.duration !== undefined ? { durationMs: documentRequest.duration } : {})
1264
+ };
1265
+ state.requests.push(request);
1266
+ state.byRequestId.set(requestId, request);
1267
+ }
1268
+ const resources = await evaluateCdp(pageClient, String.raw `() => performance.getEntriesByType("resource").map((entry) => ({
1269
+ name: String(entry.name || ""),
1270
+ initiatorType: String(entry.initiatorType || "other"),
1271
+ duration: Math.round(Number(entry.duration || 0)),
1272
+ responseStatus: typeof entry.responseStatus === "number" ? entry.responseStatus : undefined
1273
+ }))`, undefined, contextId).catch(() => []);
1274
+ for (const resource of resources) {
1275
+ if (!resource.name || Array.from(state.byRequestId.values()).some((request) => request.url === resource.name)) {
1276
+ continue;
1277
+ }
1278
+ const status = resource.responseStatus && resource.responseStatus > 0
1279
+ ? resource.responseStatus
1280
+ : await this.probeResourceStatus(pageClient, contextId, resource.name);
1281
+ const requestId = `performance:${resource.name}`;
1282
+ const request = {
1283
+ index: state.requests.length + 1,
1284
+ requestId,
1285
+ method: "GET",
1286
+ url: resource.name,
1287
+ resourceType: normalizeResourceType(resource.initiatorType),
1288
+ requestHeaders: {},
1289
+ ...(status !== undefined ? { status, statusText: statusTextForStatus(status) } : {}),
1290
+ ...(resource.duration !== undefined ? { durationMs: resource.duration } : {})
1291
+ };
1292
+ state.requests.push(request);
1293
+ state.byRequestId.set(requestId, request);
1294
+ }
1295
+ }
1296
+ async probeResourceStatus(pageClient, contextId, url) {
1297
+ return evaluateCdp(pageClient, String.raw `async (url) => {
1298
+ try {
1299
+ const response = await fetch(url, { method: "HEAD", cache: "no-store" });
1300
+ return response.status;
1301
+ } catch {
1302
+ return undefined;
1303
+ }
1304
+ }`, url, contextId).catch(() => undefined);
1305
+ }
1116
1306
  addConsoleMessage(tabId, message) {
1117
1307
  const state = this.ensureConsoleState(tabId);
1118
1308
  if (!shouldIncludeConsoleMessage(message.type)) {
@@ -1282,6 +1472,59 @@ function canReadResponseBody(request) {
1282
1472
  }
1283
1473
  return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
1284
1474
  }
1475
+ function loadingDoneEntry(state, requestId) {
1476
+ let entry = state.loadingDone.get(requestId);
1477
+ if (!entry) {
1478
+ let resolve;
1479
+ let reject;
1480
+ const promise = new Promise((res, rej) => {
1481
+ resolve = res;
1482
+ reject = rej;
1483
+ });
1484
+ entry = { promise, resolve, reject };
1485
+ state.loadingDone.set(requestId, entry);
1486
+ }
1487
+ return entry;
1488
+ }
1489
+ function resolveLoadingDone(state, requestId, success) {
1490
+ const entry = state.loadingDone.get(requestId);
1491
+ if (!entry) {
1492
+ return;
1493
+ }
1494
+ state.loadingDone.delete(requestId);
1495
+ if (success) {
1496
+ entry.resolve();
1497
+ }
1498
+ else {
1499
+ entry.reject(new Error("Request failed before the response body was available."));
1500
+ }
1501
+ }
1502
+ async function waitForLoadingDone(state, requestId, timeoutMs) {
1503
+ const entry = loadingDoneEntry(state, requestId);
1504
+ await Promise.race([
1505
+ entry.promise,
1506
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
1507
+ ]);
1508
+ }
1509
+ function statusTextForStatus(status) {
1510
+ if (status === 200)
1511
+ return "OK";
1512
+ if (status === 204)
1513
+ return "No Content";
1514
+ if (status === 304)
1515
+ return "Not Modified";
1516
+ if (status === 400)
1517
+ return "Bad Request";
1518
+ if (status === 401)
1519
+ return "Unauthorized";
1520
+ if (status === 403)
1521
+ return "Forbidden";
1522
+ if (status === 404)
1523
+ return "Not Found";
1524
+ if (status === 500)
1525
+ return "Internal Server Error";
1526
+ return "";
1527
+ }
1285
1528
  function cloneNetworkRequest(request) {
1286
1529
  return {
1287
1530
  ...request,
@@ -1381,6 +1624,7 @@ class BidiConnectedBrowserSession {
1381
1624
  pageConsoleStates = new Map();
1382
1625
  pageNetworkStates = new Map();
1383
1626
  pageDialogStates = new Map();
1627
+ dialogWaiters = new Map();
1384
1628
  bidiListeners = new Map();
1385
1629
  responseDataCollector;
1386
1630
  activeTabId;
@@ -1398,15 +1642,12 @@ class BidiConnectedBrowserSession {
1398
1642
  }
1399
1643
  const client = await getBidiClientFactory()({
1400
1644
  browserName: "firefox",
1401
- webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint)
1645
+ webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint, args.sessionId)
1402
1646
  });
1403
1647
  const session = new BidiConnectedBrowserSession(client);
1404
- session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint);
1648
+ session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
1405
1649
  await session.initialize();
1406
- const tabs = await session.refreshTabs();
1407
- if (tabs.length === 0) {
1408
- await session.newTab();
1409
- }
1650
+ await session.refreshTabs();
1410
1651
  return session;
1411
1652
  }
1412
1653
  async version() {
@@ -1465,10 +1706,14 @@ class BidiConnectedBrowserSession {
1465
1706
  async snapshot(request = {}) {
1466
1707
  const tabId = await this.getActiveTabId();
1467
1708
  const result = await retryUntilReady(() => evaluateBiDi(this.client, tabId, ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request)));
1468
- return toBrowserSnapshot(result, request, {
1709
+ const snapshot = toBrowserSnapshot(result, request, {
1469
1710
  console: this.consoleSummary(tabId),
1470
1711
  consoleLink: await this.takeConsoleLink(tabId)
1471
1712
  });
1713
+ return {
1714
+ ...snapshot,
1715
+ retryable: true
1716
+ };
1472
1717
  }
1473
1718
  async consoleMessages(level = "info", all = false) {
1474
1719
  const activeTabId = await this.getActiveTabId();
@@ -1812,7 +2057,7 @@ class BidiConnectedBrowserSession {
1812
2057
  }
1813
2058
  }
1814
2059
  async handleDialog(accept, promptText) {
1815
- const tabId = await this.getActiveTabId();
2060
+ const tabId = this.dialogTabId();
1816
2061
  if (!this.pageDialogStates.has(tabId)) {
1817
2062
  throw new McpToolError("no_dialog", "No dialog visible.");
1818
2063
  }
@@ -1823,6 +2068,9 @@ class BidiConnectedBrowserSession {
1823
2068
  ...(promptText !== undefined ? { userText: promptText } : {})
1824
2069
  });
1825
2070
  }
2071
+ async hasDialog() {
2072
+ return this.pageDialogStates.size > 0;
2073
+ }
1826
2074
  async networkRequests() {
1827
2075
  const tabId = await this.getActiveTabId();
1828
2076
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
@@ -1832,6 +2080,21 @@ class BidiConnectedBrowserSession {
1832
2080
  const request = this.ensureNetworkState(tabId).requests[index - 1];
1833
2081
  return request ? cloneNetworkRequest(request) : undefined;
1834
2082
  }
2083
+ async fetchResponseBody(index) {
2084
+ const tabId = await this.getActiveTabId();
2085
+ const request = this.ensureNetworkState(tabId).requests[index - 1];
2086
+ if (!request || !request.requestId) {
2087
+ return request?.responseBody;
2088
+ }
2089
+ if (request.responseBody !== undefined) {
2090
+ return request.responseBody;
2091
+ }
2092
+ const body = await this.getResponseBody(request.requestId).catch(() => undefined);
2093
+ if (body !== undefined) {
2094
+ request.responseBody = body;
2095
+ }
2096
+ return request.responseBody;
2097
+ }
1835
2098
  async runCodeUnsafe(code) {
1836
2099
  return this.evaluate(`async () => {
1837
2100
  const fn = eval(${JSON.stringify(`(${code})`)});
@@ -1904,6 +2167,63 @@ class BidiConnectedBrowserSession {
1904
2167
  targetArg(target) {
1905
2168
  return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
1906
2169
  }
2170
+ dialogTabId() {
2171
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) {
2172
+ return this.activeTabId;
2173
+ }
2174
+ const tabId = this.pageDialogStates.keys().next().value;
2175
+ if (!tabId) {
2176
+ throw new McpToolError("no_dialog", "No dialog visible.");
2177
+ }
2178
+ return tabId;
2179
+ }
2180
+ waitForDialog(tabId, timeoutMs) {
2181
+ if (this.pageDialogStates.has(tabId)) {
2182
+ return Promise.resolve();
2183
+ }
2184
+ return new Promise((resolve, reject) => {
2185
+ const waiter = {
2186
+ resolve: () => {
2187
+ if (waiter.timer) {
2188
+ clearTimeout(waiter.timer);
2189
+ }
2190
+ this.removeDialogWaiter(tabId, waiter);
2191
+ resolve();
2192
+ },
2193
+ reject: (error) => {
2194
+ if (waiter.timer) {
2195
+ clearTimeout(waiter.timer);
2196
+ }
2197
+ this.removeDialogWaiter(tabId, waiter);
2198
+ reject(error);
2199
+ }
2200
+ };
2201
+ waiter.timer = setTimeout(() => waiter.reject?.(new Error("Timed out waiting for dialog.")), timeoutMs);
2202
+ const waiters = this.dialogWaiters.get(tabId) ?? new Set();
2203
+ waiters.add(waiter);
2204
+ this.dialogWaiters.set(tabId, waiters);
2205
+ });
2206
+ }
2207
+ resolveDialogWaiters(tabId) {
2208
+ const waiters = this.dialogWaiters.get(tabId);
2209
+ if (!waiters) {
2210
+ return;
2211
+ }
2212
+ this.dialogWaiters.delete(tabId);
2213
+ for (const waiter of waiters) {
2214
+ waiter.resolve();
2215
+ }
2216
+ }
2217
+ removeDialogWaiter(tabId, waiter) {
2218
+ const waiters = this.dialogWaiters.get(tabId);
2219
+ if (!waiters) {
2220
+ return;
2221
+ }
2222
+ waiters.delete(waiter);
2223
+ if (waiters.size === 0) {
2224
+ this.dialogWaiters.delete(tabId);
2225
+ }
2226
+ }
1907
2227
  async actionPoint(tabId, target) {
1908
2228
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
1909
2229
  const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
@@ -2086,7 +2406,10 @@ class BidiConnectedBrowserSession {
2086
2406
  state = {
2087
2407
  requests: [],
2088
2408
  byRequestId: new Map(),
2089
- startedAt: new Map()
2409
+ startedAt: new Map(),
2410
+ hydratedPerformanceResources: false,
2411
+ loadingDone: new Map(),
2412
+ bodyRead: new Set()
2090
2413
  };
2091
2414
  this.pageNetworkStates.set(tabId, state);
2092
2415
  }
@@ -2102,7 +2425,10 @@ class BidiConnectedBrowserSession {
2102
2425
  this.pageNetworkStates.set(tabId, {
2103
2426
  requests: [],
2104
2427
  byRequestId: new Map(),
2105
- startedAt: new Map()
2428
+ startedAt: new Map(),
2429
+ hydratedPerformanceResources: false,
2430
+ loadingDone: new Map(),
2431
+ bodyRead: new Set()
2106
2432
  });
2107
2433
  this.pageDialogStates.delete(tabId);
2108
2434
  }
@@ -2148,16 +2474,20 @@ class BidiConnectedBrowserSession {
2148
2474
  return `${relativePath}${lineRange}`;
2149
2475
  }
2150
2476
  }
2151
- function normalizeFirefoxBidiEndpoint(endpoint) {
2477
+ function normalizeFirefoxBidiEndpoint(endpoint, sessionId) {
2152
2478
  const url = new URL(endpoint);
2479
+ if (sessionId) {
2480
+ url.pathname = `/session/${sessionId}`;
2481
+ return url.toString();
2482
+ }
2153
2483
  if (url.pathname === "/" || url.pathname === "") {
2154
2484
  url.pathname = "/session";
2155
2485
  }
2156
2486
  return url.toString();
2157
2487
  }
2158
- async function ensureMcpBiDiSession(client, endpoint) {
2488
+ async function ensureMcpBiDiSession(client, endpoint, sessionId) {
2159
2489
  await client.sessionStatus({});
2160
- if (isSessionSpecificFirefoxBidiEndpoint(endpoint)) {
2490
+ if (sessionId || isSessionSpecificFirefoxBidiEndpoint(endpoint)) {
2161
2491
  return false;
2162
2492
  }
2163
2493
  try {