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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14532,8 +14532,10 @@ var WebSocketBidiClient = class {
14532
14532
  eventListeners = /* @__PURE__ */ new Map();
14533
14533
  pendingCommands = /* @__PURE__ */ new Map();
14534
14534
  socket;
14535
+ webSocketUrl;
14535
14536
  constructor(options) {
14536
14537
  this.capabilities = { browserName: options.browserName };
14538
+ this.webSocketUrl = options.webSocketUrl;
14537
14539
  this.socket = new globalThis.WebSocket(options.webSocketUrl);
14538
14540
  this.socket.addEventListener("message", (event) => this.handleMessage(event.data));
14539
14541
  this.socket.addEventListener("close", () => this.handleClose());
@@ -14543,7 +14545,7 @@ var WebSocketBidiClient = class {
14543
14545
  return new Promise((resolve, reject) => {
14544
14546
  this.socket.addEventListener("open", resolve, { once: true });
14545
14547
  this.socket.addEventListener("error", (event) => {
14546
- reject(/* @__PURE__ */ new Error(`Failed to establish a WebDriver BiDi connection: ${String(event)}`));
14548
+ reject(new Error(formatBidiConnectError(event, this.webSocketUrl)));
14547
14549
  }, { once: true });
14548
14550
  });
14549
14551
  }
@@ -14714,6 +14716,28 @@ var WebSocketBidiClient = class {
14714
14716
  this.pendingCommands.clear();
14715
14717
  }
14716
14718
  };
14719
+ function formatBidiConnectError(event, webSocketUrl) {
14720
+ const details = extractBidiConnectErrorDetails(event);
14721
+ return details ? `Failed to establish a WebDriver BiDi connection to ${webSocketUrl}: ${details}` : `Failed to establish a WebDriver BiDi connection to ${webSocketUrl}.`;
14722
+ }
14723
+ function extractBidiConnectErrorDetails(event) {
14724
+ if (event instanceof Error) return event.message;
14725
+ if (!event || typeof event !== "object") return typeof event === "string" ? event : void 0;
14726
+ const candidate = event;
14727
+ const parts = [];
14728
+ if (typeof candidate.message === "string" && candidate.message) parts.push(candidate.message);
14729
+ if (typeof candidate.error === "string" && candidate.error) parts.push(candidate.error);
14730
+ else if (candidate.error instanceof Error && candidate.error.message) parts.push(candidate.error.message);
14731
+ const socketLike = candidate.target ?? candidate.currentTarget;
14732
+ if (socketLike && typeof socketLike === "object") {
14733
+ const socketParts = [];
14734
+ if (typeof socketLike.url === "string" && socketLike.url) socketParts.push(`url=${socketLike.url}`);
14735
+ if (typeof socketLike.readyState === "number") socketParts.push(`readyState=${socketLike.readyState}`);
14736
+ if (socketParts.length > 0) parts.push(`socket(${socketParts.join(", ")})`);
14737
+ }
14738
+ if (parts.length > 0) return parts.join("; ");
14739
+ if (typeof candidate.type === "string" && candidate.type) return `event type=${candidate.type}`;
14740
+ }
14717
14741
  var bidiClientFactory = createBidiClient;
14718
14742
  function getBidiClientFactory() {
14719
14743
  return bidiClientFactory;
@@ -17590,13 +17614,13 @@ function buildFirefoxBidiEndpoint(wsEndpoint, sessionId) {
17590
17614
  if (url.pathname === "/" || url.pathname === "") url.pathname = "/session";
17591
17615
  return url.toString();
17592
17616
  }
17593
- function isSessionSpecificFirefoxBidiEndpoint(wsEndpoint) {
17617
+ function isSessionSpecificFirefoxBidiEndpoint$1(wsEndpoint) {
17594
17618
  const pathname = new URL(wsEndpoint).pathname;
17595
17619
  return /^\/session\/[^/]+$/.test(pathname);
17596
17620
  }
17597
17621
  async function ensureBiDiSession(client, sessionId, wsEndpoint) {
17598
17622
  await client.sessionStatus({});
17599
- if (sessionId || isSessionSpecificFirefoxBidiEndpoint(wsEndpoint)) return false;
17623
+ if (sessionId || isSessionSpecificFirefoxBidiEndpoint$1(wsEndpoint)) return false;
17600
17624
  try {
17601
17625
  await client.browsingContextGetTree({});
17602
17626
  return false;
@@ -73280,11 +73304,6 @@ function formatConnectResult(input) {
73280
73304
  if (input.snapshot) parts.push(formatSnapshot(input.snapshot));
73281
73305
  return parts.join("\n\n");
73282
73306
  }
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
73307
  //#endregion
73289
73308
  //#region src/mcp/backend/response.ts
73290
73309
  var Response$1 = class {
@@ -73294,6 +73313,7 @@ var Response$1 = class {
73294
73313
  results = [];
73295
73314
  errors = [];
73296
73315
  code = [];
73316
+ images = [];
73297
73317
  includeSnapshot = "none";
73298
73318
  fullSnapshot;
73299
73319
  isClose = false;
@@ -73311,6 +73331,12 @@ var Response$1 = class {
73311
73331
  addCode(code) {
73312
73332
  this.code.push(code);
73313
73333
  }
73334
+ addImageResult(data, mimeType) {
73335
+ this.images.push({
73336
+ data,
73337
+ mimeType
73338
+ });
73339
+ }
73314
73340
  setClose() {
73315
73341
  this.isClose = true;
73316
73342
  }
@@ -73364,17 +73390,21 @@ var Response$1 = class {
73364
73390
  content: [{
73365
73391
  type: "text",
73366
73392
  text: sections.join("\n")
73367
- }],
73393
+ }, ...this.images.map((image) => ({
73394
+ type: "image",
73395
+ data: image.data,
73396
+ mimeType: image.mimeType
73397
+ }))],
73368
73398
  ...this.isClose ? { isClose: true } : {},
73369
73399
  ...this.errors.length ? { isError: true } : {}
73370
73400
  };
73371
73401
  }
73372
73402
  };
73373
73403
  //#endregion
73374
- //#region src/mcp/backend/common.ts
73375
- var common_default$1 = [];
73376
- //#endregion
73377
73404
  //#region src/mcp/backend/tool.ts
73405
+ function defineTool$1(tool) {
73406
+ return tool;
73407
+ }
73378
73408
  function defineTabTool(tool) {
73379
73409
  return {
73380
73410
  ...tool,
@@ -73391,6 +73421,144 @@ function missingModalStateMessage(tool) {
73391
73421
  if (tool.clearsModalState === "fileChooser") return "[no_file_chooser] No file chooser visible.";
73392
73422
  return `Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.`;
73393
73423
  }
73424
+ var common_default = [defineTool$1({
73425
+ capability: "core-tabs",
73426
+ schema: {
73427
+ name: "browser_close",
73428
+ title: "Close browser",
73429
+ description: "Close the current browser session.",
73430
+ inputSchema: object({}),
73431
+ type: "action"
73432
+ },
73433
+ handle: async (context, _params, response) => {
73434
+ await context.runtime.close();
73435
+ response.setClose();
73436
+ response.addTextResult("Browser session closed.");
73437
+ }
73438
+ }), defineTool$1({
73439
+ capability: "core",
73440
+ schema: {
73441
+ name: "browser_resize",
73442
+ title: "Resize browser",
73443
+ description: "Resize the active page viewport.",
73444
+ inputSchema: object({
73445
+ width: number().int().positive(),
73446
+ height: number().int().positive()
73447
+ }),
73448
+ type: "action"
73449
+ },
73450
+ handle: async (context, params, response) => {
73451
+ const snapshot = await context.runtime.resize(params.width, params.height);
73452
+ response.setIncludeSnapshot();
73453
+ response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
73454
+ if (!snapshot) response.addTextResult(`Resized viewport to ${params.width}x${params.height}.`);
73455
+ }
73456
+ })];
73457
+ var console_default = [defineTool$1({
73458
+ capability: "core",
73459
+ schema: {
73460
+ name: "browser_console_messages",
73461
+ title: "Get console messages",
73462
+ description: "Returns all console messages",
73463
+ inputSchema: object({
73464
+ level: _enum([
73465
+ "error",
73466
+ "warning",
73467
+ "info",
73468
+ "debug"
73469
+ ]).default("info").describe("Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\"."),
73470
+ all: boolean().optional().describe("Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false."),
73471
+ filename: string().optional().describe("Filename to save the console messages to. If not provided, messages are returned as text.")
73472
+ }),
73473
+ type: "readOnly"
73474
+ },
73475
+ handle: async (context, params, response) => {
73476
+ const messages = await context.runtime.consoleMessages(params.level, params.all);
73477
+ const errors = messages.filter((message) => message.type === "error" || message.type === "assert").length;
73478
+ const warnings = messages.filter((message) => message.type === "warning").length;
73479
+ const text = [
73480
+ `Total messages: ${messages.length} (Errors: ${errors}, Warnings: ${warnings})`,
73481
+ "",
73482
+ ...messages.map((message) => message.formattedText)
73483
+ ].join("\n");
73484
+ if (params.filename) {
73485
+ const resolvedFilename = await context.resolveOutputFile(params.filename);
73486
+ await writeFile(resolvedFilename, text);
73487
+ response.addTextResult(`Saved console messages to "${resolvedFilename}".`);
73488
+ return;
73489
+ }
73490
+ response.addTextResult(text);
73491
+ }
73492
+ })];
73493
+ var connect_default = [defineTool$1({
73494
+ capability: "config",
73495
+ schema: {
73496
+ name: "roxy_browser_connect",
73497
+ title: "Roxy Browser Connect",
73498
+ description: "Attach to an existing browser and seed the active tab snapshot.",
73499
+ inputSchema: object({
73500
+ endpoint: string().min(1),
73501
+ browser: _enum(["chrome", "firefox"]).default("chrome")
73502
+ }),
73503
+ type: "action"
73504
+ },
73505
+ handle: async (context, params, response) => {
73506
+ const protocol = params.browser === "firefox" ? "bidi" : "cdp";
73507
+ const result = await context.runtime.connect({
73508
+ protocol,
73509
+ endpoint: params.endpoint,
73510
+ browser: params.browser === "chrome" ? "chromium" : params.browser
73511
+ });
73512
+ response.addTextResult(formatConnectResult({
73513
+ ...result,
73514
+ browserName: result.browserName === "chromium" ? "chrome" : result.browserName
73515
+ }));
73516
+ }
73517
+ })];
73518
+ var dialogs_default = [defineTool$1({
73519
+ capability: "core",
73520
+ schema: {
73521
+ name: "browser_handle_dialog",
73522
+ title: "Handle a dialog",
73523
+ description: "Handle a dialog",
73524
+ inputSchema: object({
73525
+ accept: boolean().describe("Whether to accept the dialog."),
73526
+ promptText: string().optional().describe("The text of the prompt in case of a prompt dialog.")
73527
+ }),
73528
+ type: "action"
73529
+ },
73530
+ handle: async (context, params, response) => {
73531
+ const snapshot = await context.runtime.handleDialog(params.accept, params.promptText);
73532
+ response.setIncludeSnapshot();
73533
+ if (!snapshot) response.addTextResult(params.accept ? "Accepted dialog." : "Dismissed dialog.");
73534
+ }
73535
+ })];
73536
+ var evaluate_default = [defineTool$1({
73537
+ capability: "core",
73538
+ schema: {
73539
+ name: "browser_evaluate",
73540
+ title: "Evaluate JavaScript",
73541
+ description: "Evaluate JavaScript expression on page or element",
73542
+ inputSchema: object({
73543
+ element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73544
+ target: string().optional().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73545
+ function: string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
73546
+ filename: string().optional().describe("Filename to save the result to. If not provided, result is returned as text.")
73547
+ }),
73548
+ type: "action"
73549
+ },
73550
+ handle: async (context, params, response) => {
73551
+ const result = await context.runtime.evaluate(params.function, params.target);
73552
+ const text = JSON.stringify(result, null, 2) ?? "undefined";
73553
+ if (params.filename) {
73554
+ const resolvedFilename = await context.resolveOutputFile(params.filename);
73555
+ await writeFile(resolvedFilename, text);
73556
+ response.addTextResult(`Saved evaluation result to "${resolvedFilename}".`);
73557
+ return;
73558
+ }
73559
+ response.addTextResult(text);
73560
+ }
73561
+ })];
73394
73562
  var files_default = [defineTabTool({
73395
73563
  capability: "core",
73396
73564
  schema: {
@@ -73602,7 +73770,7 @@ var typeSchema = elementSchema$2.extend({
73602
73770
  submit: boolean().optional().describe("Whether to submit entered text (press Enter after)"),
73603
73771
  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
73772
  });
73605
- var keyboard_default$1 = [defineTabTool({
73773
+ var keyboard_default = [defineTabTool({
73606
73774
  capability: "core-input",
73607
73775
  schema: {
73608
73776
  name: "browser_press_key",
@@ -73648,13 +73816,301 @@ var keyboard_default$1 = [defineTabTool({
73648
73816
  });
73649
73817
  }
73650
73818
  })];
73819
+ var navigate_default = [
73820
+ defineTool$1({
73821
+ capability: "core-navigation",
73822
+ schema: {
73823
+ name: "browser_navigate",
73824
+ title: "Navigate to a URL",
73825
+ description: "Navigate to a URL",
73826
+ inputSchema: object({ url: string().describe("The URL to navigate to") }),
73827
+ type: "action"
73828
+ },
73829
+ handle: async (context, params, response) => {
73830
+ await context.ensureTab();
73831
+ await context.runtime.navigate(params.url);
73832
+ response.setIncludeSnapshot();
73833
+ response.addCode(`await page.goto('${params.url.startsWith("http") ? params.url : params.url.startsWith("localhost") ? `http://${params.url}` : `https://${params.url}`}');`);
73834
+ }
73835
+ }),
73836
+ defineTool$1({
73837
+ capability: "core-navigation",
73838
+ schema: {
73839
+ name: "browser_navigate_back",
73840
+ title: "Go back",
73841
+ description: "Go back to the previous page in the history",
73842
+ inputSchema: object({}),
73843
+ type: "action"
73844
+ },
73845
+ handle: async (context, _params, response) => {
73846
+ await context.runtime.goBack();
73847
+ response.setIncludeSnapshot();
73848
+ response.addCode("await page.goBack();");
73849
+ }
73850
+ }),
73851
+ defineTool$1({
73852
+ capability: "core-navigation",
73853
+ schema: {
73854
+ name: "browser_navigate_forward",
73855
+ title: "Go forward",
73856
+ description: "Go forward to the next page in the history",
73857
+ inputSchema: object({}),
73858
+ type: "action"
73859
+ },
73860
+ handle: async (context, _params, response) => {
73861
+ await context.runtime.goForward();
73862
+ response.setIncludeSnapshot();
73863
+ response.addCode("await page.goForward();");
73864
+ }
73865
+ }),
73866
+ defineTool$1({
73867
+ capability: "core-navigation",
73868
+ schema: {
73869
+ name: "browser_wait_for",
73870
+ title: "Wait for",
73871
+ description: "Wait for text to appear or disappear or a specified time to pass",
73872
+ inputSchema: object({
73873
+ time: number().optional().describe("The time to wait in seconds"),
73874
+ text: string().optional().describe("The text to wait for"),
73875
+ textGone: string().optional().describe("The text to wait for to disappear")
73876
+ }),
73877
+ type: "action"
73878
+ },
73879
+ handle: async (context, params, response) => {
73880
+ if (!params.text && !params.textGone && !params.time) throw new Error("Either time, text or textGone must be provided");
73881
+ const waitSeconds = params.time;
73882
+ if (waitSeconds !== void 0) await new Promise((resolve) => setTimeout(resolve, Math.min(3e4, waitSeconds * 1e3)));
73883
+ if (params.text || params.textGone) await context.runtime.waitFor({
73884
+ ...params.text !== void 0 ? { text: params.text } : {},
73885
+ ...params.textGone !== void 0 ? { textGone: params.textGone } : {}
73886
+ }, 5e3);
73887
+ response.setIncludeSnapshot();
73888
+ }
73889
+ })
73890
+ ];
73891
+ //#endregion
73892
+ //#region src/mcp/backend/network.ts
73893
+ var requestParts = [
73894
+ "request-headers",
73895
+ "request-body",
73896
+ "response-headers",
73897
+ "response-body"
73898
+ ];
73899
+ var networkRequests = defineTool$1({
73900
+ capability: "core",
73901
+ schema: {
73902
+ name: "browser_network_requests",
73903
+ title: "List network requests",
73904
+ description: "Returns a numbered list of network requests since loading the page. Use browser_network_request with the number to get full details.",
73905
+ inputSchema: object({
73906
+ static: boolean().default(false).describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."),
73907
+ filter: string().optional().describe("Only return requests whose URL matches this regexp (e.g. \"/api/.*user\")."),
73908
+ filename: string().optional().describe("Filename to save the network requests to. If not provided, requests are returned as text.")
73909
+ }),
73910
+ type: "readOnly"
73911
+ },
73912
+ handle: async (context, args, response) => {
73913
+ const requests = await context.runtime.networkRequests();
73914
+ const filter = args.filter ? new RegExp(args.filter) : void 0;
73915
+ const lines = [];
73916
+ let hiddenStaticCount = 0;
73917
+ for (const request of requests) {
73918
+ if (!args.static && !isFetch(request) && isSuccessfulResponse(request)) {
73919
+ hiddenStaticCount++;
73920
+ continue;
73921
+ }
73922
+ if (filter && !filter.test(request.url)) continue;
73923
+ lines.push(`${request.index}. ${renderRequestLine(request)}`);
73924
+ }
73925
+ if (hiddenStaticCount > 0) lines.push(`\nNote: ${hiddenStaticCount} static request${hiddenStaticCount === 1 ? "" : "s"} not shown, run with "static" option to see ${hiddenStaticCount === 1 ? "it" : "them"}.`);
73926
+ const text = lines.join("\n");
73927
+ if (args.filename) {
73928
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
73929
+ await writeFile(resolvedFilename, text);
73930
+ response.addTextResult(`Saved network requests to "${resolvedFilename}".`);
73931
+ return;
73932
+ }
73933
+ response.addTextResult(text);
73934
+ }
73935
+ });
73936
+ var networkRequest = defineTool$1({
73937
+ capability: "core",
73938
+ schema: {
73939
+ name: "browser_network_request",
73940
+ title: "Show network request details",
73941
+ description: "Returns full details (headers and body) of a single network request, or a single part if part is set. Use the number from browser_network_requests.",
73942
+ inputSchema: object({
73943
+ index: number().int().min(1).describe("1-based index of the request, as printed by browser_network_requests."),
73944
+ part: _enum(requestParts).optional().describe("Return only this part of the request. Omit to return full details."),
73945
+ filename: string().optional().describe("Filename to save the result to. If not provided, output is returned as text.")
73946
+ }),
73947
+ type: "readOnly"
73948
+ },
73949
+ handle: async (context, args, response) => {
73950
+ const request = await context.runtime.networkRequest(args.index);
73951
+ if (!request) {
73952
+ response.addError(`Request #${args.index} not found. Use browser_network_requests to see available indexes.`);
73953
+ return;
73954
+ }
73955
+ const text = args.part ? renderRequestPart(request, args.part) : renderRequestDetails(request);
73956
+ if (args.filename) {
73957
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
73958
+ await writeFile(resolvedFilename, text);
73959
+ response.addTextResult(`Saved network request to "${resolvedFilename}".`);
73960
+ return;
73961
+ }
73962
+ response.addTextResult(text);
73963
+ }
73964
+ });
73965
+ function isSuccessfulResponse(request) {
73966
+ return !request.failureText && request.status !== void 0 && request.status < 400;
73967
+ }
73968
+ function isFetch(request) {
73969
+ return request.resourceType === "fetch" || request.resourceType === "xhr";
73970
+ }
73971
+ function renderRequestLine(request) {
73972
+ let line = `[${request.method.toUpperCase()}] ${request.url}`;
73973
+ if (request.status !== void 0) line += ` => [${request.status}] ${request.statusText ?? ""}`.trimEnd();
73974
+ else if (request.failureText) line += ` => [FAILED] ${request.failureText}`;
73975
+ return line;
73976
+ }
73977
+ function renderRequestDetails(request) {
73978
+ const lines = [];
73979
+ lines.push(`#${request.index} [${request.method.toUpperCase()}] ${request.url}`);
73980
+ lines.push("");
73981
+ lines.push(" General");
73982
+ if (request.status !== void 0) lines.push(` status: [${request.status}] ${request.statusText ?? ""}`.trimEnd());
73983
+ else if (request.failureText) lines.push(` status: [FAILED] ${request.failureText}`);
73984
+ if (request.durationMs !== void 0) lines.push(` duration: ${request.durationMs}ms`);
73985
+ lines.push(` type: ${request.resourceType}`);
73986
+ if (request.mimeType) lines.push(` mimeType: ${request.mimeType}`);
73987
+ appendHeaders(lines, "Request headers", request.requestHeaders);
73988
+ if (request.responseHeaders) appendHeaders(lines, "Response headers", request.responseHeaders);
73989
+ if (request.requestBody) lines.push("", `Call browser_network_request with part="request-body" to read the request body.`);
73990
+ if (request.responseBody) lines.push("", `Call browser_network_request with part="response-body" to read the response body.`);
73991
+ return lines.join("\n");
73992
+ }
73993
+ function renderRequestPart(request, part) {
73994
+ if (part === "request-headers") return renderHeaders(request.requestHeaders);
73995
+ if (part === "request-body") return request.requestBody ?? "";
73996
+ if (part === "response-headers") return renderHeaders(request.responseHeaders ?? {});
73997
+ return request.responseBody ?? "";
73998
+ }
73999
+ function appendHeaders(lines, title, headers) {
74000
+ const entries = Object.entries(headers);
74001
+ if (!entries.length) return;
74002
+ lines.push("");
74003
+ lines.push(` ${title}`);
74004
+ for (const [key, value] of entries) lines.push(` ${key}: ${value}`);
74005
+ }
74006
+ function renderHeaders(headers) {
74007
+ return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
74008
+ }
74009
+ var network_default = [networkRequests, networkRequest];
74010
+ var runCode_default = [defineTool$1({
74011
+ capability: "devtools",
74012
+ schema: {
74013
+ name: "browser_run_code_unsafe",
74014
+ title: "Run code (unsafe)",
74015
+ description: "Run arbitrary code against the current browser session.",
74016
+ inputSchema: object({ code: string().describe("JavaScript code to run against the browser session.") }),
74017
+ type: "action"
74018
+ },
74019
+ handle: async (context, args, response) => {
74020
+ const result = await context.runtime.runCodeUnsafe(args.code);
74021
+ response.addTextResult(JSON.stringify(result, null, 2) ?? "undefined");
74022
+ }
74023
+ })];
74024
+ var screenshot_default = [defineTool$1({
74025
+ capability: "core",
74026
+ schema: {
74027
+ name: "browser_take_screenshot",
74028
+ title: "Browser Take Screenshot",
74029
+ description: "Capture a full-page screenshot of the active tab as a base64-encoded PNG.",
74030
+ inputSchema: object({
74031
+ element: string().optional().describe("Human-readable description of the area to screenshot"),
74032
+ target: string().optional().describe("Element reference or CSS selector to clip screenshot to; omit for full page"),
74033
+ type: _enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
74034
+ filename: string().optional().describe("File name to save the screenshot to."),
74035
+ 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.")
74036
+ }),
74037
+ type: "readOnly"
74038
+ },
74039
+ handle: async (context, args, response) => {
74040
+ const result = await context.runtime.takeScreenshot({
74041
+ type: args.type,
74042
+ ...args.fullPage !== void 0 ? { fullPage: args.fullPage } : {},
74043
+ ...args.target !== void 0 ? { target: args.target } : {}
74044
+ });
74045
+ const requestedFilename = args.filename?.trim();
74046
+ const resolvedFilename = await context.resolveOutputFile(requestedFilename || `page-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}.${args.type}`);
74047
+ await writeFile(resolvedFilename, Buffer.from(result.data, "base64"));
74048
+ if (requestedFilename) {
74049
+ response.addTextResult(`Screenshot saved to "${resolvedFilename}".`);
74050
+ return;
74051
+ }
74052
+ response.addTextResult(resolvedFilename);
74053
+ response.addImageResult(result.data, result.mimeType);
74054
+ }
74055
+ })];
74056
+ var tabs_default = [defineTool$1({
74057
+ capability: "core-tabs",
74058
+ schema: {
74059
+ name: "browser_tabs",
74060
+ title: "Browser Tabs",
74061
+ description: "List, create, select, and close browser tabs for the current MCP browser session.",
74062
+ inputSchema: object({
74063
+ action: _enum([
74064
+ "list",
74065
+ "new",
74066
+ "select",
74067
+ "close"
74068
+ ]).describe("Operation to perform"),
74069
+ index: number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed."),
74070
+ url: string().optional().describe("URL to navigate to in the new tab, used for new.")
74071
+ }),
74072
+ type: "action"
74073
+ },
74074
+ handle: async (context, params, response) => {
74075
+ switch (params.action) {
74076
+ case "list":
74077
+ await context.ensureTab();
74078
+ break;
74079
+ case "new":
74080
+ await context.runtime.newTab(params.url);
74081
+ if (params.url) {
74082
+ response.setIncludeSnapshot();
74083
+ response.addCode(`await page.goto('${params.url}');`);
74084
+ }
74085
+ break;
74086
+ case "close":
74087
+ await context.runtime.closeTab(params.index ?? 0);
74088
+ break;
74089
+ case "select":
74090
+ if (params.index === void 0) throw new Error("Tab index is required");
74091
+ await context.runtime.selectTab(params.index);
74092
+ break;
74093
+ }
74094
+ const tabs = await context.runtime.listTabs();
74095
+ response.addTextResult(formatTabs(tabs));
74096
+ }
74097
+ })];
73651
74098
  //#endregion
73652
74099
  //#region src/mcp/backend/tools.ts
73653
74100
  var browserTools = [
73654
- ...common_default$1,
74101
+ ...common_default,
74102
+ ...console_default,
74103
+ ...connect_default,
74104
+ ...dialogs_default,
74105
+ ...evaluate_default,
73655
74106
  ...files_default,
73656
- ...keyboard_default$1,
73657
- ...snapshot_default
74107
+ ...keyboard_default,
74108
+ ...navigate_default,
74109
+ ...network_default,
74110
+ ...runCode_default,
74111
+ ...screenshot_default,
74112
+ ...snapshot_default,
74113
+ ...tabs_default
73658
74114
  ];
73659
74115
  //#endregion
73660
74116
  //#region src/mcp/errors.ts
@@ -73683,135 +74139,6 @@ function textResult(text, isError = false) {
73683
74139
  ...isError ? { isError: true } : {}
73684
74140
  };
73685
74141
  }
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));
73753
- }
73754
- if (args.action === "select") {
73755
- const result = await runtime.selectTab(args.index);
73756
- return textResult(formatTabsWithOptionalSnapshot(result.tabs, result.snapshot));
73757
- }
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({
73795
- schema: {
73796
- name: "browser_wait_for",
73797
- title: "Wait for",
73798
- description: "Wait for text to appear or disappear or a specified time to pass",
73799
- inputSchema: object({
73800
- time: number().optional().describe("The time to wait in seconds"),
73801
- text: string().optional().describe("The text to wait for"),
73802
- textGone: string().optional().describe("The text to wait for to disappear")
73803
- })
73804
- },
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)));
73812
- }
73813
- })
73814
- ];
73815
74142
  object({
73816
74143
  element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73817
74144
  target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
@@ -73848,17 +74175,6 @@ object({
73848
74175
  deltaY: number().optional().describe("Vertical scroll delta in pixels (default 0)")
73849
74176
  });
73850
74177
  var mouse_default = [];
73851
- //#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
74178
  object({
73863
74179
  element: string().optional().describe("Human-readable element description used to obtain permission"),
73864
74180
  target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
@@ -73909,254 +74225,9 @@ var form_default = [defineTool({
73909
74225
  return textResult(formatSnapshot(snap));
73910
74226
  }
73911
74227
  })];
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
74018
- var requestParts = [
74019
- "request-headers",
74020
- "request-body",
74021
- "response-headers",
74022
- "response-body"
74023
- ];
74024
- var networkRequests = defineTool({
74025
- schema: {
74026
- name: "browser_network_requests",
74027
- title: "List network requests",
74028
- description: "Returns a numbered list of network requests since loading the page. Use browser_network_request with the number to get full details.",
74029
- inputSchema: object({
74030
- static: boolean().default(false).describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."),
74031
- filter: string().optional().describe("Only return requests whose URL matches this regexp (e.g. \"/api/.*user\")."),
74032
- filename: string().optional().describe("Filename to save the network requests to. If not provided, requests are returned as text.")
74033
- })
74034
- },
74035
- handle: async (args, runtime) => {
74036
- const requests = await runtime.networkRequests();
74037
- const filter = args.filter ? new RegExp(args.filter) : void 0;
74038
- const lines = [];
74039
- let hiddenStaticCount = 0;
74040
- for (const request of requests) {
74041
- if (!args.static && !isFetch(request) && isSuccessfulResponse(request)) {
74042
- hiddenStaticCount++;
74043
- continue;
74044
- }
74045
- if (filter && !filter.test(request.url)) continue;
74046
- lines.push(`${request.index}. ${renderRequestLine(request)}`);
74047
- }
74048
- 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
- const text = lines.join("\n");
74050
- if (args.filename) {
74051
- const resolvedFilename = await resolveOutputFilePath(args.filename, { outputDir: runtime.getOutputDir() });
74052
- await writeFile(resolvedFilename, text);
74053
- return textResult(`Saved network requests to "${resolvedFilename}".`);
74054
- }
74055
- return textResult(text);
74056
- }
74057
- });
74058
- var networkRequest = defineTool({
74059
- schema: {
74060
- name: "browser_network_request",
74061
- title: "Show network request details",
74062
- description: "Returns full details (headers and body) of a single network request, or a single part if part is set. Use the number from browser_network_requests.",
74063
- inputSchema: object({
74064
- index: number().int().min(1).describe("1-based index of the request, as printed by browser_network_requests."),
74065
- part: _enum(requestParts).optional().describe("Return only this part of the request. Omit to return full details."),
74066
- filename: string().optional().describe("Filename to save the result to. If not provided, output is returned as text.")
74067
- })
74068
- },
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);
74073
- if (args.filename) {
74074
- const resolvedFilename = await resolveOutputFilePath(args.filename, { outputDir: runtime.getOutputDir() });
74075
- await writeFile(resolvedFilename, text);
74076
- return textResult(`Saved network request to "${resolvedFilename}".`);
74077
- }
74078
- return textResult(text);
74079
- }
74080
- });
74081
- function isSuccessfulResponse(request) {
74082
- return !request.failureText && request.status !== void 0 && request.status < 400;
74083
- }
74084
- function isFetch(request) {
74085
- return request.resourceType === "fetch" || request.resourceType === "xhr";
74086
- }
74087
- function renderRequestLine(request) {
74088
- let line = `[${request.method.toUpperCase()}] ${request.url}`;
74089
- if (request.status !== void 0) line += ` => [${request.status}] ${request.statusText ?? ""}`.trimEnd();
74090
- else if (request.failureText) line += ` => [FAILED] ${request.failureText}`;
74091
- return line;
74092
- }
74093
- function renderRequestDetails(request) {
74094
- const lines = [];
74095
- lines.push(`#${request.index} [${request.method.toUpperCase()}] ${request.url}`);
74096
- lines.push("");
74097
- lines.push(" General");
74098
- if (request.status !== void 0) lines.push(` status: [${request.status}] ${request.statusText ?? ""}`.trimEnd());
74099
- else if (request.failureText) lines.push(` status: [FAILED] ${request.failureText}`);
74100
- if (request.durationMs !== void 0) lines.push(` duration: ${request.durationMs}ms`);
74101
- lines.push(` type: ${request.resourceType}`);
74102
- if (request.mimeType) lines.push(` mimeType: ${request.mimeType}`);
74103
- appendHeaders(lines, "Request headers", request.requestHeaders);
74104
- 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.`);
74107
- return lines.join("\n");
74108
- }
74109
- function renderRequestPart(request, part) {
74110
- if (part === "request-headers") return renderHeaders(request.requestHeaders);
74111
- if (part === "request-body") return request.requestBody ?? "";
74112
- if (part === "response-headers") return renderHeaders(request.responseHeaders ?? {});
74113
- return request.responseBody ?? "";
74114
- }
74115
- function appendHeaders(lines, title, headers) {
74116
- const entries = Object.entries(headers);
74117
- if (!entries.length) return;
74118
- lines.push("");
74119
- lines.push(` ${title}`);
74120
- for (const [key, value] of entries) lines.push(` ${key}: ${value}`);
74121
- }
74122
- function renderHeaders(headers) {
74123
- return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
74124
- }
74125
- var network_default = [networkRequests, networkRequest];
74126
- var runCode_default = [defineTool({
74127
- schema: {
74128
- 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.",
74131
- 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
- })
74135
- },
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");
74141
- }
74142
- })];
74143
74228
  //#endregion
74144
74229
  //#region src/mcp/tools/index.ts
74145
- var allTools = [
74146
- ...connect_default,
74147
- ...common_default,
74148
- ...tabs_default,
74149
- ...navigate_default,
74150
- ...mouse_default,
74151
- ...keyboard_default,
74152
- ...form_default,
74153
- ...screenshot_default,
74154
- ...console_default,
74155
- ...evaluate_default,
74156
- ...dialog_default,
74157
- ...network_default,
74158
- ...runCode_default
74159
- ];
74230
+ var allTools = [...mouse_default, ...form_default];
74160
74231
  //#endregion
74161
74232
  //#region src/mcp/connectedBrowser.ts
74162
74233
  function delay$1(ms) {
@@ -75490,6 +75561,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75490
75561
  bidiListeners = /* @__PURE__ */ new Map();
75491
75562
  responseDataCollector;
75492
75563
  activeTabId;
75564
+ ownsSession = false;
75493
75565
  constructor(client) {
75494
75566
  this.client = client;
75495
75567
  }
@@ -75497,10 +75569,12 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75497
75569
  if (args.browser && args.browser !== "firefox") throw new McpToolError("unsupported_protocol_input", "BiDi attach only supports browser \"firefox\" in v1.");
75498
75570
  const parsed = new URL(args.endpoint);
75499
75571
  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()({
75572
+ const client = await getBidiClientFactory()({
75501
75573
  browserName: "firefox",
75502
- webSocketUrl: args.endpoint
75503
- }));
75574
+ webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint)
75575
+ });
75576
+ const session = new BidiConnectedBrowserSession(client);
75577
+ session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint);
75504
75578
  await session.initialize();
75505
75579
  if ((await session.refreshTabs()).length === 0) await session.newTab();
75506
75580
  return session;
@@ -75747,7 +75821,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75747
75821
  await this.client.networkRemoveDataCollector({ collector: this.responseDataCollector }).catch(() => {});
75748
75822
  this.responseDataCollector = void 0;
75749
75823
  }
75750
- await this.client.sessionEnd({}).catch(() => {});
75824
+ if (this.ownsSession) await this.client.sessionEnd({}).catch(() => {});
75751
75825
  this.client.close();
75752
75826
  }
75753
75827
  async navigate(url) {
@@ -76248,6 +76322,32 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76248
76322
  return `${path.relative(process.cwd(), state.logFile)}${fromLine === state.logLine ? `#L${fromLine}` : `#L${fromLine}-L${state.logLine}`}`;
76249
76323
  }
76250
76324
  };
76325
+ function normalizeFirefoxBidiEndpoint(endpoint) {
76326
+ const url = new URL(endpoint);
76327
+ if (url.pathname === "/" || url.pathname === "") url.pathname = "/session";
76328
+ return url.toString();
76329
+ }
76330
+ async function ensureMcpBiDiSession(client, endpoint) {
76331
+ await client.sessionStatus({});
76332
+ if (isSessionSpecificFirefoxBidiEndpoint(endpoint)) return false;
76333
+ try {
76334
+ await client.browsingContextGetTree({});
76335
+ return false;
76336
+ } catch (error) {
76337
+ const message = String(error instanceof Error ? error.message : error);
76338
+ if (!message.includes("session does not exist") && !message.includes("invalid session id") && !message.includes("not active")) throw error;
76339
+ }
76340
+ try {
76341
+ await client.sessionNew({ capabilities: { alwaysMatch: { acceptInsecureCerts: true } } });
76342
+ return true;
76343
+ } catch (error) {
76344
+ 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.");
76345
+ throw error;
76346
+ }
76347
+ }
76348
+ function isSessionSpecificFirefoxBidiEndpoint(endpoint) {
76349
+ return /^\/session\/[^/]+$/.test(new URL(endpoint).pathname);
76350
+ }
76251
76351
  async function connectBrowserSession(args) {
76252
76352
  if (args.protocol === "cdp") return CdpConnectedBrowserSession.connect(args);
76253
76353
  return BidiConnectedBrowserSession.connect(args);
@@ -76279,7 +76379,7 @@ var McpRuntime = class {
76279
76379
  constructor(sessionFactory = connectBrowserSession, options = {}) {
76280
76380
  this.sessionFactory = sessionFactory;
76281
76381
  this.snapshotMode = options.snapshotMode ?? "full";
76282
- this.outputDir = configuredOutputDir({ outputDir: options.outputDir });
76382
+ this.outputDir = configuredOutputDir({ ...options.outputDir !== void 0 ? { outputDir: options.outputDir } : {} });
76283
76383
  }
76284
76384
  getOutputDir() {
76285
76385
  return this.outputDir;
@@ -76348,6 +76448,7 @@ var McpRuntime = class {
76348
76448
  }
76349
76449
  async snapshot(args = {}) {
76350
76450
  const session = this.requireConnected();
76451
+ this.tabs = await session.listTabs();
76351
76452
  const activeTab = this.requireActiveTab();
76352
76453
  const requestKey = this.snapshotRequestKey(args);
76353
76454
  const request = {