@roxybrowser/playwright 2.0.2-beta.0 → 2.0.2-beta.10

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 (67) hide show
  1. package/README.md +85 -7
  2. package/dist/bin/roxybrowser-mcp.js +72 -2
  3. package/dist/bin/roxybrowser-mcp.js.map +1 -1
  4. package/dist/browser.d.ts +33 -5
  5. package/dist/browser.d.ts.map +1 -1
  6. package/dist/browser.js +134 -15
  7. package/dist/browser.js.map +1 -1
  8. package/dist/browserType.d.ts +23 -1
  9. package/dist/browserType.d.ts.map +1 -1
  10. package/dist/browserType.js +33 -5
  11. package/dist/browserType.js.map +1 -1
  12. package/dist/mcp/backend/connect.d.ts +1 -0
  13. package/dist/mcp/backend/connect.d.ts.map +1 -1
  14. package/dist/mcp/backend/connect.js +4 -2
  15. package/dist/mcp/backend/connect.js.map +1 -1
  16. package/dist/mcp/backend/context.d.ts +3 -0
  17. package/dist/mcp/backend/context.d.ts.map +1 -1
  18. package/dist/mcp/backend/context.js +9 -1
  19. package/dist/mcp/backend/context.js.map +1 -1
  20. package/dist/mcp/backend/network.d.ts.map +1 -1
  21. package/dist/mcp/backend/network.js +30 -4
  22. package/dist/mcp/backend/network.js.map +1 -1
  23. package/dist/mcp/backend/response.d.ts +2 -0
  24. package/dist/mcp/backend/response.d.ts.map +1 -1
  25. package/dist/mcp/backend/response.js +53 -7
  26. package/dist/mcp/backend/response.js.map +1 -1
  27. package/dist/mcp/connectedBrowser.d.ts.map +1 -1
  28. package/dist/mcp/connectedBrowser.js +494 -37
  29. package/dist/mcp/connectedBrowser.js.map +1 -1
  30. package/dist/mcp/output.d.ts +5 -0
  31. package/dist/mcp/output.d.ts.map +1 -1
  32. package/dist/mcp/output.js +17 -1
  33. package/dist/mcp/output.js.map +1 -1
  34. package/dist/mcp/runtime.d.ts +7 -0
  35. package/dist/mcp/runtime.d.ts.map +1 -1
  36. package/dist/mcp/runtime.js +84 -9
  37. package/dist/mcp/runtime.js.map +1 -1
  38. package/dist/mcp/server.d.ts.map +1 -1
  39. package/dist/mcp/server.js +3 -1
  40. package/dist/mcp/server.js.map +1 -1
  41. package/dist/mcp/transports/inMemory.d.ts.map +1 -1
  42. package/dist/mcp/transports/inMemory.js +1 -0
  43. package/dist/mcp/transports/inMemory.js.map +1 -1
  44. package/dist/mcp/types.d.ts +7 -0
  45. package/dist/mcp/types.d.ts.map +1 -1
  46. package/dist/page.d.ts +2 -0
  47. package/dist/page.d.ts.map +1 -1
  48. package/dist/page.js +12 -0
  49. package/dist/page.js.map +1 -1
  50. package/dist/protocol/adapter.d.ts +1 -0
  51. package/dist/protocol/adapter.d.ts.map +1 -1
  52. package/dist/protocol/bidi/backend.d.ts +1 -1
  53. package/dist/protocol/bidi/backend.d.ts.map +1 -1
  54. package/dist/protocol/bidi/backend.js +6 -2
  55. package/dist/protocol/bidi/backend.js.map +1 -1
  56. package/dist/protocol/bidi/client.d.ts +1 -0
  57. package/dist/protocol/bidi/client.d.ts.map +1 -1
  58. package/dist/protocol/bidi/client.js +48 -1
  59. package/dist/protocol/bidi/client.js.map +1 -1
  60. package/dist/protocol/cdp/backend.d.ts.map +1 -1
  61. package/dist/protocol/cdp/backend.js +122 -8
  62. package/dist/protocol/cdp/backend.js.map +1 -1
  63. package/dist/roxybrowser.bundle.js +1060 -467
  64. package/dist/roxybrowser.bundle.js.map +1 -1
  65. package/dist/types/api.d.ts +23 -4
  66. package/dist/types/api.d.ts.map +1 -1
  67. package/package.json +11 -4
@@ -5124,6 +5124,7 @@ var RoxyPage = class RoxyPage {
5124
5124
  closed = false;
5125
5125
  closePromise = null;
5126
5126
  closeReason;
5127
+ ownedContext;
5127
5128
  defaultTimeoutMs = DEFAULT_EVENT_TIMEOUT_MS;
5128
5129
  defaultNavigationTimeoutMs = DEFAULT_EVENT_TIMEOUT_MS;
5129
5130
  currentViewportSize = null;
@@ -6111,6 +6112,10 @@ waiting for navigation${navigationTargetDescription} until "${waitUntil}"\n=====
6111
6112
  }
6112
6113
  this.closeReason = options.reason;
6113
6114
  this.closePromise = (async () => {
6115
+ if (this.ownedContext) {
6116
+ await this.ownedContext.close().catch(() => {});
6117
+ return;
6118
+ }
6114
6119
  await this.dismissActiveDialogsForClose();
6115
6120
  await this.finalizeVideoRecording();
6116
6121
  await this.adapter.close(options).catch(() => {});
@@ -6122,6 +6127,9 @@ waiting for navigation${navigationTargetDescription} until "${waitUntil}"\n=====
6122
6127
  this.closePromise = null;
6123
6128
  }
6124
6129
  }
6130
+ setOwnedContext(context) {
6131
+ this.ownedContext = context;
6132
+ }
6125
6133
  defaultTimeout() {
6126
6134
  return this.defaultTimeoutMs;
6127
6135
  }
@@ -10650,12 +10658,74 @@ var RoxyBrowser = class {
10650
10658
  session;
10651
10659
  adapter;
10652
10660
  humanDefaults;
10653
- browserName;
10654
- constructor(session, adapter, humanDefaults, browserName = "chromium") {
10661
+ _browserName;
10662
+ _browserType;
10663
+ _version;
10664
+ _contexts = [];
10665
+ _listeners = /* @__PURE__ */ new Map();
10666
+ _connected = true;
10667
+ constructor(session, adapter, humanDefaults, _browserName, _browserType, _version) {
10655
10668
  this.session = session;
10656
10669
  this.adapter = adapter;
10657
10670
  this.humanDefaults = humanDefaults;
10658
- this.browserName = browserName;
10671
+ this._browserName = _browserName;
10672
+ this._browserType = _browserType;
10673
+ this._version = _version;
10674
+ }
10675
+ on(event, listener) {
10676
+ return this._addListenerInternal(event, listener);
10677
+ }
10678
+ once(event, listener) {
10679
+ const wrapped = (payload) => {
10680
+ this._removeListenerInternal(event, listener);
10681
+ listener(payload);
10682
+ };
10683
+ this._ensureListenerSet(event).add({
10684
+ original: listener,
10685
+ wrapped
10686
+ });
10687
+ return this;
10688
+ }
10689
+ addListener(event, listener) {
10690
+ return this._addListenerInternal(event, listener);
10691
+ }
10692
+ removeListener(event, listener) {
10693
+ return this._removeListenerInternal(event, listener);
10694
+ }
10695
+ off(event, listener) {
10696
+ return this._removeListenerInternal(event, listener);
10697
+ }
10698
+ prependListener(event, listener) {
10699
+ const entry = {
10700
+ original: listener,
10701
+ wrapped: listener
10702
+ };
10703
+ const existing = this._listeners.get(event);
10704
+ if (!existing) {
10705
+ const s = new Set([entry]);
10706
+ this._listeners.set(event, s);
10707
+ } else {
10708
+ const s = new Set([entry, ...Array.from(existing)]);
10709
+ this._listeners.set(event, s);
10710
+ }
10711
+ return this;
10712
+ }
10713
+ removeAllListeners(type) {
10714
+ if (type === void 0) this._listeners.clear();
10715
+ else this._listeners.delete(type);
10716
+ return this;
10717
+ }
10718
+ browserType() {
10719
+ return this._browserType;
10720
+ }
10721
+ contexts() {
10722
+ return [...this._contexts];
10723
+ }
10724
+ isConnected() {
10725
+ return this._connected;
10726
+ }
10727
+ version() {
10728
+ return this._version;
10659
10729
  }
10660
10730
  async newContext(options = {}) {
10661
10731
  const normalizedOptions = {
@@ -10666,26 +10736,66 @@ var RoxyBrowser = class {
10666
10736
  ...options.recordVideo.dir ? { dir: resolve(options.recordVideo.dir) } : {}
10667
10737
  } } : {}
10668
10738
  };
10669
- return new RoxyBrowserContext(await this.session.newContext(normalizedOptions), resolveHumanizationOptions(normalizedOptions.human, this.humanDefaults), normalizedOptions, this.browserName);
10670
- }
10671
- async version() {
10672
- return this.session.version();
10739
+ const context = new RoxyBrowserContext(await this.session.newContext(normalizedOptions), resolveHumanizationOptions(normalizedOptions.human, this.humanDefaults), normalizedOptions, this._browserName);
10740
+ this._contexts.push(context);
10741
+ context.on("close", () => {
10742
+ const index = this._contexts.indexOf(context);
10743
+ if (index !== -1) this._contexts.splice(index, 1);
10744
+ });
10745
+ this._emit("context", context);
10746
+ return context;
10747
+ }
10748
+ async newPage(options) {
10749
+ const context = await this.newContext(options);
10750
+ const page = await context.newPage();
10751
+ const roxyPage = page;
10752
+ if (typeof roxyPage.setOwnedContext === "function") roxyPage.setOwnedContext(context);
10753
+ else page.once("close", () => context.close().catch(() => {}));
10754
+ return page;
10673
10755
  }
10674
- async close() {
10756
+ async close(options) {
10757
+ if (!this._connected) return;
10758
+ this._connected = false;
10675
10759
  try {
10676
10760
  await withCloseTimeout$1(this.session.close(), BROWSER_SESSION_CLOSE_TIMEOUT_MS);
10677
10761
  } finally {
10678
10762
  await this.adapter.close();
10763
+ this._emit("disconnected", this);
10679
10764
  }
10680
10765
  }
10766
+ _addListenerInternal(event, listener) {
10767
+ this._ensureListenerSet(event).add({
10768
+ original: listener,
10769
+ wrapped: listener
10770
+ });
10771
+ return this;
10772
+ }
10773
+ _removeListenerInternal(event, listener) {
10774
+ const entries = this._listeners.get(event);
10775
+ if (!entries) return this;
10776
+ for (const entry of Array.from(entries)) if (entry.original === listener) entries.delete(entry);
10777
+ if (entries.size === 0) this._listeners.delete(event);
10778
+ return this;
10779
+ }
10780
+ _ensureListenerSet(event) {
10781
+ const existing = this._listeners.get(event);
10782
+ if (existing) return existing;
10783
+ const created = /* @__PURE__ */ new Set();
10784
+ this._listeners.set(event, created);
10785
+ return created;
10786
+ }
10787
+ _emit(event, payload) {
10788
+ const entries = this._listeners.get(event);
10789
+ if (!entries?.size) return false;
10790
+ for (const entry of Array.from(entries)) entry.wrapped(payload);
10791
+ return true;
10792
+ }
10681
10793
  };
10682
10794
  async function withCloseTimeout$1(promise, timeoutMs) {
10683
10795
  let timer;
10684
10796
  try {
10685
10797
  return await Promise.race([promise, new Promise((_, reject) => {
10686
- timer = setTimeout(() => {
10687
- reject(/* @__PURE__ */ new Error(`Timed out closing browser session after ${timeoutMs}ms.`));
10688
- }, timeoutMs);
10798
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timed out closing browser session after ${timeoutMs}ms.`)), timeoutMs);
10689
10799
  })]);
10690
10800
  } finally {
10691
10801
  if (timer) clearTimeout(timer);
@@ -14532,8 +14642,10 @@ var WebSocketBidiClient = class {
14532
14642
  eventListeners = /* @__PURE__ */ new Map();
14533
14643
  pendingCommands = /* @__PURE__ */ new Map();
14534
14644
  socket;
14645
+ webSocketUrl;
14535
14646
  constructor(options) {
14536
14647
  this.capabilities = { browserName: options.browserName };
14648
+ this.webSocketUrl = options.webSocketUrl;
14537
14649
  this.socket = new globalThis.WebSocket(options.webSocketUrl);
14538
14650
  this.socket.addEventListener("message", (event) => this.handleMessage(event.data));
14539
14651
  this.socket.addEventListener("close", () => this.handleClose());
@@ -14543,7 +14655,7 @@ var WebSocketBidiClient = class {
14543
14655
  return new Promise((resolve, reject) => {
14544
14656
  this.socket.addEventListener("open", resolve, { once: true });
14545
14657
  this.socket.addEventListener("error", (event) => {
14546
- reject(/* @__PURE__ */ new Error(`Failed to establish a WebDriver BiDi connection: ${String(event)}`));
14658
+ reject(new Error(formatBidiConnectError(event, this.webSocketUrl)));
14547
14659
  }, { once: true });
14548
14660
  });
14549
14661
  }
@@ -14714,6 +14826,28 @@ var WebSocketBidiClient = class {
14714
14826
  this.pendingCommands.clear();
14715
14827
  }
14716
14828
  };
14829
+ function formatBidiConnectError(event, webSocketUrl) {
14830
+ const details = extractBidiConnectErrorDetails(event);
14831
+ return details ? `Failed to establish a WebDriver BiDi connection to ${webSocketUrl}: ${details}` : `Failed to establish a WebDriver BiDi connection to ${webSocketUrl}.`;
14832
+ }
14833
+ function extractBidiConnectErrorDetails(event) {
14834
+ if (event instanceof Error) return event.message;
14835
+ if (!event || typeof event !== "object") return typeof event === "string" ? event : void 0;
14836
+ const candidate = event;
14837
+ const parts = [];
14838
+ if (typeof candidate.message === "string" && candidate.message) parts.push(candidate.message);
14839
+ if (typeof candidate.error === "string" && candidate.error) parts.push(candidate.error);
14840
+ else if (candidate.error instanceof Error && candidate.error.message) parts.push(candidate.error.message);
14841
+ const socketLike = candidate.target ?? candidate.currentTarget;
14842
+ if (socketLike && typeof socketLike === "object") {
14843
+ const socketParts = [];
14844
+ if (typeof socketLike.url === "string" && socketLike.url) socketParts.push(`url=${socketLike.url}`);
14845
+ if (typeof socketLike.readyState === "number") socketParts.push(`readyState=${socketLike.readyState}`);
14846
+ if (socketParts.length > 0) parts.push(`socket(${socketParts.join(", ")})`);
14847
+ }
14848
+ if (parts.length > 0) return parts.join("; ");
14849
+ if (typeof candidate.type === "string" && candidate.type) return `event type=${candidate.type}`;
14850
+ }
14717
14851
  var bidiClientFactory = createBidiClient;
14718
14852
  function getBidiClientFactory() {
14719
14853
  return bidiClientFactory;
@@ -15981,7 +16115,7 @@ var BidiPageAdapter = class BidiPageAdapter {
15981
16115
  resultOwnership: "none"
15982
16116
  }));
15983
16117
  if (response.type === "exception") throw new Error(response.exceptionDetails.text || "BiDi evaluation failed.");
15984
- return parseSerializedEvaluationResult(extractBiDiValue(response.result));
16118
+ return parseSerializedEvaluationResult(extractBiDiValue$1(response.result));
15985
16119
  }
15986
16120
  async evaluateFunction(expression, arg) {
15987
16121
  const serializedArg = arg === void 0 ? "" : serializeForEvaluation$1(arg);
@@ -17225,11 +17359,11 @@ var BidiElementHandleAdapter = class BidiElementHandleAdapter {
17225
17359
  return this.page.selectOptionReference(this.reference(), values, options);
17226
17360
  }
17227
17361
  };
17228
- function extractBiDiValue(value) {
17229
- if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue(entry));
17362
+ function extractBiDiValue$1(value) {
17363
+ if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue$1(entry));
17230
17364
  if (value.type === "object" && Array.isArray(value.value)) {
17231
17365
  const obj = {};
17232
- for (const [key, val] of value.value) obj[key] = extractBiDiValue(val);
17366
+ for (const [key, val] of value.value) obj[key] = extractBiDiValue$1(val);
17233
17367
  return obj;
17234
17368
  }
17235
17369
  return value.value;
@@ -17590,13 +17724,13 @@ function buildFirefoxBidiEndpoint(wsEndpoint, sessionId) {
17590
17724
  if (url.pathname === "/" || url.pathname === "") url.pathname = "/session";
17591
17725
  return url.toString();
17592
17726
  }
17593
- function isSessionSpecificFirefoxBidiEndpoint(wsEndpoint) {
17727
+ function isSessionSpecificFirefoxBidiEndpoint$1(wsEndpoint) {
17594
17728
  const pathname = new URL(wsEndpoint).pathname;
17595
17729
  return /^\/session\/[^/]+$/.test(pathname);
17596
17730
  }
17597
17731
  async function ensureBiDiSession(client, sessionId, wsEndpoint) {
17598
17732
  await client.sessionStatus({});
17599
- if (sessionId || isSessionSpecificFirefoxBidiEndpoint(wsEndpoint)) return false;
17733
+ if (sessionId || isSessionSpecificFirefoxBidiEndpoint$1(wsEndpoint)) return false;
17600
17734
  try {
17601
17735
  await client.browsingContextGetTree({});
17602
17736
  return false;
@@ -17621,9 +17755,9 @@ function buildFirefoxLaunchArgs(options, userDataDir, port) {
17621
17755
  ...options.args ?? []
17622
17756
  ];
17623
17757
  }
17624
- function resolveFirefoxExecutableCandidates(options, platform = currentPlatform$1(), fileExistsFn = fileExists$1) {
17758
+ function resolveFirefoxExecutableCandidates(options, platform = currentPlatform$1(), playwrightFirefoxExecutablePath, fileExistsFn = fileExists$1) {
17625
17759
  if (options.executablePath) return [options.executablePath];
17626
- return filterExistingFirefoxExecutableCandidates(defaultFirefoxExecutableCandidates(platform), platform, fileExistsFn);
17760
+ return filterExistingFirefoxExecutableCandidates([...playwrightFirefoxExecutablePath ? [playwrightFirefoxExecutablePath] : [], ...defaultFirefoxExecutableCandidates(platform)], platform, fileExistsFn);
17627
17761
  }
17628
17762
  function defaultFirefoxExecutableCandidates(platform) {
17629
17763
  const candidates = [];
@@ -53274,11 +53408,34 @@ var RoxyBrowserType = class {
53274
53408
  protocol: options.protocol ?? (this.browserName === "firefox" ? "bidi" : "cdp")
53275
53409
  });
53276
53410
  }
53277
- async connect(options) {
53411
+ /**
53412
+ * Connect to an already-running browser over a WebSocket endpoint.
53413
+ *
53414
+ * ⚠️ DIVERGENCE FROM PLAYWRIGHT — DO NOT "FIX" THIS BACK TO CDP-ONLY:
53415
+ *
53416
+ * In upstream Playwright, `BrowserType.connect()` is a CDP-only operation: it
53417
+ * connects to a running Chromium over the DevTools Protocol. Firefox/WebKit are
53418
+ * not supported here (Playwright routes BiDi through `connectOverCDP`-style
53419
+ * helpers or its own internal plumbing, not this method).
53420
+ *
53421
+ * Roxy intentionally changes that semantics. This method dispatches on
53422
+ * `browserName` so a single entry point serves both browser families:
53423
+ * - chromium → CDP (ws:// DevTools endpoint)
53424
+ * - firefox → BiDi (ws:// WebDriver BiDi endpoint)
53425
+ *
53426
+ * This is deliberate, not a bug. If you are an agent "normalizing" this code to
53427
+ * match Playwright upstream, STOP — the dual-protocol dispatch is the feature.
53428
+ * To force CDP specifically, call `connectOverCDP()` instead, which remains
53429
+ * chromium-only and CDP-only by design (see the guard below).
53430
+ *
53431
+ * `options.protocol` may still be passed to override the per-browser default.
53432
+ */
53433
+ async connect(endpointURL, options) {
53278
53434
  return this.connectBrowser({
53279
- ...options,
53280
- browserName: options.browserName ?? this.browserName,
53281
- protocol: options.protocol ?? (this.browserName === "firefox" ? "bidi" : "cdp")
53435
+ browserName: this.browserName,
53436
+ protocol: this.browserName === "chromium" ? "cdp" : "bidi",
53437
+ wsEndpoint: endpointURL,
53438
+ ...options
53282
53439
  });
53283
53440
  }
53284
53441
  async connectOverCDP(progressOrEndpointURL, endpointURLOrOptions, maybeOptions = {}) {
@@ -53310,7 +53467,9 @@ var RoxyBrowserType = class {
53310
53467
  protocol
53311
53468
  });
53312
53469
  await adapter.connect();
53313
- return new RoxyBrowser(await adapter.browser(), adapter, resolveHumanizationOptions(options.human), options.browserName ?? this.browserName);
53470
+ const session = await adapter.browser();
53471
+ const versionStr = await session.version();
53472
+ return new RoxyBrowser(session, adapter, resolveHumanizationOptions(options.human), options.browserName ?? this.browserName, this, versionStr);
53314
53473
  }
53315
53474
  };
53316
53475
  var chromium = new RoxyBrowserType("chromium", {
@@ -73280,11 +73439,6 @@ function formatConnectResult(input) {
73280
73439
  if (input.snapshot) parts.push(formatSnapshot(input.snapshot));
73281
73440
  return parts.join("\n\n");
73282
73441
  }
73283
- function formatTabsWithOptionalSnapshot(tabs, snapshot) {
73284
- const parts = [formatTabs(tabs)];
73285
- if (snapshot) parts.push(formatSnapshot(snapshot));
73286
- return parts.join("\n\n");
73287
- }
73288
73442
  //#endregion
73289
73443
  //#region src/mcp/backend/response.ts
73290
73444
  var Response$1 = class {
@@ -73294,9 +73448,11 @@ var Response$1 = class {
73294
73448
  results = [];
73295
73449
  errors = [];
73296
73450
  code = [];
73451
+ images = [];
73297
73452
  includeSnapshot = "none";
73298
73453
  fullSnapshot;
73299
73454
  isClose = false;
73455
+ rawResults = false;
73300
73456
  constructor(context, toolName, toolArgs) {
73301
73457
  this.context = context;
73302
73458
  this.toolName = toolName;
@@ -73311,9 +73467,18 @@ var Response$1 = class {
73311
73467
  addCode(code) {
73312
73468
  this.code.push(code);
73313
73469
  }
73470
+ addImageResult(data, mimeType) {
73471
+ this.images.push({
73472
+ data,
73473
+ mimeType
73474
+ });
73475
+ }
73314
73476
  setClose() {
73315
73477
  this.isClose = true;
73316
73478
  }
73479
+ setRawResults() {
73480
+ this.rawResults = true;
73481
+ }
73317
73482
  setIncludeSnapshot() {
73318
73483
  this.includeSnapshot = this.context.config.snapshot?.mode ?? "full";
73319
73484
  }
@@ -73329,6 +73494,21 @@ var Response$1 = class {
73329
73494
  async serialize() {
73330
73495
  const sections = [];
73331
73496
  if (this.errors.length) sections.push("### Error", ...this.errors);
73497
+ if (this.rawResults) {
73498
+ if (this.results.length) sections.push(...this.results);
73499
+ return {
73500
+ content: [{
73501
+ type: "text",
73502
+ text: sections.join("\n")
73503
+ }, ...this.images.map((image) => ({
73504
+ type: "image",
73505
+ data: image.data,
73506
+ mimeType: image.mimeType
73507
+ }))],
73508
+ ...this.isClose ? { isClose: true } : {},
73509
+ ...this.errors.length ? { isError: true } : {}
73510
+ };
73511
+ }
73332
73512
  if (this.results.length) {
73333
73513
  if (sections.length) sections.push("");
73334
73514
  sections.push("### Result", ...this.results);
@@ -73338,16 +73518,23 @@ var Response$1 = class {
73338
73518
  sections.push("### Code", "```js", ...this.code, "```");
73339
73519
  }
73340
73520
  if (this.includeSnapshot === "full") {
73341
- const snapshot = await this.context.runtime.snapshot();
73342
- if (sections.length) sections.push("");
73343
- sections.push(formatSnapshot(snapshot));
73521
+ if (!await this.context.runtime.hasDialog()) {
73522
+ const snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot());
73523
+ if (sections.length) sections.push("");
73524
+ sections.push(formatSnapshot(snapshot));
73525
+ }
73344
73526
  }
73345
73527
  if (this.fullSnapshot) {
73346
- const snapshot = await this.context.runtime.snapshot({
73528
+ let snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
73347
73529
  ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73348
73530
  ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73349
73531
  ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73350
- });
73532
+ }));
73533
+ if (!this.fullSnapshot.filename && snapshot.retryable && snapshot.text.trim().length === 0 && snapshot.url && snapshot.url !== "about:blank") snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
73534
+ ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73535
+ ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73536
+ ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73537
+ }));
73351
73538
  if (this.fullSnapshot.filename) {
73352
73539
  const resolvedFilename = await this.context.resolveOutputFile(this.fullSnapshot.filename);
73353
73540
  await writeFile(resolvedFilename, snapshot.text);
@@ -73364,17 +73551,30 @@ var Response$1 = class {
73364
73551
  content: [{
73365
73552
  type: "text",
73366
73553
  text: sections.join("\n")
73367
- }],
73554
+ }, ...this.images.map((image) => ({
73555
+ type: "image",
73556
+ data: image.data,
73557
+ mimeType: image.mimeType
73558
+ }))],
73368
73559
  ...this.isClose ? { isClose: true } : {},
73369
73560
  ...this.errors.length ? { isError: true } : {}
73370
73561
  };
73371
73562
  }
73372
73563
  };
73373
- //#endregion
73374
- //#region src/mcp/backend/common.ts
73375
- var common_default$1 = [];
73564
+ async function reconcileSnapshotWithTabs(context, snapshot) {
73565
+ const activeTab = (await context.runtime.listTabs()).find((tab) => tab.active);
73566
+ if (!activeTab) return snapshot;
73567
+ return {
73568
+ ...snapshot,
73569
+ title: activeTab.title || snapshot.title,
73570
+ url: activeTab.url || snapshot.url
73571
+ };
73572
+ }
73376
73573
  //#endregion
73377
73574
  //#region src/mcp/backend/tool.ts
73575
+ function defineTool$1(tool) {
73576
+ return tool;
73577
+ }
73378
73578
  function defineTabTool(tool) {
73379
73579
  return {
73380
73580
  ...tool,
@@ -73391,6 +73591,146 @@ function missingModalStateMessage(tool) {
73391
73591
  if (tool.clearsModalState === "fileChooser") return "[no_file_chooser] No file chooser visible.";
73392
73592
  return `Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.`;
73393
73593
  }
73594
+ var common_default = [defineTool$1({
73595
+ capability: "core-tabs",
73596
+ schema: {
73597
+ name: "browser_close",
73598
+ title: "Close browser",
73599
+ description: "Close the current browser session.",
73600
+ inputSchema: object({}),
73601
+ type: "action"
73602
+ },
73603
+ handle: async (context, _params, response) => {
73604
+ await context.runtime.close();
73605
+ response.setClose();
73606
+ response.addTextResult("Browser session closed.");
73607
+ }
73608
+ }), defineTool$1({
73609
+ capability: "core",
73610
+ schema: {
73611
+ name: "browser_resize",
73612
+ title: "Resize browser",
73613
+ description: "Resize the active page viewport.",
73614
+ inputSchema: object({
73615
+ width: number().int().positive(),
73616
+ height: number().int().positive()
73617
+ }),
73618
+ type: "action"
73619
+ },
73620
+ handle: async (context, params, response) => {
73621
+ const snapshot = await context.runtime.resize(params.width, params.height);
73622
+ response.setIncludeSnapshot();
73623
+ response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
73624
+ if (!snapshot) response.addTextResult(`Resized viewport to ${params.width}x${params.height}.`);
73625
+ }
73626
+ })];
73627
+ var console_default = [defineTool$1({
73628
+ capability: "core",
73629
+ schema: {
73630
+ name: "browser_console_messages",
73631
+ title: "Get console messages",
73632
+ description: "Returns all console messages",
73633
+ inputSchema: object({
73634
+ level: _enum([
73635
+ "error",
73636
+ "warning",
73637
+ "info",
73638
+ "debug"
73639
+ ]).default("info").describe("Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\"."),
73640
+ all: boolean().optional().describe("Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false."),
73641
+ filename: string().optional().describe("Filename to save the console messages to. If not provided, messages are returned as text.")
73642
+ }),
73643
+ type: "readOnly"
73644
+ },
73645
+ handle: async (context, params, response) => {
73646
+ const messages = await context.runtime.consoleMessages(params.level, params.all);
73647
+ const errors = messages.filter((message) => message.type === "error" || message.type === "assert").length;
73648
+ const warnings = messages.filter((message) => message.type === "warning").length;
73649
+ const text = [
73650
+ `Total messages: ${messages.length} (Errors: ${errors}, Warnings: ${warnings})`,
73651
+ "",
73652
+ ...messages.map((message) => message.formattedText)
73653
+ ].join("\n");
73654
+ if (params.filename) {
73655
+ const resolvedFilename = await context.resolveOutputFile(params.filename);
73656
+ await writeFile(resolvedFilename, text);
73657
+ response.addTextResult(`Saved console messages to "${resolvedFilename}".`);
73658
+ return;
73659
+ }
73660
+ response.addTextResult(text);
73661
+ }
73662
+ })];
73663
+ var connect_default = [defineTool$1({
73664
+ capability: "config",
73665
+ schema: {
73666
+ name: "roxy_browser_connect",
73667
+ title: "Roxy Browser Connect",
73668
+ description: "Attach to an existing browser and seed the active tab snapshot.",
73669
+ inputSchema: object({
73670
+ endpoint: string().min(1),
73671
+ browser: _enum(["chrome", "firefox"]).default("chrome"),
73672
+ sessionId: string().min(1).optional()
73673
+ }),
73674
+ type: "action"
73675
+ },
73676
+ handle: async (context, params, response) => {
73677
+ const protocol = params.browser === "firefox" ? "bidi" : "cdp";
73678
+ const result = await context.runtime.connect({
73679
+ protocol,
73680
+ endpoint: params.endpoint,
73681
+ browser: params.browser === "chrome" ? "chromium" : params.browser,
73682
+ ...params.sessionId ? { sessionId: params.sessionId } : {}
73683
+ });
73684
+ response.addTextResult(formatConnectResult({
73685
+ ...result,
73686
+ browserName: result.browserName === "chromium" ? "chrome" : result.browserName
73687
+ }));
73688
+ }
73689
+ })];
73690
+ var dialogs_default = [defineTool$1({
73691
+ capability: "core",
73692
+ schema: {
73693
+ name: "browser_handle_dialog",
73694
+ title: "Handle a dialog",
73695
+ description: "Handle a dialog",
73696
+ inputSchema: object({
73697
+ accept: boolean().describe("Whether to accept the dialog."),
73698
+ promptText: string().optional().describe("The text of the prompt in case of a prompt dialog.")
73699
+ }),
73700
+ type: "action"
73701
+ },
73702
+ handle: async (context, params, response) => {
73703
+ const snapshot = await context.runtime.handleDialog(params.accept, params.promptText);
73704
+ response.setIncludeSnapshot();
73705
+ if (!snapshot) response.addTextResult(params.accept ? "Accepted dialog." : "Dismissed dialog.");
73706
+ }
73707
+ })];
73708
+ var evaluate_default = [defineTool$1({
73709
+ capability: "core",
73710
+ schema: {
73711
+ name: "browser_evaluate",
73712
+ title: "Evaluate JavaScript",
73713
+ description: "Evaluate JavaScript expression on page or element",
73714
+ inputSchema: object({
73715
+ element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73716
+ target: string().optional().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73717
+ function: string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
73718
+ filename: string().optional().describe("Filename to save the result to. If not provided, result is returned as text.")
73719
+ }),
73720
+ type: "action"
73721
+ },
73722
+ handle: async (context, params, response) => {
73723
+ const result = await context.runtime.evaluate(params.function, params.target);
73724
+ const text = JSON.stringify(result, null, 2) ?? "undefined";
73725
+ if (params.filename) {
73726
+ const resolvedFilename = await context.resolveOutputFile(params.filename);
73727
+ await writeFile(resolvedFilename, text);
73728
+ response.addTextResult(`Saved evaluation result to "${resolvedFilename}".`);
73729
+ return;
73730
+ }
73731
+ response.addTextResult(text);
73732
+ }
73733
+ })];
73394
73734
  var files_default = [defineTabTool({
73395
73735
  capability: "core",
73396
73736
  schema: {
@@ -73602,7 +73942,7 @@ var typeSchema = elementSchema$2.extend({
73602
73942
  submit: boolean().optional().describe("Whether to submit entered text (press Enter after)"),
73603
73943
  slowly: boolean().optional().describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.")
73604
73944
  });
73605
- var keyboard_default$1 = [defineTabTool({
73945
+ var keyboard_default = [defineTabTool({
73606
73946
  capability: "core-input",
73607
73947
  schema: {
73608
73948
  name: "browser_press_key",
@@ -73648,150 +73988,55 @@ var keyboard_default$1 = [defineTabTool({
73648
73988
  });
73649
73989
  }
73650
73990
  })];
73651
- //#endregion
73652
- //#region src/mcp/backend/tools.ts
73653
- var browserTools = [
73654
- ...common_default$1,
73655
- ...files_default,
73656
- ...keyboard_default$1,
73657
- ...snapshot_default
73658
- ];
73659
- //#endregion
73660
- //#region src/mcp/errors.ts
73661
- var McpToolError = class extends Error {
73662
- code;
73663
- constructor(code, message) {
73664
- super(message);
73665
- this.code = code;
73666
- this.name = "McpToolError";
73667
- }
73668
- };
73669
- function isMcpToolError(error) {
73670
- return error instanceof McpToolError;
73671
- }
73672
- //#endregion
73673
- //#region src/mcp/tool.ts
73674
- function defineTool(tool) {
73675
- return tool;
73676
- }
73677
- function textResult(text, isError = false) {
73678
- return {
73679
- content: [{
73680
- type: "text",
73681
- text
73682
- }],
73683
- ...isError ? { isError: true } : {}
73684
- };
73685
- }
73686
- var connect_default = [defineTool({
73687
- schema: {
73688
- name: "roxy_browser_connect",
73689
- title: "Roxy Browser Connect",
73690
- description: "Attach to an existing browser over CDP or BiDi and seed the active tab snapshot.",
73691
- inputSchema: object({
73692
- protocol: _enum(["cdp", "bidi"]),
73693
- endpoint: string().min(1),
73694
- browser: _enum(["chromium", "firefox"]).optional()
73695
- })
73696
- },
73697
- handle: async (args, runtime) => {
73698
- return textResult(formatConnectResult(await runtime.connect({
73699
- protocol: args.protocol,
73700
- endpoint: args.endpoint,
73701
- ...args.browser ? { browser: args.browser } : {}
73702
- })));
73703
- }
73704
- })];
73705
- var common_default = [defineTool({
73706
- schema: {
73707
- name: "browser_close",
73708
- title: "Close browser",
73709
- description: "Close the page",
73710
- inputSchema: object({})
73711
- },
73712
- handle: async (_args, runtime) => {
73713
- await runtime.close();
73714
- return textResult(formatTabs([]));
73715
- }
73716
- }), defineTool({
73717
- schema: {
73718
- name: "browser_resize",
73719
- title: "Resize browser window",
73720
- description: "Resize the browser window",
73721
- inputSchema: object({
73722
- width: number().describe("Width of the browser window"),
73723
- height: number().describe("Height of the browser window")
73724
- })
73725
- },
73726
- handle: async (args, runtime) => {
73727
- const snap = await runtime.resize(args.width, args.height);
73728
- if (!snap) return textResult(`Resized browser window to ${args.width}x${args.height}.`);
73729
- return textResult(formatSnapshot(snap));
73730
- }
73731
- })];
73732
- var tabs_default = [defineTool({
73733
- schema: {
73734
- name: "browser_tabs",
73735
- title: "Browser Tabs",
73736
- description: "List, create, select, and close browser tabs for the current MCP browser session.",
73737
- inputSchema: object({
73738
- action: _enum([
73739
- "list",
73740
- "new",
73741
- "select",
73742
- "close"
73743
- ]),
73744
- index: number().int().nonnegative().optional(),
73745
- url: string().url().optional()
73746
- })
73747
- },
73748
- handle: async (args, runtime) => {
73749
- if (args.action === "list") return textResult(formatTabs(await runtime.listTabs()));
73750
- if (args.action === "new") {
73751
- const result = await runtime.newTab(args.url);
73752
- return textResult(formatTabsWithOptionalSnapshot(result.tabs, result.snapshot));
73991
+ var navigate_default = [
73992
+ defineTool$1({
73993
+ capability: "core-navigation",
73994
+ schema: {
73995
+ name: "browser_navigate",
73996
+ title: "Navigate to a URL",
73997
+ description: "Navigate to a URL",
73998
+ inputSchema: object({ url: string().describe("The URL to navigate to") }),
73999
+ type: "action"
74000
+ },
74001
+ handle: async (context, params, response) => {
74002
+ await context.ensureTab();
74003
+ await context.runtime.navigate(params.url);
74004
+ response.setIncludeSnapshot();
74005
+ response.addCode(`await page.goto('${params.url.startsWith("http") ? params.url : params.url.startsWith("localhost") ? `http://${params.url}` : `https://${params.url}`}');`);
73753
74006
  }
73754
- if (args.action === "select") {
73755
- const result = await runtime.selectTab(args.index);
73756
- return textResult(formatTabsWithOptionalSnapshot(result.tabs, result.snapshot));
74007
+ }),
74008
+ defineTool$1({
74009
+ capability: "core-navigation",
74010
+ schema: {
74011
+ name: "browser_navigate_back",
74012
+ title: "Go back",
74013
+ description: "Go back to the previous page in the history",
74014
+ inputSchema: object({}),
74015
+ type: "action"
74016
+ },
74017
+ handle: async (context, _params, response) => {
74018
+ await context.runtime.goBack();
74019
+ response.setIncludeSnapshot();
74020
+ response.addCode("await page.goBack();");
73757
74021
  }
73758
- const result = await runtime.closeTab(args.index);
73759
- return textResult(formatTabsWithOptionalSnapshot(result.tabs, result.snapshot));
73760
- }
73761
- })];
73762
- //#endregion
73763
- //#region src/mcp/tools/navigate.ts
73764
- var navigate = defineTool({
73765
- schema: {
73766
- name: "browser_navigate",
73767
- title: "Navigate to a URL",
73768
- description: "Navigate to a URL",
73769
- inputSchema: object({ url: string().describe("The URL to navigate to") })
73770
- },
73771
- handle: async (args, runtime) => {
73772
- const snap = await runtime.navigate(args.url);
73773
- if (!snap) return textResult(`Navigated to "${args.url}".`);
73774
- return textResult(formatSnapshot(snap));
73775
- }
73776
- });
73777
- var goBack = defineTool({
73778
- schema: {
73779
- name: "browser_navigate_back",
73780
- title: "Go back",
73781
- description: "Go back to the previous page in the history",
73782
- inputSchema: object({})
73783
- },
73784
- handle: async (_args, runtime) => {
73785
- const snap = await runtime.goBack();
73786
- if (!snap) return textResult("Navigated back.");
73787
- return textResult(formatSnapshot(snap));
73788
- }
73789
- });
73790
- object({});
73791
- var navigate_default = [
73792
- navigate,
73793
- goBack,
73794
- defineTool({
74022
+ }),
74023
+ defineTool$1({
74024
+ capability: "core-navigation",
74025
+ schema: {
74026
+ name: "browser_navigate_forward",
74027
+ title: "Go forward",
74028
+ description: "Go forward to the next page in the history",
74029
+ inputSchema: object({}),
74030
+ type: "action"
74031
+ },
74032
+ handle: async (context, _params, response) => {
74033
+ await context.runtime.goForward();
74034
+ response.setIncludeSnapshot();
74035
+ response.addCode("await page.goForward();");
74036
+ }
74037
+ }),
74038
+ defineTool$1({
74039
+ capability: "core-navigation",
73795
74040
  schema: {
73796
74041
  name: "browser_wait_for",
73797
74042
  title: "Wait for",
@@ -73800,228 +74045,31 @@ var navigate_default = [
73800
74045
  time: number().optional().describe("The time to wait in seconds"),
73801
74046
  text: string().optional().describe("The text to wait for"),
73802
74047
  textGone: string().optional().describe("The text to wait for to disappear")
73803
- })
74048
+ }),
74049
+ type: "action"
73804
74050
  },
73805
- handle: async (args, runtime) => {
73806
- if (!args.text && !args.textGone && !args.time) throw new Error("Either time, text or textGone must be provided");
73807
- if (args.time) await new Promise((resolve) => setTimeout(resolve, Math.min(3e4, args.time * 1e3)));
73808
- return textResult(formatSnapshot(await runtime.waitFor({
73809
- ...args.text !== void 0 ? { text: args.text } : {},
73810
- ...args.textGone !== void 0 ? { textGone: args.textGone } : {}
73811
- }, 5e3)));
74051
+ handle: async (context, params, response) => {
74052
+ if (!params.text && !params.textGone && !params.time) throw new Error("Either time, text or textGone must be provided");
74053
+ const waitSeconds = params.time;
74054
+ if (waitSeconds !== void 0) await new Promise((resolve) => setTimeout(resolve, Math.min(3e4, waitSeconds * 1e3)));
74055
+ if (params.text || params.textGone) await context.runtime.waitFor({
74056
+ ...params.text !== void 0 ? { text: params.text } : {},
74057
+ ...params.textGone !== void 0 ? { textGone: params.textGone } : {}
74058
+ }, 5e3);
74059
+ response.setIncludeSnapshot();
73812
74060
  }
73813
74061
  })
73814
74062
  ];
73815
- object({
73816
- element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73817
- target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
73818
- }).extend({
73819
- doubleClick: boolean().optional().describe("Whether to perform a double click instead of a single click"),
73820
- button: _enum([
73821
- "left",
73822
- "right",
73823
- "middle"
73824
- ]).optional().describe("Button to click, defaults to left"),
73825
- modifiers: array(_enum([
73826
- "Alt",
73827
- "Control",
73828
- "ControlOrMeta",
73829
- "Meta",
73830
- "Shift"
73831
- ])).optional().describe("Modifier keys to press during the click"),
73832
- human: object({ profile: _enum([
73833
- "cautious",
73834
- "balanced",
73835
- "fast"
73836
- ]).optional().describe("Humanization timing profile, defaults to balanced") }).optional().describe("Humanization settings for this click")
73837
- });
73838
- object({
73839
- startElement: string().optional().describe("Human-readable source element description used to obtain the permission to interact with the element"),
73840
- startTarget: string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73841
- endElement: string().optional().describe("Human-readable target element description used to obtain the permission to interact with the element"),
73842
- endTarget: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
73843
- });
73844
- object({
73845
- element: string().optional().describe("Human-readable element description used to obtain permission"),
73846
- ref: string().optional().describe("Element to scroll; omit to scroll the whole page"),
73847
- deltaX: number().optional().describe("Horizontal scroll delta in pixels (default 0)"),
73848
- deltaY: number().optional().describe("Vertical scroll delta in pixels (default 0)")
73849
- });
73850
- var mouse_default = [];
73851
74063
  //#endregion
73852
- //#region src/mcp/tools/keyboard.ts
73853
- object({
73854
- element: string().optional().describe("Human-readable element description used to obtain permission"),
73855
- target: string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73856
- text: string().describe("Text to type into the element"),
73857
- submit: boolean().optional().describe("Whether to submit entered text (press Enter after)"),
73858
- slowly: boolean().optional().describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.")
73859
- });
73860
- object({ key: string().describe("Key to press, e.g. Enter, Escape, Tab, ArrowLeft, Backspace, Delete, or printable characters") });
73861
- var keyboard_default = [];
73862
- object({
73863
- element: string().optional().describe("Human-readable element description used to obtain permission"),
73864
- target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
73865
- }).extend({ values: array(string()).describe("Option values or visible labels to select") });
73866
- object({ paths: array(string()).optional().describe("The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.") });
73867
- var form_default = [defineTool({
73868
- schema: {
73869
- name: "browser_fill_form",
73870
- title: "Fill form",
73871
- description: "Fill multiple form fields",
73872
- inputSchema: object({ fields: array(object({
73873
- name: string().describe("Human-readable field name"),
73874
- type: _enum([
73875
- "textbox",
73876
- "checkbox",
73877
- "radio",
73878
- "combobox",
73879
- "slider"
73880
- ]).describe("Type of the field"),
73881
- target: string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73882
- value: string().describe("Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.")
73883
- })).describe("Fields to fill in") })
73884
- },
73885
- handle: async (args, runtime) => {
73886
- const snap = await runtime.fillForm(args.fields);
73887
- if (!snap) return textResult("Filled form.");
73888
- return textResult(formatSnapshot(snap));
73889
- }
73890
- }), defineTool({
73891
- schema: {
73892
- name: "browser_drop",
73893
- title: "Drop files or data onto an element",
73894
- description: "Drop files or MIME-typed data onto an element, as if dragged from outside the page. At least one of paths or data must be provided.",
73895
- inputSchema: object({
73896
- element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73897
- target: string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73898
- paths: array(string()).optional().describe("Absolute paths to files to drop onto the element."),
73899
- data: record(string(), string()).optional().describe("Data to drop, as a map of MIME type to string value.")
73900
- })
73901
- },
73902
- handle: async (args, runtime) => {
73903
- if (!args.paths?.length && !args.data) throw new Error("At least one of \"paths\" or \"data\" must be provided.");
73904
- const snap = await runtime.drop(args.target, {
73905
- ...args.paths !== void 0 ? { paths: args.paths } : {},
73906
- ...args.data !== void 0 ? { data: args.data } : {}
73907
- });
73908
- if (!snap) return textResult(`Dropped data onto "${args.element ?? args.target}".`);
73909
- return textResult(formatSnapshot(snap));
73910
- }
73911
- })];
73912
- var screenshot_default = [defineTool({
73913
- schema: {
73914
- name: "browser_take_screenshot",
73915
- title: "Browser Take Screenshot",
73916
- description: "Capture a full-page screenshot of the active tab as a base64-encoded PNG.",
73917
- inputSchema: object({
73918
- element: string().optional().describe("Human-readable description of the area to screenshot"),
73919
- target: string().optional().describe("Element reference or CSS selector to clip screenshot to; omit for full page"),
73920
- type: _enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
73921
- filename: string().optional().describe("File name to save the screenshot to."),
73922
- fullPage: boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.")
73923
- })
73924
- },
73925
- handle: async (args, runtime) => {
73926
- const target = args.target;
73927
- const result = await runtime.takeScreenshot({
73928
- type: args.type,
73929
- ...args.fullPage !== void 0 ? { fullPage: args.fullPage } : {},
73930
- ...target !== void 0 ? { target } : {}
73931
- });
73932
- if (args.filename) {
73933
- const resolvedFilename = await resolveOutputFilePath(args.filename, { outputDir: runtime.getOutputDir() });
73934
- await writeFile(resolvedFilename, Buffer.from(result.data, "base64"));
73935
- return textResult(`Screenshot saved to "${resolvedFilename}".`);
73936
- }
73937
- return { content: [{
73938
- type: "image",
73939
- data: result.data,
73940
- mimeType: result.mimeType
73941
- }] };
73942
- }
73943
- })];
73944
- var console_default = [defineTool({
73945
- schema: {
73946
- name: "browser_console_messages",
73947
- title: "Get console messages",
73948
- description: "Returns all console messages",
73949
- inputSchema: object({
73950
- level: _enum([
73951
- "error",
73952
- "warning",
73953
- "info",
73954
- "debug"
73955
- ]).default("info").describe("Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\"."),
73956
- all: boolean().optional().describe("Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false."),
73957
- filename: string().optional().describe("Filename to save the console messages to. If not provided, messages are returned as text.")
73958
- })
73959
- },
73960
- handle: async (args, runtime) => {
73961
- const messages = await runtime.consoleMessages(args.level, args.all);
73962
- const errors = messages.filter((message) => message.type === "error" || message.type === "assert").length;
73963
- const warnings = messages.filter((message) => message.type === "warning").length;
73964
- const text = [
73965
- `Total messages: ${messages.length} (Errors: ${errors}, Warnings: ${warnings})`,
73966
- "",
73967
- ...messages.map((message) => message.formattedText)
73968
- ].join("\n");
73969
- if (args.filename) {
73970
- const resolvedFilename = await resolveOutputFilePath(args.filename, { outputDir: runtime.getOutputDir() });
73971
- await writeFile(resolvedFilename, text);
73972
- return textResult(`Saved console messages to "${resolvedFilename}".`);
73973
- }
73974
- return textResult(text);
73975
- }
73976
- })];
73977
- var evaluate_default = [defineTool({
73978
- schema: {
73979
- name: "browser_evaluate",
73980
- title: "Evaluate JavaScript",
73981
- description: "Evaluate JavaScript expression on page or element",
73982
- inputSchema: object({
73983
- element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73984
- target: string().optional().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73985
- function: string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
73986
- filename: string().optional().describe("Filename to save the result to. If not provided, result is returned as text.")
73987
- })
73988
- },
73989
- handle: async (args, runtime) => {
73990
- const result = await runtime.evaluate(args.function, args.target);
73991
- const text = JSON.stringify(result, null, 2) ?? "undefined";
73992
- if (args.filename) {
73993
- const resolvedFilename = await resolveOutputFilePath(args.filename, { outputDir: runtime.getOutputDir() });
73994
- await writeFile(resolvedFilename, text);
73995
- return textResult(`Saved evaluation result to "${resolvedFilename}".`);
73996
- }
73997
- return textResult(text);
73998
- }
73999
- })];
74000
- var dialog_default = [defineTool({
74001
- schema: {
74002
- name: "browser_handle_dialog",
74003
- title: "Handle a dialog",
74004
- description: "Handle a dialog",
74005
- inputSchema: object({
74006
- accept: boolean().describe("Whether to accept the dialog."),
74007
- promptText: string().optional().describe("The text of the prompt in case of a prompt dialog.")
74008
- })
74009
- },
74010
- handle: async (args, runtime) => {
74011
- const snap = await runtime.handleDialog(args.accept, args.promptText);
74012
- if (!snap) return textResult(args.accept ? "Accepted dialog." : "Dismissed dialog.");
74013
- return textResult(formatSnapshot(snap));
74014
- }
74015
- })];
74016
- //#endregion
74017
- //#region src/mcp/tools/network.ts
74064
+ //#region src/mcp/backend/network.ts
74018
74065
  var requestParts = [
74019
74066
  "request-headers",
74020
74067
  "request-body",
74021
74068
  "response-headers",
74022
74069
  "response-body"
74023
74070
  ];
74024
- var networkRequests = defineTool({
74071
+ var networkRequests = defineTool$1({
74072
+ capability: "core",
74025
74073
  schema: {
74026
74074
  name: "browser_network_requests",
74027
74075
  title: "List network requests",
@@ -74030,10 +74078,11 @@ var networkRequests = defineTool({
74030
74078
  static: boolean().default(false).describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."),
74031
74079
  filter: string().optional().describe("Only return requests whose URL matches this regexp (e.g. \"/api/.*user\")."),
74032
74080
  filename: string().optional().describe("Filename to save the network requests to. If not provided, requests are returned as text.")
74033
- })
74081
+ }),
74082
+ type: "readOnly"
74034
74083
  },
74035
- handle: async (args, runtime) => {
74036
- const requests = await runtime.networkRequests();
74084
+ handle: async (context, args, response) => {
74085
+ const requests = await context.runtime.networkRequests();
74037
74086
  const filter = args.filter ? new RegExp(args.filter) : void 0;
74038
74087
  const lines = [];
74039
74088
  let hiddenStaticCount = 0;
@@ -74048,14 +74097,16 @@ var networkRequests = defineTool({
74048
74097
  if (hiddenStaticCount > 0) lines.push(`\nNote: ${hiddenStaticCount} static request${hiddenStaticCount === 1 ? "" : "s"} not shown, run with "static" option to see ${hiddenStaticCount === 1 ? "it" : "them"}.`);
74049
74098
  const text = lines.join("\n");
74050
74099
  if (args.filename) {
74051
- const resolvedFilename = await resolveOutputFilePath(args.filename, { outputDir: runtime.getOutputDir() });
74100
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
74052
74101
  await writeFile(resolvedFilename, text);
74053
- return textResult(`Saved network requests to "${resolvedFilename}".`);
74102
+ response.addTextResult(`Saved network requests to "${resolvedFilename}".`);
74103
+ return;
74054
74104
  }
74055
- return textResult(text);
74105
+ response.addTextResult(text);
74056
74106
  }
74057
74107
  });
74058
- var networkRequest = defineTool({
74108
+ var networkRequest = defineTool$1({
74109
+ capability: "core",
74059
74110
  schema: {
74060
74111
  name: "browser_network_request",
74061
74112
  title: "Show network request details",
@@ -74064,18 +74115,33 @@ var networkRequest = defineTool({
74064
74115
  index: number().int().min(1).describe("1-based index of the request, as printed by browser_network_requests."),
74065
74116
  part: _enum(requestParts).optional().describe("Return only this part of the request. Omit to return full details."),
74066
74117
  filename: string().optional().describe("Filename to save the result to. If not provided, output is returned as text.")
74067
- })
74118
+ }),
74119
+ type: "readOnly"
74068
74120
  },
74069
- handle: async (args, runtime) => {
74070
- const request = await runtime.networkRequest(args.index);
74071
- if (!request) return textResult(`Request #${args.index} not found. Use browser_network_requests to see available indexes.`, true);
74072
- const text = args.part ? renderRequestPart(request, args.part) : renderRequestDetails(request);
74121
+ handle: async (context, args, response) => {
74122
+ const request = await context.runtime.networkRequest(args.index);
74123
+ if (!request) {
74124
+ response.addError(`Request #${args.index} not found. Use browser_network_requests to see available indexes.`);
74125
+ return;
74126
+ }
74127
+ if (args.part) {
74128
+ response.setRawResults();
74129
+ const partText = args.part === "response-body" ? await context.runtime.fetchResponseBody(args.index) ?? "" : renderRequestPart(request, args.part);
74130
+ if (args.filename) {
74131
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
74132
+ await writeFile(resolvedFilename, partText);
74133
+ response.addTextResult(`Saved network request to "${resolvedFilename}".`);
74134
+ } else response.addTextResult(partText);
74135
+ return;
74136
+ }
74137
+ const text = renderRequestDetails(request);
74073
74138
  if (args.filename) {
74074
- const resolvedFilename = await resolveOutputFilePath(args.filename, { outputDir: runtime.getOutputDir() });
74139
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
74075
74140
  await writeFile(resolvedFilename, text);
74076
- return textResult(`Saved network request to "${resolvedFilename}".`);
74141
+ response.addTextResult(`Saved network request to "${resolvedFilename}".`);
74142
+ return;
74077
74143
  }
74078
- return textResult(text);
74144
+ response.addTextResult(text);
74079
74145
  }
74080
74146
  });
74081
74147
  function isSuccessfulResponse(request) {
@@ -74102,8 +74168,10 @@ function renderRequestDetails(request) {
74102
74168
  if (request.mimeType) lines.push(` mimeType: ${request.mimeType}`);
74103
74169
  appendHeaders(lines, "Request headers", request.requestHeaders);
74104
74170
  if (request.responseHeaders) appendHeaders(lines, "Response headers", request.responseHeaders);
74105
- if (request.requestBody) lines.push("", `Call browser_network_request with part="request-body" to read the request body.`);
74106
- if (request.responseBody) lines.push("", `Call browser_network_request with part="response-body" to read the response body.`);
74171
+ const hints = [];
74172
+ if (request.requestBody) hints.push(`Call browser_network_request with part="request-body" to read the request body.`);
74173
+ if (canHaveResponseBody(request)) hints.push(`Call browser_network_request with part="response-body" to read the response body.`);
74174
+ if (hints.length) lines.push("", ...hints);
74107
74175
  return lines.join("\n");
74108
74176
  }
74109
74177
  function renderRequestPart(request, part) {
@@ -74122,46 +74190,247 @@ function appendHeaders(lines, title, headers) {
74122
74190
  function renderHeaders(headers) {
74123
74191
  return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
74124
74192
  }
74193
+ function canHaveResponseBody(request) {
74194
+ if (request.failureText || request.status === void 0) return false;
74195
+ return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
74196
+ }
74125
74197
  var network_default = [networkRequests, networkRequest];
74126
- var runCode_default = [defineTool({
74198
+ var runCode_default = [defineTool$1({
74199
+ capability: "devtools",
74127
74200
  schema: {
74128
74201
  name: "browser_run_code_unsafe",
74129
- title: "Run Playwright code (unsafe)",
74130
- description: "Run a Playwright code snippet. Unsafe: executes arbitrary JavaScript in the browser context approximation and is RCE-equivalent in intent.",
74202
+ title: "Run code (unsafe)",
74203
+ description: "Run arbitrary code against the current browser session.",
74204
+ inputSchema: object({ code: string().describe("JavaScript code to run against the browser session.") }),
74205
+ type: "action"
74206
+ },
74207
+ handle: async (context, args, response) => {
74208
+ const result = await context.runtime.runCodeUnsafe(args.code);
74209
+ response.addTextResult(JSON.stringify(result, null, 2) ?? "undefined");
74210
+ }
74211
+ })];
74212
+ var screenshot_default = [defineTool$1({
74213
+ capability: "core",
74214
+ schema: {
74215
+ name: "browser_take_screenshot",
74216
+ title: "Browser Take Screenshot",
74217
+ description: "Capture a full-page screenshot of the active tab as a base64-encoded PNG.",
74131
74218
  inputSchema: object({
74132
- code: string().optional().describe("A JavaScript function containing Playwright-like code to execute."),
74133
- filename: string().optional().describe("Load code from the specified file. If both code and filename are provided, code will be ignored.")
74134
- })
74219
+ element: string().optional().describe("Human-readable description of the area to screenshot"),
74220
+ target: string().optional().describe("Element reference or CSS selector to clip screenshot to; omit for full page"),
74221
+ type: _enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
74222
+ filename: string().optional().describe("File name to save the screenshot to."),
74223
+ fullPage: boolean().optional().describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.")
74224
+ }),
74225
+ type: "readOnly"
74135
74226
  },
74136
- handle: async (args, runtime) => {
74137
- const code = args.filename ? await readFile(args.filename, "utf8") : args.code;
74138
- if (!code) throw new Error("Either code or filename must be provided.");
74139
- const result = await runtime.runCodeUnsafe(code);
74140
- return textResult(JSON.stringify(result, null, 2) ?? "undefined");
74227
+ handle: async (context, args, response) => {
74228
+ const result = await context.runtime.takeScreenshot({
74229
+ type: args.type,
74230
+ ...args.fullPage !== void 0 ? { fullPage: args.fullPage } : {},
74231
+ ...args.target !== void 0 ? { target: args.target } : {}
74232
+ });
74233
+ const requestedFilename = args.filename?.trim();
74234
+ const resolvedFilename = await context.resolveOutputFile(requestedFilename || `page-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}.${args.type}`);
74235
+ await writeFile(resolvedFilename, Buffer.from(result.data, "base64"));
74236
+ if (requestedFilename) {
74237
+ response.addTextResult(`Screenshot saved to "${resolvedFilename}".`);
74238
+ return;
74239
+ }
74240
+ response.addTextResult(resolvedFilename);
74241
+ response.addImageResult(result.data, result.mimeType);
74242
+ }
74243
+ })];
74244
+ var tabs_default = [defineTool$1({
74245
+ capability: "core-tabs",
74246
+ schema: {
74247
+ name: "browser_tabs",
74248
+ title: "Browser Tabs",
74249
+ description: "List, create, select, and close browser tabs for the current MCP browser session.",
74250
+ inputSchema: object({
74251
+ action: _enum([
74252
+ "list",
74253
+ "new",
74254
+ "select",
74255
+ "close"
74256
+ ]).describe("Operation to perform"),
74257
+ index: number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed."),
74258
+ url: string().optional().describe("URL to navigate to in the new tab, used for new.")
74259
+ }),
74260
+ type: "action"
74261
+ },
74262
+ handle: async (context, params, response) => {
74263
+ switch (params.action) {
74264
+ case "list":
74265
+ await context.ensureTab();
74266
+ break;
74267
+ case "new":
74268
+ await context.runtime.newTab(params.url);
74269
+ if (params.url) {
74270
+ response.setIncludeSnapshot();
74271
+ response.addCode(`await page.goto('${params.url}');`);
74272
+ }
74273
+ break;
74274
+ case "close":
74275
+ await context.runtime.closeTab(params.index ?? 0);
74276
+ break;
74277
+ case "select":
74278
+ if (params.index === void 0) throw new Error("Tab index is required");
74279
+ await context.runtime.selectTab(params.index);
74280
+ break;
74281
+ }
74282
+ const tabs = await context.runtime.listTabs();
74283
+ response.addTextResult(formatTabs(tabs));
74141
74284
  }
74142
74285
  })];
74143
74286
  //#endregion
74144
- //#region src/mcp/tools/index.ts
74145
- var allTools = [
74146
- ...connect_default,
74287
+ //#region src/mcp/backend/tools.ts
74288
+ var browserTools = [
74147
74289
  ...common_default,
74148
- ...tabs_default,
74149
- ...navigate_default,
74150
- ...mouse_default,
74151
- ...keyboard_default,
74152
- ...form_default,
74153
- ...screenshot_default,
74154
74290
  ...console_default,
74291
+ ...connect_default,
74292
+ ...dialogs_default,
74155
74293
  ...evaluate_default,
74156
- ...dialog_default,
74294
+ ...files_default,
74295
+ ...keyboard_default,
74296
+ ...navigate_default,
74157
74297
  ...network_default,
74158
- ...runCode_default
74298
+ ...runCode_default,
74299
+ ...screenshot_default,
74300
+ ...snapshot_default,
74301
+ ...tabs_default
74159
74302
  ];
74160
74303
  //#endregion
74304
+ //#region src/mcp/errors.ts
74305
+ var McpToolError = class extends Error {
74306
+ code;
74307
+ constructor(code, message) {
74308
+ super(message);
74309
+ this.code = code;
74310
+ this.name = "McpToolError";
74311
+ }
74312
+ };
74313
+ function isMcpToolError(error) {
74314
+ return error instanceof McpToolError;
74315
+ }
74316
+ //#endregion
74317
+ //#region src/mcp/tool.ts
74318
+ function defineTool(tool) {
74319
+ return tool;
74320
+ }
74321
+ function textResult(text, isError = false) {
74322
+ return {
74323
+ content: [{
74324
+ type: "text",
74325
+ text
74326
+ }],
74327
+ ...isError ? { isError: true } : {}
74328
+ };
74329
+ }
74330
+ object({
74331
+ element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
74332
+ target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
74333
+ }).extend({
74334
+ doubleClick: boolean().optional().describe("Whether to perform a double click instead of a single click"),
74335
+ button: _enum([
74336
+ "left",
74337
+ "right",
74338
+ "middle"
74339
+ ]).optional().describe("Button to click, defaults to left"),
74340
+ modifiers: array(_enum([
74341
+ "Alt",
74342
+ "Control",
74343
+ "ControlOrMeta",
74344
+ "Meta",
74345
+ "Shift"
74346
+ ])).optional().describe("Modifier keys to press during the click"),
74347
+ human: object({ profile: _enum([
74348
+ "cautious",
74349
+ "balanced",
74350
+ "fast"
74351
+ ]).optional().describe("Humanization timing profile, defaults to balanced") }).optional().describe("Humanization settings for this click")
74352
+ });
74353
+ object({
74354
+ startElement: string().optional().describe("Human-readable source element description used to obtain the permission to interact with the element"),
74355
+ startTarget: string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
74356
+ endElement: string().optional().describe("Human-readable target element description used to obtain the permission to interact with the element"),
74357
+ endTarget: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
74358
+ });
74359
+ object({
74360
+ element: string().optional().describe("Human-readable element description used to obtain permission"),
74361
+ ref: string().optional().describe("Element to scroll; omit to scroll the whole page"),
74362
+ deltaX: number().optional().describe("Horizontal scroll delta in pixels (default 0)"),
74363
+ deltaY: number().optional().describe("Vertical scroll delta in pixels (default 0)")
74364
+ });
74365
+ var mouse_default = [];
74366
+ object({
74367
+ element: string().optional().describe("Human-readable element description used to obtain permission"),
74368
+ target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
74369
+ }).extend({ values: array(string()).describe("Option values or visible labels to select") });
74370
+ object({ paths: array(string()).optional().describe("The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.") });
74371
+ var form_default = [defineTool({
74372
+ schema: {
74373
+ name: "browser_fill_form",
74374
+ title: "Fill form",
74375
+ description: "Fill multiple form fields",
74376
+ inputSchema: object({ fields: array(object({
74377
+ name: string().describe("Human-readable field name"),
74378
+ type: _enum([
74379
+ "textbox",
74380
+ "checkbox",
74381
+ "radio",
74382
+ "combobox",
74383
+ "slider"
74384
+ ]).describe("Type of the field"),
74385
+ target: string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
74386
+ value: string().describe("Value to fill in the field. If the field is a checkbox, the value should be `true` or `false`. If the field is a combobox, the value should be the text of the option.")
74387
+ })).describe("Fields to fill in") })
74388
+ },
74389
+ handle: async (args, runtime) => {
74390
+ const snap = await runtime.fillForm(args.fields);
74391
+ if (!snap) return textResult("Filled form.");
74392
+ return textResult(formatSnapshot(snap));
74393
+ }
74394
+ }), defineTool({
74395
+ schema: {
74396
+ name: "browser_drop",
74397
+ title: "Drop files or data onto an element",
74398
+ description: "Drop files or MIME-typed data onto an element, as if dragged from outside the page. At least one of paths or data must be provided.",
74399
+ inputSchema: object({
74400
+ element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
74401
+ target: string().describe("Exact target element reference from the page snapshot, or a unique element selector"),
74402
+ paths: array(string()).optional().describe("Absolute paths to files to drop onto the element."),
74403
+ data: record(string(), string()).optional().describe("Data to drop, as a map of MIME type to string value.")
74404
+ })
74405
+ },
74406
+ handle: async (args, runtime) => {
74407
+ if (!args.paths?.length && !args.data) throw new Error("At least one of \"paths\" or \"data\" must be provided.");
74408
+ const snap = await runtime.drop(args.target, {
74409
+ ...args.paths !== void 0 ? { paths: args.paths } : {},
74410
+ ...args.data !== void 0 ? { data: args.data } : {}
74411
+ });
74412
+ if (!snap) return textResult(`Dropped data onto "${args.element ?? args.target}".`);
74413
+ return textResult(formatSnapshot(snap));
74414
+ }
74415
+ })];
74416
+ //#endregion
74417
+ //#region src/mcp/tools/index.ts
74418
+ var allTools = [...mouse_default, ...form_default];
74419
+ //#endregion
74161
74420
  //#region src/mcp/connectedBrowser.ts
74162
74421
  function delay$1(ms) {
74163
74422
  return new Promise((resolve) => setTimeout(resolve, ms));
74164
74423
  }
74424
+ async function withBiDiTimeout(promise, timeoutMs) {
74425
+ let timer;
74426
+ try {
74427
+ return await Promise.race([promise, new Promise((_, reject) => {
74428
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`Timed out after ${timeoutMs}ms.`)), timeoutMs);
74429
+ })]);
74430
+ } finally {
74431
+ if (timer) clearTimeout(timer);
74432
+ }
74433
+ }
74165
74434
  var chromeRemoteInterface = "default" in import_chrome_remote_interface ? import_chrome_remote_interface.default : import_chrome_remote_interface;
74166
74435
  function buildConnectionFromWsEndpoint(browserWsEndpoint) {
74167
74436
  const parsed = new URL(browserWsEndpoint);
@@ -74595,13 +74864,13 @@ var CDP_KEY_MAP = {
74595
74864
  async function evaluateBiDi(client, contextId, functionSource, arg) {
74596
74865
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
74597
74866
  const response = await client.scriptEvaluate({
74598
- expression,
74867
+ expression: wrapWithSerializedEvaluationResult(expression),
74599
74868
  target: { context: contextId },
74600
74869
  awaitPromise: true,
74601
74870
  resultOwnership: "none"
74602
74871
  });
74603
74872
  if (response.type === "exception") throw new Error(response.exceptionDetails?.text || "BiDi runtime evaluation failed.");
74604
- return response.result?.value;
74873
+ return parseSerializedEvaluationResult(extractBiDiValue(response.result));
74605
74874
  }
74606
74875
  async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74607
74876
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
@@ -74623,6 +74892,16 @@ async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74623
74892
  ...response.result?.value?.handle !== void 0 ? { handle: response.result.value.handle } : {}
74624
74893
  };
74625
74894
  }
74895
+ function extractBiDiValue(value) {
74896
+ if (!value) return;
74897
+ if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue(entry));
74898
+ if (value.type === "object" && Array.isArray(value.value)) {
74899
+ const obj = {};
74900
+ for (const [key, val] of value.value) obj[key] = extractBiDiValue(val);
74901
+ return obj;
74902
+ }
74903
+ return value.value;
74904
+ }
74626
74905
  function toAriaSnapshotPayload(request = {}) {
74627
74906
  return {
74628
74907
  options: normalizeAriaSnapshotOptions({
@@ -74662,6 +74941,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74662
74941
  pageConsoleStates = /* @__PURE__ */ new Map();
74663
74942
  pageNetworkStates = /* @__PURE__ */ new Map();
74664
74943
  pageDialogStates = /* @__PURE__ */ new Map();
74944
+ dialogWaiters = /* @__PURE__ */ new Map();
74665
74945
  activeTabId;
74666
74946
  versionString = "Chromium/unknown";
74667
74947
  constructor(browserClient, connection) {
@@ -74677,7 +74957,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74677
74957
  });
74678
74958
  const session = new CdpConnectedBrowserSession(await chromeRemoteInterface({ target: connection.browserWsEndpoint }), connection);
74679
74959
  session.versionString = version.Browser;
74680
- if ((await session.refreshTabs()).length === 0) await session.newTab();
74960
+ await session.refreshTabs();
74681
74961
  await session.getActivePageClient().catch(() => void 0);
74682
74962
  return session;
74683
74963
  }
@@ -74729,6 +75009,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74729
75009
  async click(target, options) {
74730
75010
  const pageClient = await this.getActivePageClient();
74731
75011
  const contextId = await this.getActiveUtilityContextId(pageClient);
75012
+ const tabId = await this.getActiveTabId();
74732
75013
  const point = await evaluateCdp(pageClient, "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE, "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector }, contextId);
74733
75014
  if (!point.ok || point.x === void 0 || point.y === void 0) {
74734
75015
  const isSelector = "selector" in target;
@@ -74762,7 +75043,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74762
75043
  modifiers: modifiersMask
74763
75044
  });
74764
75045
  await delay$1(options.clickHoldMs);
74765
- await pageClient.Input.dispatchMouseEvent({
75046
+ const releasePromise = pageClient.Input.dispatchMouseEvent({
74766
75047
  type: "mouseReleased",
74767
75048
  x: point.x,
74768
75049
  y: point.y,
@@ -74770,6 +75051,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
74770
75051
  clickCount,
74771
75052
  modifiers: modifiersMask
74772
75053
  });
75054
+ await Promise.race([releasePromise, this.waitForDialog(tabId, options.clickHoldMs + 1e3)]);
74773
75055
  }
74774
75056
  }
74775
75057
  async drag(start, end, options) {
@@ -75025,24 +75307,72 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75025
75307
  }
75026
75308
  }
75027
75309
  async handleDialog(accept, promptText) {
75028
- const tabId = await this.getActiveTabId();
75310
+ const tabId = this.dialogTabId();
75029
75311
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
75030
- const pageClient = await this.getActivePageClient();
75312
+ const pageClient = this.pageClients.get(tabId) ?? await this.getActivePageClient();
75031
75313
  this.pageDialogStates.delete(tabId);
75032
75314
  await pageClient.Page.handleJavaScriptDialog({
75033
75315
  accept,
75034
75316
  ...promptText !== void 0 ? { promptText } : {}
75035
75317
  });
75036
75318
  }
75319
+ async hasDialog() {
75320
+ return this.pageDialogStates.size > 0;
75321
+ }
75322
+ waitForDialog(tabId, timeoutMs) {
75323
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
75324
+ return new Promise((resolve) => {
75325
+ const waiter = { resolve: () => {
75326
+ if (waiter.timer) clearTimeout(waiter.timer);
75327
+ this.removeDialogWaiter(tabId, waiter);
75328
+ resolve();
75329
+ } };
75330
+ waiter.timer = setTimeout(() => waiter.resolve(), timeoutMs);
75331
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
75332
+ waiters.add(waiter);
75333
+ this.dialogWaiters.set(tabId, waiters);
75334
+ });
75335
+ }
75336
+ resolveDialogWaiters(tabId) {
75337
+ const waiters = this.dialogWaiters.get(tabId);
75338
+ if (!waiters) return;
75339
+ this.dialogWaiters.delete(tabId);
75340
+ for (const waiter of waiters) waiter.resolve();
75341
+ }
75342
+ removeDialogWaiter(tabId, waiter) {
75343
+ const waiters = this.dialogWaiters.get(tabId);
75344
+ if (!waiters) return;
75345
+ waiters.delete(waiter);
75346
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
75347
+ }
75037
75348
  async networkRequests() {
75038
75349
  const tabId = await this.getActiveTabId();
75350
+ await this.hydratePerformanceResourceRequests(tabId);
75039
75351
  return this.ensureNetworkState(tabId).requests.map(cloneNetworkRequest);
75040
75352
  }
75041
75353
  async networkRequest(index) {
75042
75354
  const tabId = await this.getActiveTabId();
75355
+ await this.hydratePerformanceResourceRequests(tabId);
75043
75356
  const request = this.ensureNetworkState(tabId).requests[index - 1];
75044
75357
  return request ? cloneNetworkRequest(request) : void 0;
75045
75358
  }
75359
+ async fetchResponseBody(index) {
75360
+ const tabId = await this.getActiveTabId();
75361
+ const state = this.ensureNetworkState(tabId);
75362
+ const request = state.requests[index - 1];
75363
+ if (!request || !request.requestId) return request?.responseBody;
75364
+ if (!canReadResponseBody(request)) return;
75365
+ if (request.responseBody !== void 0) return request.responseBody;
75366
+ await waitForLoadingDone(state, request.requestId, 5e3).catch(() => void 0);
75367
+ if (request.responseBody !== void 0) return request.responseBody;
75368
+ if (state.bodyRead.has(request.requestId)) return;
75369
+ state.bodyRead.add(request.requestId);
75370
+ const clientNetwork = (this.pageClients.get(tabId) ?? await this.getActivePageClient()).Network;
75371
+ if (!clientNetwork) return;
75372
+ const body = await clientNetwork.getResponseBody({ requestId: request.requestId }).catch(() => void 0);
75373
+ if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75374
+ return request.responseBody;
75375
+ }
75046
75376
  async runCodeUnsafe(code) {
75047
75377
  const pageClient = await this.getActivePageClient();
75048
75378
  const contextId = await this.getActiveUtilityContextId(pageClient);
@@ -75102,6 +75432,12 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75102
75432
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
75103
75433
  return activeTab.id;
75104
75434
  }
75435
+ dialogTabId() {
75436
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
75437
+ const tabId = this.pageDialogStates.keys().next().value;
75438
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
75439
+ return tabId;
75440
+ }
75105
75441
  async getActivePageClient() {
75106
75442
  const activeTab = (await this.refreshTabs()).find((tab) => tab.active);
75107
75443
  if (!activeTab) throw new McpToolError("no_active_tab", "No active tab is available.");
@@ -75183,6 +75519,7 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75183
75519
  ...event.defaultPrompt !== void 0 ? { defaultPrompt: event.defaultPrompt } : {},
75184
75520
  ...event.url !== void 0 ? { url: event.url } : {}
75185
75521
  });
75522
+ this.resolveDialogWaiters(tabId);
75186
75523
  });
75187
75524
  this.installNetworkCollection(tabId, client);
75188
75525
  }
@@ -75221,20 +75558,29 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75221
75558
  });
75222
75559
  client.Network.loadingFinished(async (event) => {
75223
75560
  const request = state.byRequestId.get(event.requestId);
75224
- if (!request) return;
75561
+ if (!request) {
75562
+ resolveLoadingDone(state, event.requestId, true);
75563
+ return;
75564
+ }
75225
75565
  const startedAt = state.startedAt.get(event.requestId);
75226
75566
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75227
- if (canReadResponseBody(request)) {
75567
+ if (canReadResponseBody(request) && !state.bodyRead.has(event.requestId)) {
75568
+ state.bodyRead.add(event.requestId);
75228
75569
  const body = await client.Network?.getResponseBody({ requestId: event.requestId }).catch(() => void 0);
75229
75570
  if (body) request.responseBody = body.base64Encoded ? Buffer.from(body.body, "base64").toString("utf8") : body.body;
75230
75571
  }
75572
+ resolveLoadingDone(state, event.requestId, true);
75231
75573
  });
75232
75574
  client.Network.loadingFailed((event) => {
75233
75575
  const request = state.byRequestId.get(event.requestId);
75234
- if (!request) return;
75576
+ if (!request) {
75577
+ resolveLoadingDone(state, event.requestId, false);
75578
+ return;
75579
+ }
75235
75580
  request.failureText = event.errorText ?? "Unknown error";
75236
75581
  const startedAt = state.startedAt.get(event.requestId);
75237
75582
  if (startedAt !== void 0 && event.timestamp !== void 0) request.durationMs = Math.round(event.timestamp * 1e3 - startedAt);
75583
+ resolveLoadingDone(state, event.requestId, false);
75238
75584
  });
75239
75585
  }
75240
75586
  ensureConsoleState(tabId) {
@@ -75256,7 +75602,10 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75256
75602
  state = {
75257
75603
  requests: [],
75258
75604
  byRequestId: /* @__PURE__ */ new Map(),
75259
- startedAt: /* @__PURE__ */ new Map()
75605
+ startedAt: /* @__PURE__ */ new Map(),
75606
+ hydratedPerformanceResources: false,
75607
+ loadingDone: /* @__PURE__ */ new Map(),
75608
+ bodyRead: /* @__PURE__ */ new Set()
75260
75609
  };
75261
75610
  this.pageNetworkStates.set(tabId, state);
75262
75611
  }
@@ -75272,10 +75621,79 @@ var CdpConnectedBrowserSession = class CdpConnectedBrowserSession {
75272
75621
  this.pageNetworkStates.set(tabId, {
75273
75622
  requests: [],
75274
75623
  byRequestId: /* @__PURE__ */ new Map(),
75275
- startedAt: /* @__PURE__ */ new Map()
75624
+ startedAt: /* @__PURE__ */ new Map(),
75625
+ hydratedPerformanceResources: false,
75626
+ loadingDone: /* @__PURE__ */ new Map(),
75627
+ bodyRead: /* @__PURE__ */ new Set()
75276
75628
  });
75277
75629
  this.pageDialogStates.delete(tabId);
75278
75630
  }
75631
+ async hydratePerformanceResourceRequests(tabId) {
75632
+ const state = this.ensureNetworkState(tabId);
75633
+ if (state.hydratedPerformanceResources) return;
75634
+ state.hydratedPerformanceResources = true;
75635
+ const pageClient = await this.getActivePageClient();
75636
+ const contextId = await this.getActiveUtilityContextId(pageClient);
75637
+ const documentRequest = await evaluateCdp(pageClient, String.raw`() => {
75638
+ const navigation = performance.getEntriesByType("navigation")[0];
75639
+ return {
75640
+ url: String(location.href || ""),
75641
+ duration: navigation ? Math.round(Number(navigation.duration || 0)) : undefined
75642
+ };
75643
+ }`, void 0, contextId).catch(() => void 0);
75644
+ if (documentRequest?.url && !Array.from(state.byRequestId.values()).some((request) => request.url === documentRequest.url)) {
75645
+ const requestId = `performance:navigation:${documentRequest.url}`;
75646
+ const request = {
75647
+ index: state.requests.length + 1,
75648
+ requestId,
75649
+ method: "GET",
75650
+ url: documentRequest.url,
75651
+ resourceType: "document",
75652
+ requestHeaders: {},
75653
+ status: 200,
75654
+ statusText: "OK",
75655
+ ...documentRequest.duration !== void 0 ? { durationMs: documentRequest.duration } : {}
75656
+ };
75657
+ state.requests.push(request);
75658
+ state.byRequestId.set(requestId, request);
75659
+ }
75660
+ const resources = await evaluateCdp(pageClient, String.raw`() => performance.getEntriesByType("resource").map((entry) => ({
75661
+ name: String(entry.name || ""),
75662
+ initiatorType: String(entry.initiatorType || "other"),
75663
+ duration: Math.round(Number(entry.duration || 0)),
75664
+ responseStatus: typeof entry.responseStatus === "number" ? entry.responseStatus : undefined
75665
+ }))`, void 0, contextId).catch(() => []);
75666
+ for (const resource of resources) {
75667
+ if (!resource.name || Array.from(state.byRequestId.values()).some((request) => request.url === resource.name)) continue;
75668
+ const status = resource.responseStatus && resource.responseStatus > 0 ? resource.responseStatus : await this.probeResourceStatus(pageClient, contextId, resource.name);
75669
+ const requestId = `performance:${resource.name}`;
75670
+ const request = {
75671
+ index: state.requests.length + 1,
75672
+ requestId,
75673
+ method: "GET",
75674
+ url: resource.name,
75675
+ resourceType: normalizeResourceType(resource.initiatorType),
75676
+ requestHeaders: {},
75677
+ ...status !== void 0 ? {
75678
+ status,
75679
+ statusText: statusTextForStatus(status)
75680
+ } : {},
75681
+ ...resource.duration !== void 0 ? { durationMs: resource.duration } : {}
75682
+ };
75683
+ state.requests.push(request);
75684
+ state.byRequestId.set(requestId, request);
75685
+ }
75686
+ }
75687
+ async probeResourceStatus(pageClient, contextId, url) {
75688
+ return evaluateCdp(pageClient, String.raw`async (url) => {
75689
+ try {
75690
+ const response = await fetch(url, { method: "HEAD", cache: "no-store" });
75691
+ return response.status;
75692
+ } catch {
75693
+ return undefined;
75694
+ }
75695
+ }`, url, contextId).catch(() => void 0);
75696
+ }
75279
75697
  addConsoleMessage(tabId, message) {
75280
75698
  const state = this.ensureConsoleState(tabId);
75281
75699
  if (!shouldIncludeConsoleMessage(message.type)) return;
@@ -75409,6 +75827,45 @@ function canReadResponseBody(request) {
75409
75827
  if (request.failureText || request.status === void 0) return false;
75410
75828
  return request.status !== 204 && request.status !== 304 && !(request.status >= 100 && request.status < 200);
75411
75829
  }
75830
+ function loadingDoneEntry(state, requestId) {
75831
+ let entry = state.loadingDone.get(requestId);
75832
+ if (!entry) {
75833
+ let resolve;
75834
+ let reject;
75835
+ entry = {
75836
+ promise: new Promise((res, rej) => {
75837
+ resolve = res;
75838
+ reject = rej;
75839
+ }),
75840
+ resolve,
75841
+ reject
75842
+ };
75843
+ state.loadingDone.set(requestId, entry);
75844
+ }
75845
+ return entry;
75846
+ }
75847
+ function resolveLoadingDone(state, requestId, success) {
75848
+ const entry = state.loadingDone.get(requestId);
75849
+ if (!entry) return;
75850
+ state.loadingDone.delete(requestId);
75851
+ if (success) entry.resolve();
75852
+ else entry.reject(/* @__PURE__ */ new Error("Request failed before the response body was available."));
75853
+ }
75854
+ async function waitForLoadingDone(state, requestId, timeoutMs) {
75855
+ const entry = loadingDoneEntry(state, requestId);
75856
+ await Promise.race([entry.promise, new Promise((resolve) => setTimeout(resolve, timeoutMs))]);
75857
+ }
75858
+ function statusTextForStatus(status) {
75859
+ if (status === 200) return "OK";
75860
+ if (status === 204) return "No Content";
75861
+ if (status === 304) return "Not Modified";
75862
+ if (status === 400) return "Bad Request";
75863
+ if (status === 401) return "Unauthorized";
75864
+ if (status === 403) return "Forbidden";
75865
+ if (status === 404) return "Not Found";
75866
+ if (status === 500) return "Internal Server Error";
75867
+ return "";
75868
+ }
75412
75869
  function cloneNetworkRequest(request) {
75413
75870
  return {
75414
75871
  ...request,
@@ -75487,9 +75944,11 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75487
75944
  pageConsoleStates = /* @__PURE__ */ new Map();
75488
75945
  pageNetworkStates = /* @__PURE__ */ new Map();
75489
75946
  pageDialogStates = /* @__PURE__ */ new Map();
75947
+ dialogWaiters = /* @__PURE__ */ new Map();
75490
75948
  bidiListeners = /* @__PURE__ */ new Map();
75491
75949
  responseDataCollector;
75492
75950
  activeTabId;
75951
+ ownsSession = false;
75493
75952
  constructor(client) {
75494
75953
  this.client = client;
75495
75954
  }
@@ -75497,12 +75956,14 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75497
75956
  if (args.browser && args.browser !== "firefox") throw new McpToolError("unsupported_protocol_input", "BiDi attach only supports browser \"firefox\" in v1.");
75498
75957
  const parsed = new URL(args.endpoint);
75499
75958
  if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") throw new McpToolError("unsupported_protocol_input", `BiDi endpoint must be a ws(s) URL. Received "${parsed.protocol}".`);
75500
- const session = new BidiConnectedBrowserSession(await getBidiClientFactory()({
75959
+ const client = await getBidiClientFactory()({
75501
75960
  browserName: "firefox",
75502
- webSocketUrl: args.endpoint
75503
- }));
75961
+ webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint, args.sessionId)
75962
+ });
75963
+ const session = new BidiConnectedBrowserSession(client);
75964
+ session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
75504
75965
  await session.initialize();
75505
- if ((await session.refreshTabs()).length === 0) await session.newTab();
75966
+ await session.refreshTabs();
75506
75967
  return session;
75507
75968
  }
75508
75969
  async version() {
@@ -75545,10 +76006,13 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75545
76006
  }
75546
76007
  async snapshot(request = {}) {
75547
76008
  const tabId = await this.getActiveTabId();
75548
- return toBrowserSnapshot(await evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request)), request, {
75549
- console: this.consoleSummary(tabId),
75550
- consoleLink: await this.takeConsoleLink(tabId)
75551
- });
76009
+ return {
76010
+ ...toBrowserSnapshot(await retryUntilReady(() => evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request))), request, {
76011
+ console: this.consoleSummary(tabId),
76012
+ consoleLink: await this.takeConsoleLink(tabId)
76013
+ }),
76014
+ retryable: true
76015
+ };
75552
76016
  }
75553
76017
  async consoleMessages(level = "info", all = false) {
75554
76018
  const activeTabId = await this.getActiveTabId();
@@ -75651,10 +76115,12 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75651
76115
  parameters: { pointerType: "mouse" },
75652
76116
  actions: pointerActions
75653
76117
  });
75654
- await this.client.inputPerformActions({
76118
+ const performPromise = this.client.inputPerformActions({
75655
76119
  context: tabId,
75656
76120
  actions
75657
76121
  });
76122
+ await Promise.race([performPromise, this.waitForDialog(tabId, options.clickHoldMs + 5e3)]);
76123
+ performPromise.catch(() => {});
75658
76124
  await this.client.inputReleaseActions({ context: tabId }).catch(() => {});
75659
76125
  }
75660
76126
  async drag(start, end, options) {
@@ -75747,7 +76213,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75747
76213
  await this.client.networkRemoveDataCollector({ collector: this.responseDataCollector }).catch(() => {});
75748
76214
  this.responseDataCollector = void 0;
75749
76215
  }
75750
- await this.client.sessionEnd({}).catch(() => {});
76216
+ if (this.ownsSession) await this.client.sessionEnd({}).catch(() => {});
75751
76217
  this.client.close();
75752
76218
  }
75753
76219
  async navigate(url) {
@@ -75955,14 +76421,17 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75955
76421
  }
75956
76422
  }
75957
76423
  async handleDialog(accept, promptText) {
75958
- const tabId = await this.getActiveTabId();
76424
+ const tabId = this.dialogTabId();
75959
76425
  if (!this.pageDialogStates.has(tabId)) throw new McpToolError("no_dialog", "No dialog visible.");
75960
76426
  this.pageDialogStates.delete(tabId);
75961
- await this.client.browsingContextHandleUserPrompt({
76427
+ await withBiDiTimeout(this.client.browsingContextHandleUserPrompt({
75962
76428
  context: tabId,
75963
76429
  accept,
75964
76430
  ...promptText !== void 0 ? { userText: promptText } : {}
75965
- });
76431
+ }), 5e3);
76432
+ }
76433
+ async hasDialog() {
76434
+ return this.pageDialogStates.size > 0;
75966
76435
  }
75967
76436
  async networkRequests() {
75968
76437
  const tabId = await this.getActiveTabId();
@@ -75973,6 +76442,15 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75973
76442
  const request = this.ensureNetworkState(tabId).requests[index - 1];
75974
76443
  return request ? cloneNetworkRequest(request) : void 0;
75975
76444
  }
76445
+ async fetchResponseBody(index) {
76446
+ const tabId = await this.getActiveTabId();
76447
+ const request = this.ensureNetworkState(tabId).requests[index - 1];
76448
+ if (!request || !request.requestId) return request?.responseBody;
76449
+ if (request.responseBody !== void 0) return request.responseBody;
76450
+ const body = await this.getResponseBody(request.requestId).catch(() => void 0);
76451
+ if (body !== void 0) request.responseBody = body;
76452
+ return request.responseBody;
76453
+ }
75976
76454
  async runCodeUnsafe(code) {
75977
76455
  return this.evaluate(`async () => {
75978
76456
  const fn = eval(${JSON.stringify(`(${code})`)});
@@ -75988,6 +76466,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75988
76466
  }`);
75989
76467
  }
75990
76468
  async initialize() {
76469
+ this.attachBiDiListeners();
75991
76470
  await this.client.sessionSubscribe({ events: [
75992
76471
  "browsingContext.userPromptOpened",
75993
76472
  "log.entryAdded",
@@ -76001,7 +76480,6 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76001
76480
  maxEncodedDataSize: 1e7
76002
76481
  }).catch(() => void 0);
76003
76482
  this.responseDataCollector = collectorResult?.collector;
76004
- this.attachBiDiListeners();
76005
76483
  }
76006
76484
  attachBiDiListeners() {
76007
76485
  this.attachBiDiListener("log.entryAdded", (payload) => this.handleLogEntry(payload));
@@ -76036,6 +76514,45 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76036
76514
  targetArg(target) {
76037
76515
  return "nodeToken" in target ? { nodeToken: target.nodeToken } : { selector: target.selector };
76038
76516
  }
76517
+ dialogTabId() {
76518
+ if (this.activeTabId && this.pageDialogStates.has(this.activeTabId)) return this.activeTabId;
76519
+ const tabId = this.pageDialogStates.keys().next().value;
76520
+ if (!tabId) throw new McpToolError("no_dialog", "No dialog visible.");
76521
+ return tabId;
76522
+ }
76523
+ waitForDialog(tabId, timeoutMs) {
76524
+ if (this.pageDialogStates.has(tabId)) return Promise.resolve();
76525
+ return new Promise((resolve, reject) => {
76526
+ const waiter = {
76527
+ resolve: () => {
76528
+ if (waiter.timer) clearTimeout(waiter.timer);
76529
+ this.removeDialogWaiter(tabId, waiter);
76530
+ resolve();
76531
+ },
76532
+ reject: (error) => {
76533
+ if (waiter.timer) clearTimeout(waiter.timer);
76534
+ this.removeDialogWaiter(tabId, waiter);
76535
+ reject(error);
76536
+ }
76537
+ };
76538
+ waiter.timer = setTimeout(() => waiter.reject?.(/* @__PURE__ */ new Error("Timed out waiting for dialog.")), timeoutMs);
76539
+ const waiters = this.dialogWaiters.get(tabId) ?? /* @__PURE__ */ new Set();
76540
+ waiters.add(waiter);
76541
+ this.dialogWaiters.set(tabId, waiters);
76542
+ });
76543
+ }
76544
+ resolveDialogWaiters(tabId) {
76545
+ const waiters = this.dialogWaiters.get(tabId);
76546
+ if (!waiters) return;
76547
+ this.dialogWaiters.delete(tabId);
76548
+ for (const waiter of waiters) waiter.resolve();
76549
+ }
76550
+ removeDialogWaiter(tabId, waiter) {
76551
+ const waiters = this.dialogWaiters.get(tabId);
76552
+ if (!waiters) return;
76553
+ waiters.delete(waiter);
76554
+ if (waiters.size === 0) this.dialogWaiters.delete(tabId);
76555
+ }
76039
76556
  async actionPoint(tabId, target) {
76040
76557
  const source = "nodeToken" in target ? ACTION_POINT_EVALUATE_SOURCE : ACTION_POINT_BY_SELECTOR_SOURCE;
76041
76558
  const point = await evaluateBiDi(this.client, tabId, source, this.targetArg(target));
@@ -76092,6 +76609,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76092
76609
  type: event.type ?? "alert",
76093
76610
  ...event.defaultValue !== void 0 ? { defaultPrompt: event.defaultValue } : {}
76094
76611
  });
76612
+ this.resolveDialogWaiters(event.context);
76095
76613
  }
76096
76614
  handleBeforeRequestSent(payload) {
76097
76615
  const event = parseBidiNetworkEvent(payload);
@@ -76194,7 +76712,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76194
76712
  state = {
76195
76713
  requests: [],
76196
76714
  byRequestId: /* @__PURE__ */ new Map(),
76197
- startedAt: /* @__PURE__ */ new Map()
76715
+ startedAt: /* @__PURE__ */ new Map(),
76716
+ hydratedPerformanceResources: false,
76717
+ loadingDone: /* @__PURE__ */ new Map(),
76718
+ bodyRead: /* @__PURE__ */ new Set()
76198
76719
  };
76199
76720
  this.pageNetworkStates.set(tabId, state);
76200
76721
  }
@@ -76210,7 +76731,10 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76210
76731
  this.pageNetworkStates.set(tabId, {
76211
76732
  requests: [],
76212
76733
  byRequestId: /* @__PURE__ */ new Map(),
76213
- startedAt: /* @__PURE__ */ new Map()
76734
+ startedAt: /* @__PURE__ */ new Map(),
76735
+ hydratedPerformanceResources: false,
76736
+ loadingDone: /* @__PURE__ */ new Map(),
76737
+ bodyRead: /* @__PURE__ */ new Set()
76214
76738
  });
76215
76739
  this.pageDialogStates.delete(tabId);
76216
76740
  }
@@ -76248,6 +76772,36 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76248
76772
  return `${path.relative(process.cwd(), state.logFile)}${fromLine === state.logLine ? `#L${fromLine}` : `#L${fromLine}-L${state.logLine}`}`;
76249
76773
  }
76250
76774
  };
76775
+ function normalizeFirefoxBidiEndpoint(endpoint, sessionId) {
76776
+ const url = new URL(endpoint);
76777
+ if (sessionId) {
76778
+ url.pathname = `/session/${sessionId}`;
76779
+ return url.toString();
76780
+ }
76781
+ if (url.pathname === "/" || url.pathname === "") url.pathname = "/session";
76782
+ return url.toString();
76783
+ }
76784
+ async function ensureMcpBiDiSession(client, endpoint, sessionId) {
76785
+ await client.sessionStatus({});
76786
+ if (sessionId || isSessionSpecificFirefoxBidiEndpoint(endpoint)) return false;
76787
+ try {
76788
+ await client.browsingContextGetTree({});
76789
+ return false;
76790
+ } catch (error) {
76791
+ const message = String(error instanceof Error ? error.message : error);
76792
+ if (!message.includes("session does not exist") && !message.includes("invalid session id") && !message.includes("not active")) throw error;
76793
+ }
76794
+ try {
76795
+ await client.sessionNew({ capabilities: { alwaysMatch: { acceptInsecureCerts: true } } });
76796
+ return true;
76797
+ } catch (error) {
76798
+ if (String(error instanceof Error ? error.message : error).includes("Maximum number of active sessions")) throw new Error("Maximum number of active BiDi sessions. Reuse an existing one with sessionId or close the current session first.");
76799
+ throw error;
76800
+ }
76801
+ }
76802
+ function isSessionSpecificFirefoxBidiEndpoint(endpoint) {
76803
+ return /^\/session\/[^/]+$/.test(new URL(endpoint).pathname);
76804
+ }
76251
76805
  async function connectBrowserSession(args) {
76252
76806
  if (args.protocol === "cdp") return CdpConnectedBrowserSession.connect(args);
76253
76807
  return BidiConnectedBrowserSession.connect(args);
@@ -76279,7 +76833,7 @@ var McpRuntime = class {
76279
76833
  constructor(sessionFactory = connectBrowserSession, options = {}) {
76280
76834
  this.sessionFactory = sessionFactory;
76281
76835
  this.snapshotMode = options.snapshotMode ?? "full";
76282
- this.outputDir = configuredOutputDir({ outputDir: options.outputDir });
76836
+ this.outputDir = configuredOutputDir({ ...options.outputDir !== void 0 ? { outputDir: options.outputDir } : {} });
76283
76837
  }
76284
76838
  getOutputDir() {
76285
76839
  return this.outputDir;
@@ -76314,6 +76868,7 @@ var McpRuntime = class {
76314
76868
  this.invalidateSnapshot();
76315
76869
  this.pendingFileUploadTarget = void 0;
76316
76870
  this.tabs = await session.newTab(url);
76871
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76317
76872
  const snapshot = this.tabs.some((tab) => tab.active) ? await this.snapshot() : void 0;
76318
76873
  return snapshot ? {
76319
76874
  tabs: this.tabs,
@@ -76327,6 +76882,7 @@ var McpRuntime = class {
76327
76882
  this.invalidateSnapshot();
76328
76883
  this.pendingFileUploadTarget = void 0;
76329
76884
  this.tabs = await session.selectTab(tab.id);
76885
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76330
76886
  const snapshot = await this.snapshot();
76331
76887
  return {
76332
76888
  tabs: this.tabs,
@@ -76340,6 +76896,7 @@ var McpRuntime = class {
76340
76896
  this.invalidateSnapshot();
76341
76897
  this.pendingFileUploadTarget = void 0;
76342
76898
  this.tabs = await session.closeTab(tab.id);
76899
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76343
76900
  const snapshot = this.tabs.some((candidate) => candidate.active) ? await this.snapshot() : void 0;
76344
76901
  return snapshot ? {
76345
76902
  tabs: this.tabs,
@@ -76348,25 +76905,49 @@ var McpRuntime = class {
76348
76905
  }
76349
76906
  async snapshot(args = {}) {
76350
76907
  const session = this.requireConnected();
76351
- const activeTab = this.requireActiveTab();
76352
76908
  const requestKey = this.snapshotRequestKey(args);
76353
76909
  const request = {
76354
76910
  ...args.boxes !== void 0 ? { boxes: args.boxes } : {},
76355
76911
  ...args.depth !== void 0 ? { depth: args.depth } : {},
76356
76912
  ...args.target ? { target: this.resolveSnapshotTarget(args.target) } : {}
76357
76913
  };
76358
- const snapshot = await session.snapshot(request);
76914
+ const { activeTab, currentActiveTab, snapshot } = await this.captureStableSnapshot(session, request);
76359
76915
  this.snapshotCache = {
76360
- tabId: activeTab.id,
76916
+ tabId: currentActiveTab.id,
76361
76917
  requestKey,
76362
76918
  text: snapshot.text,
76363
76919
  refs: { ...snapshot.refs },
76364
- title: snapshot.title,
76365
- url: snapshot.url,
76920
+ title: currentActiveTab.title || snapshot.title,
76921
+ url: currentActiveTab.url || snapshot.url,
76366
76922
  ...snapshot.console ? { console: { ...snapshot.console } } : {},
76367
76923
  ...snapshot.consoleLink ? { consoleLink: snapshot.consoleLink } : {}
76368
76924
  };
76369
- return snapshot;
76925
+ return {
76926
+ ...snapshot,
76927
+ title: currentActiveTab.title || snapshot.title,
76928
+ url: currentActiveTab.url || snapshot.url
76929
+ };
76930
+ }
76931
+ async captureStableSnapshot(session, request) {
76932
+ let lastAttempt;
76933
+ for (let attempt = 0; attempt < 4; attempt += 1) {
76934
+ this.tabs = await session.listTabs();
76935
+ const activeTab = this.requireActiveTab();
76936
+ const snapshot = await session.snapshot(request);
76937
+ const refreshedTabs = await session.listTabs();
76938
+ this.tabs = refreshedTabs;
76939
+ const currentActiveTab = refreshedTabs.find((tab) => tab.active) ?? refreshedTabs.find((tab) => tab.id === activeTab.id) ?? activeTab;
76940
+ const captured = {
76941
+ activeTab,
76942
+ currentActiveTab,
76943
+ snapshot
76944
+ };
76945
+ lastAttempt = captured;
76946
+ if (!snapshot.retryable || snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76947
+ await delay(150 * (attempt + 1));
76948
+ }
76949
+ if (!lastAttempt) throw new McpToolError("action_failed", "Unable to capture page snapshot.");
76950
+ return lastAttempt;
76370
76951
  }
76371
76952
  async click(target, opts) {
76372
76953
  const session = this.requireConnected();
@@ -76385,6 +76966,7 @@ var McpRuntime = class {
76385
76966
  this.invalidateSnapshot();
76386
76967
  this.pendingFileUploadTarget = opensFileChooser ? resolved : void 0;
76387
76968
  if (this.snapshotMode === "none") return;
76969
+ if (await session.hasDialog()) return;
76388
76970
  return this.snapshot();
76389
76971
  }
76390
76972
  async hover(target) {
@@ -76394,13 +76976,16 @@ var McpRuntime = class {
76394
76976
  this.invalidateSnapshot();
76395
76977
  this.pendingFileUploadTarget = void 0;
76396
76978
  if (this.snapshotMode === "none") return;
76979
+ if (await session.hasDialog()) return;
76397
76980
  return this.snapshot();
76398
76981
  }
76399
76982
  async navigate(url) {
76400
- await this.requireConnected().navigate(normalizeNavigationUrl(url));
76983
+ const session = this.requireConnected();
76984
+ await session.navigate(normalizeNavigationUrl(url));
76401
76985
  this.invalidateSnapshot();
76402
76986
  this.pendingFileUploadTarget = void 0;
76403
76987
  if (this.snapshotMode === "none") return;
76988
+ if (await session.hasDialog()) return;
76404
76989
  return this.snapshot();
76405
76990
  }
76406
76991
  async type(ref, text, opts) {
@@ -76549,6 +77134,9 @@ var McpRuntime = class {
76549
77134
  async networkRequest(index) {
76550
77135
  return this.requireConnected().networkRequest(index);
76551
77136
  }
77137
+ async fetchResponseBody(index) {
77138
+ return this.requireConnected().fetchResponseBody(index);
77139
+ }
76552
77140
  async runCodeUnsafe(code) {
76553
77141
  const result = await this.requireConnected().runCodeUnsafe(code);
76554
77142
  this.invalidateSnapshot();
@@ -76585,6 +77173,10 @@ var McpRuntime = class {
76585
77173
  hasPendingFileUploadTarget() {
76586
77174
  return !!this.pendingFileUploadTarget;
76587
77175
  }
77176
+ async hasDialog() {
77177
+ if (!this.connection) return false;
77178
+ return this.connection.session.hasDialog();
77179
+ }
76588
77180
  async close() {
76589
77181
  this.invalidateSnapshot();
76590
77182
  this.pendingFileUploadTarget = void 0;
@@ -78205,6 +78797,7 @@ async function createRoxyBrowserMcpInMemory(options = {}) {
78205
78797
  await bundle.server.connect(serverTransport);
78206
78798
  return {
78207
78799
  server: bundle.server,
78800
+ runtimeManager: bundle.runtimeManager,
78208
78801
  serverTransport,
78209
78802
  clientTransport,
78210
78803
  close: async () => {