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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -15981,7 +16005,7 @@ var BidiPageAdapter = class BidiPageAdapter {
15981
16005
  resultOwnership: "none"
15982
16006
  }));
15983
16007
  if (response.type === "exception") throw new Error(response.exceptionDetails.text || "BiDi evaluation failed.");
15984
- return parseSerializedEvaluationResult(extractBiDiValue(response.result));
16008
+ return parseSerializedEvaluationResult(extractBiDiValue$1(response.result));
15985
16009
  }
15986
16010
  async evaluateFunction(expression, arg) {
15987
16011
  const serializedArg = arg === void 0 ? "" : serializeForEvaluation$1(arg);
@@ -17225,11 +17249,11 @@ var BidiElementHandleAdapter = class BidiElementHandleAdapter {
17225
17249
  return this.page.selectOptionReference(this.reference(), values, options);
17226
17250
  }
17227
17251
  };
17228
- function extractBiDiValue(value) {
17229
- if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue(entry));
17252
+ function extractBiDiValue$1(value) {
17253
+ if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue$1(entry));
17230
17254
  if (value.type === "object" && Array.isArray(value.value)) {
17231
17255
  const obj = {};
17232
- for (const [key, val] of value.value) obj[key] = extractBiDiValue(val);
17256
+ for (const [key, val] of value.value) obj[key] = extractBiDiValue$1(val);
17233
17257
  return obj;
17234
17258
  }
17235
17259
  return value.value;
@@ -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;
@@ -17621,9 +17645,9 @@ function buildFirefoxLaunchArgs(options, userDataDir, port) {
17621
17645
  ...options.args ?? []
17622
17646
  ];
17623
17647
  }
17624
- function resolveFirefoxExecutableCandidates(options, platform = currentPlatform$1(), fileExistsFn = fileExists$1) {
17648
+ function resolveFirefoxExecutableCandidates(options, platform = currentPlatform$1(), playwrightFirefoxExecutablePath, fileExistsFn = fileExists$1) {
17625
17649
  if (options.executablePath) return [options.executablePath];
17626
- return filterExistingFirefoxExecutableCandidates(defaultFirefoxExecutableCandidates(platform), platform, fileExistsFn);
17650
+ return filterExistingFirefoxExecutableCandidates([...playwrightFirefoxExecutablePath ? [playwrightFirefoxExecutablePath] : [], ...defaultFirefoxExecutableCandidates(platform)], platform, fileExistsFn);
17627
17651
  }
17628
17652
  function defaultFirefoxExecutableCandidates(platform) {
17629
17653
  const candidates = [];
@@ -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
  }
@@ -73338,16 +73364,21 @@ var Response$1 = class {
73338
73364
  sections.push("### Code", "```js", ...this.code, "```");
73339
73365
  }
73340
73366
  if (this.includeSnapshot === "full") {
73341
- const snapshot = await this.context.runtime.snapshot();
73367
+ const snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot());
73342
73368
  if (sections.length) sections.push("");
73343
73369
  sections.push(formatSnapshot(snapshot));
73344
73370
  }
73345
73371
  if (this.fullSnapshot) {
73346
- const snapshot = await this.context.runtime.snapshot({
73372
+ let snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
73347
73373
  ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73348
73374
  ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73349
73375
  ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73350
- });
73376
+ }));
73377
+ if (!this.fullSnapshot.filename && snapshot.text.trim().length === 0 && snapshot.url && snapshot.url !== "about:blank") snapshot = await reconcileSnapshotWithTabs(this.context, await this.context.runtime.snapshot({
73378
+ ...this.fullSnapshot.target !== void 0 ? { target: this.fullSnapshot.target } : {},
73379
+ ...this.fullSnapshot.depth !== void 0 ? { depth: this.fullSnapshot.depth } : {},
73380
+ ...this.fullSnapshot.boxes !== void 0 ? { boxes: this.fullSnapshot.boxes } : {}
73381
+ }));
73351
73382
  if (this.fullSnapshot.filename) {
73352
73383
  const resolvedFilename = await this.context.resolveOutputFile(this.fullSnapshot.filename);
73353
73384
  await writeFile(resolvedFilename, snapshot.text);
@@ -73364,17 +73395,30 @@ var Response$1 = class {
73364
73395
  content: [{
73365
73396
  type: "text",
73366
73397
  text: sections.join("\n")
73367
- }],
73398
+ }, ...this.images.map((image) => ({
73399
+ type: "image",
73400
+ data: image.data,
73401
+ mimeType: image.mimeType
73402
+ }))],
73368
73403
  ...this.isClose ? { isClose: true } : {},
73369
73404
  ...this.errors.length ? { isError: true } : {}
73370
73405
  };
73371
73406
  }
73372
73407
  };
73373
- //#endregion
73374
- //#region src/mcp/backend/common.ts
73375
- var common_default$1 = [];
73408
+ async function reconcileSnapshotWithTabs(context, snapshot) {
73409
+ const activeTab = (await context.runtime.listTabs()).find((tab) => tab.active);
73410
+ if (!activeTab) return snapshot;
73411
+ return {
73412
+ ...snapshot,
73413
+ title: activeTab.title || snapshot.title,
73414
+ url: activeTab.url || snapshot.url
73415
+ };
73416
+ }
73376
73417
  //#endregion
73377
73418
  //#region src/mcp/backend/tool.ts
73419
+ function defineTool$1(tool) {
73420
+ return tool;
73421
+ }
73378
73422
  function defineTabTool(tool) {
73379
73423
  return {
73380
73424
  ...tool,
@@ -73391,6 +73435,146 @@ function missingModalStateMessage(tool) {
73391
73435
  if (tool.clearsModalState === "fileChooser") return "[no_file_chooser] No file chooser visible.";
73392
73436
  return `Error: The tool "${tool.schema.name}" can only be used when there is related modal state present.`;
73393
73437
  }
73438
+ var common_default = [defineTool$1({
73439
+ capability: "core-tabs",
73440
+ schema: {
73441
+ name: "browser_close",
73442
+ title: "Close browser",
73443
+ description: "Close the current browser session.",
73444
+ inputSchema: object({}),
73445
+ type: "action"
73446
+ },
73447
+ handle: async (context, _params, response) => {
73448
+ await context.runtime.close();
73449
+ response.setClose();
73450
+ response.addTextResult("Browser session closed.");
73451
+ }
73452
+ }), defineTool$1({
73453
+ capability: "core",
73454
+ schema: {
73455
+ name: "browser_resize",
73456
+ title: "Resize browser",
73457
+ description: "Resize the active page viewport.",
73458
+ inputSchema: object({
73459
+ width: number().int().positive(),
73460
+ height: number().int().positive()
73461
+ }),
73462
+ type: "action"
73463
+ },
73464
+ handle: async (context, params, response) => {
73465
+ const snapshot = await context.runtime.resize(params.width, params.height);
73466
+ response.setIncludeSnapshot();
73467
+ response.addCode(`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`);
73468
+ if (!snapshot) response.addTextResult(`Resized viewport to ${params.width}x${params.height}.`);
73469
+ }
73470
+ })];
73471
+ var console_default = [defineTool$1({
73472
+ capability: "core",
73473
+ schema: {
73474
+ name: "browser_console_messages",
73475
+ title: "Get console messages",
73476
+ description: "Returns all console messages",
73477
+ inputSchema: object({
73478
+ level: _enum([
73479
+ "error",
73480
+ "warning",
73481
+ "info",
73482
+ "debug"
73483
+ ]).default("info").describe("Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\"."),
73484
+ all: boolean().optional().describe("Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false."),
73485
+ filename: string().optional().describe("Filename to save the console messages to. If not provided, messages are returned as text.")
73486
+ }),
73487
+ type: "readOnly"
73488
+ },
73489
+ handle: async (context, params, response) => {
73490
+ const messages = await context.runtime.consoleMessages(params.level, params.all);
73491
+ const errors = messages.filter((message) => message.type === "error" || message.type === "assert").length;
73492
+ const warnings = messages.filter((message) => message.type === "warning").length;
73493
+ const text = [
73494
+ `Total messages: ${messages.length} (Errors: ${errors}, Warnings: ${warnings})`,
73495
+ "",
73496
+ ...messages.map((message) => message.formattedText)
73497
+ ].join("\n");
73498
+ if (params.filename) {
73499
+ const resolvedFilename = await context.resolveOutputFile(params.filename);
73500
+ await writeFile(resolvedFilename, text);
73501
+ response.addTextResult(`Saved console messages to "${resolvedFilename}".`);
73502
+ return;
73503
+ }
73504
+ response.addTextResult(text);
73505
+ }
73506
+ })];
73507
+ var connect_default = [defineTool$1({
73508
+ capability: "config",
73509
+ schema: {
73510
+ name: "roxy_browser_connect",
73511
+ title: "Roxy Browser Connect",
73512
+ description: "Attach to an existing browser and seed the active tab snapshot.",
73513
+ inputSchema: object({
73514
+ endpoint: string().min(1),
73515
+ browser: _enum(["chrome", "firefox"]).default("chrome"),
73516
+ sessionId: string().min(1).optional()
73517
+ }),
73518
+ type: "action"
73519
+ },
73520
+ handle: async (context, params, response) => {
73521
+ const protocol = params.browser === "firefox" ? "bidi" : "cdp";
73522
+ const result = await context.runtime.connect({
73523
+ protocol,
73524
+ endpoint: params.endpoint,
73525
+ browser: params.browser === "chrome" ? "chromium" : params.browser,
73526
+ ...params.sessionId ? { sessionId: params.sessionId } : {}
73527
+ });
73528
+ response.addTextResult(formatConnectResult({
73529
+ ...result,
73530
+ browserName: result.browserName === "chromium" ? "chrome" : result.browserName
73531
+ }));
73532
+ }
73533
+ })];
73534
+ var dialogs_default = [defineTool$1({
73535
+ capability: "core",
73536
+ schema: {
73537
+ name: "browser_handle_dialog",
73538
+ title: "Handle a dialog",
73539
+ description: "Handle a dialog",
73540
+ inputSchema: object({
73541
+ accept: boolean().describe("Whether to accept the dialog."),
73542
+ promptText: string().optional().describe("The text of the prompt in case of a prompt dialog.")
73543
+ }),
73544
+ type: "action"
73545
+ },
73546
+ handle: async (context, params, response) => {
73547
+ const snapshot = await context.runtime.handleDialog(params.accept, params.promptText);
73548
+ response.setIncludeSnapshot();
73549
+ if (!snapshot) response.addTextResult(params.accept ? "Accepted dialog." : "Dismissed dialog.");
73550
+ }
73551
+ })];
73552
+ var evaluate_default = [defineTool$1({
73553
+ capability: "core",
73554
+ schema: {
73555
+ name: "browser_evaluate",
73556
+ title: "Evaluate JavaScript",
73557
+ description: "Evaluate JavaScript expression on page or element",
73558
+ inputSchema: object({
73559
+ element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73560
+ target: string().optional().describe("Exact target element reference from the page snapshot, or a unique element selector"),
73561
+ function: string().describe("() => { /* code */ } or (element) => { /* code */ } when element is provided"),
73562
+ filename: string().optional().describe("Filename to save the result to. If not provided, result is returned as text.")
73563
+ }),
73564
+ type: "action"
73565
+ },
73566
+ handle: async (context, params, response) => {
73567
+ const result = await context.runtime.evaluate(params.function, params.target);
73568
+ const text = JSON.stringify(result, null, 2) ?? "undefined";
73569
+ if (params.filename) {
73570
+ const resolvedFilename = await context.resolveOutputFile(params.filename);
73571
+ await writeFile(resolvedFilename, text);
73572
+ response.addTextResult(`Saved evaluation result to "${resolvedFilename}".`);
73573
+ return;
73574
+ }
73575
+ response.addTextResult(text);
73576
+ }
73577
+ })];
73394
73578
  var files_default = [defineTabTool({
73395
73579
  capability: "core",
73396
73580
  schema: {
@@ -73602,7 +73786,7 @@ var typeSchema = elementSchema$2.extend({
73602
73786
  submit: boolean().optional().describe("Whether to submit entered text (press Enter after)"),
73603
73787
  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
73788
  });
73605
- var keyboard_default$1 = [defineTabTool({
73789
+ var keyboard_default = [defineTabTool({
73606
73790
  capability: "core-input",
73607
73791
  schema: {
73608
73792
  name: "browser_press_key",
@@ -73648,13 +73832,301 @@ var keyboard_default$1 = [defineTabTool({
73648
73832
  });
73649
73833
  }
73650
73834
  })];
73835
+ var navigate_default = [
73836
+ defineTool$1({
73837
+ capability: "core-navigation",
73838
+ schema: {
73839
+ name: "browser_navigate",
73840
+ title: "Navigate to a URL",
73841
+ description: "Navigate to a URL",
73842
+ inputSchema: object({ url: string().describe("The URL to navigate to") }),
73843
+ type: "action"
73844
+ },
73845
+ handle: async (context, params, response) => {
73846
+ await context.ensureTab();
73847
+ await context.runtime.navigate(params.url);
73848
+ response.setIncludeSnapshot();
73849
+ response.addCode(`await page.goto('${params.url.startsWith("http") ? params.url : params.url.startsWith("localhost") ? `http://${params.url}` : `https://${params.url}`}');`);
73850
+ }
73851
+ }),
73852
+ defineTool$1({
73853
+ capability: "core-navigation",
73854
+ schema: {
73855
+ name: "browser_navigate_back",
73856
+ title: "Go back",
73857
+ description: "Go back to the previous page in the history",
73858
+ inputSchema: object({}),
73859
+ type: "action"
73860
+ },
73861
+ handle: async (context, _params, response) => {
73862
+ await context.runtime.goBack();
73863
+ response.setIncludeSnapshot();
73864
+ response.addCode("await page.goBack();");
73865
+ }
73866
+ }),
73867
+ defineTool$1({
73868
+ capability: "core-navigation",
73869
+ schema: {
73870
+ name: "browser_navigate_forward",
73871
+ title: "Go forward",
73872
+ description: "Go forward to the next page in the history",
73873
+ inputSchema: object({}),
73874
+ type: "action"
73875
+ },
73876
+ handle: async (context, _params, response) => {
73877
+ await context.runtime.goForward();
73878
+ response.setIncludeSnapshot();
73879
+ response.addCode("await page.goForward();");
73880
+ }
73881
+ }),
73882
+ defineTool$1({
73883
+ capability: "core-navigation",
73884
+ schema: {
73885
+ name: "browser_wait_for",
73886
+ title: "Wait for",
73887
+ description: "Wait for text to appear or disappear or a specified time to pass",
73888
+ inputSchema: object({
73889
+ time: number().optional().describe("The time to wait in seconds"),
73890
+ text: string().optional().describe("The text to wait for"),
73891
+ textGone: string().optional().describe("The text to wait for to disappear")
73892
+ }),
73893
+ type: "action"
73894
+ },
73895
+ handle: async (context, params, response) => {
73896
+ if (!params.text && !params.textGone && !params.time) throw new Error("Either time, text or textGone must be provided");
73897
+ const waitSeconds = params.time;
73898
+ if (waitSeconds !== void 0) await new Promise((resolve) => setTimeout(resolve, Math.min(3e4, waitSeconds * 1e3)));
73899
+ if (params.text || params.textGone) await context.runtime.waitFor({
73900
+ ...params.text !== void 0 ? { text: params.text } : {},
73901
+ ...params.textGone !== void 0 ? { textGone: params.textGone } : {}
73902
+ }, 5e3);
73903
+ response.setIncludeSnapshot();
73904
+ }
73905
+ })
73906
+ ];
73907
+ //#endregion
73908
+ //#region src/mcp/backend/network.ts
73909
+ var requestParts = [
73910
+ "request-headers",
73911
+ "request-body",
73912
+ "response-headers",
73913
+ "response-body"
73914
+ ];
73915
+ var networkRequests = defineTool$1({
73916
+ capability: "core",
73917
+ schema: {
73918
+ name: "browser_network_requests",
73919
+ title: "List network requests",
73920
+ description: "Returns a numbered list of network requests since loading the page. Use browser_network_request with the number to get full details.",
73921
+ inputSchema: object({
73922
+ static: boolean().default(false).describe("Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false."),
73923
+ filter: string().optional().describe("Only return requests whose URL matches this regexp (e.g. \"/api/.*user\")."),
73924
+ filename: string().optional().describe("Filename to save the network requests to. If not provided, requests are returned as text.")
73925
+ }),
73926
+ type: "readOnly"
73927
+ },
73928
+ handle: async (context, args, response) => {
73929
+ const requests = await context.runtime.networkRequests();
73930
+ const filter = args.filter ? new RegExp(args.filter) : void 0;
73931
+ const lines = [];
73932
+ let hiddenStaticCount = 0;
73933
+ for (const request of requests) {
73934
+ if (!args.static && !isFetch(request) && isSuccessfulResponse(request)) {
73935
+ hiddenStaticCount++;
73936
+ continue;
73937
+ }
73938
+ if (filter && !filter.test(request.url)) continue;
73939
+ lines.push(`${request.index}. ${renderRequestLine(request)}`);
73940
+ }
73941
+ if (hiddenStaticCount > 0) lines.push(`\nNote: ${hiddenStaticCount} static request${hiddenStaticCount === 1 ? "" : "s"} not shown, run with "static" option to see ${hiddenStaticCount === 1 ? "it" : "them"}.`);
73942
+ const text = lines.join("\n");
73943
+ if (args.filename) {
73944
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
73945
+ await writeFile(resolvedFilename, text);
73946
+ response.addTextResult(`Saved network requests to "${resolvedFilename}".`);
73947
+ return;
73948
+ }
73949
+ response.addTextResult(text);
73950
+ }
73951
+ });
73952
+ var networkRequest = defineTool$1({
73953
+ capability: "core",
73954
+ schema: {
73955
+ name: "browser_network_request",
73956
+ title: "Show network request details",
73957
+ 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.",
73958
+ inputSchema: object({
73959
+ index: number().int().min(1).describe("1-based index of the request, as printed by browser_network_requests."),
73960
+ part: _enum(requestParts).optional().describe("Return only this part of the request. Omit to return full details."),
73961
+ filename: string().optional().describe("Filename to save the result to. If not provided, output is returned as text.")
73962
+ }),
73963
+ type: "readOnly"
73964
+ },
73965
+ handle: async (context, args, response) => {
73966
+ const request = await context.runtime.networkRequest(args.index);
73967
+ if (!request) {
73968
+ response.addError(`Request #${args.index} not found. Use browser_network_requests to see available indexes.`);
73969
+ return;
73970
+ }
73971
+ const text = args.part ? renderRequestPart(request, args.part) : renderRequestDetails(request);
73972
+ if (args.filename) {
73973
+ const resolvedFilename = await context.resolveOutputFile(args.filename);
73974
+ await writeFile(resolvedFilename, text);
73975
+ response.addTextResult(`Saved network request to "${resolvedFilename}".`);
73976
+ return;
73977
+ }
73978
+ response.addTextResult(text);
73979
+ }
73980
+ });
73981
+ function isSuccessfulResponse(request) {
73982
+ return !request.failureText && request.status !== void 0 && request.status < 400;
73983
+ }
73984
+ function isFetch(request) {
73985
+ return request.resourceType === "fetch" || request.resourceType === "xhr";
73986
+ }
73987
+ function renderRequestLine(request) {
73988
+ let line = `[${request.method.toUpperCase()}] ${request.url}`;
73989
+ if (request.status !== void 0) line += ` => [${request.status}] ${request.statusText ?? ""}`.trimEnd();
73990
+ else if (request.failureText) line += ` => [FAILED] ${request.failureText}`;
73991
+ return line;
73992
+ }
73993
+ function renderRequestDetails(request) {
73994
+ const lines = [];
73995
+ lines.push(`#${request.index} [${request.method.toUpperCase()}] ${request.url}`);
73996
+ lines.push("");
73997
+ lines.push(" General");
73998
+ if (request.status !== void 0) lines.push(` status: [${request.status}] ${request.statusText ?? ""}`.trimEnd());
73999
+ else if (request.failureText) lines.push(` status: [FAILED] ${request.failureText}`);
74000
+ if (request.durationMs !== void 0) lines.push(` duration: ${request.durationMs}ms`);
74001
+ lines.push(` type: ${request.resourceType}`);
74002
+ if (request.mimeType) lines.push(` mimeType: ${request.mimeType}`);
74003
+ appendHeaders(lines, "Request headers", request.requestHeaders);
74004
+ if (request.responseHeaders) appendHeaders(lines, "Response headers", request.responseHeaders);
74005
+ if (request.requestBody) lines.push("", `Call browser_network_request with part="request-body" to read the request body.`);
74006
+ if (request.responseBody) lines.push("", `Call browser_network_request with part="response-body" to read the response body.`);
74007
+ return lines.join("\n");
74008
+ }
74009
+ function renderRequestPart(request, part) {
74010
+ if (part === "request-headers") return renderHeaders(request.requestHeaders);
74011
+ if (part === "request-body") return request.requestBody ?? "";
74012
+ if (part === "response-headers") return renderHeaders(request.responseHeaders ?? {});
74013
+ return request.responseBody ?? "";
74014
+ }
74015
+ function appendHeaders(lines, title, headers) {
74016
+ const entries = Object.entries(headers);
74017
+ if (!entries.length) return;
74018
+ lines.push("");
74019
+ lines.push(` ${title}`);
74020
+ for (const [key, value] of entries) lines.push(` ${key}: ${value}`);
74021
+ }
74022
+ function renderHeaders(headers) {
74023
+ return Object.entries(headers).map(([key, value]) => `${key}: ${value}`).join("\n");
74024
+ }
74025
+ var network_default = [networkRequests, networkRequest];
74026
+ var runCode_default = [defineTool$1({
74027
+ capability: "devtools",
74028
+ schema: {
74029
+ name: "browser_run_code_unsafe",
74030
+ title: "Run code (unsafe)",
74031
+ description: "Run arbitrary code against the current browser session.",
74032
+ inputSchema: object({ code: string().describe("JavaScript code to run against the browser session.") }),
74033
+ type: "action"
74034
+ },
74035
+ handle: async (context, args, response) => {
74036
+ const result = await context.runtime.runCodeUnsafe(args.code);
74037
+ response.addTextResult(JSON.stringify(result, null, 2) ?? "undefined");
74038
+ }
74039
+ })];
74040
+ var screenshot_default = [defineTool$1({
74041
+ capability: "core",
74042
+ schema: {
74043
+ name: "browser_take_screenshot",
74044
+ title: "Browser Take Screenshot",
74045
+ description: "Capture a full-page screenshot of the active tab as a base64-encoded PNG.",
74046
+ inputSchema: object({
74047
+ element: string().optional().describe("Human-readable description of the area to screenshot"),
74048
+ target: string().optional().describe("Element reference or CSS selector to clip screenshot to; omit for full page"),
74049
+ type: _enum(["png", "jpeg"]).default("png").describe("Image format for the screenshot. Default is png."),
74050
+ filename: string().optional().describe("File name to save the screenshot to."),
74051
+ 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.")
74052
+ }),
74053
+ type: "readOnly"
74054
+ },
74055
+ handle: async (context, args, response) => {
74056
+ const result = await context.runtime.takeScreenshot({
74057
+ type: args.type,
74058
+ ...args.fullPage !== void 0 ? { fullPage: args.fullPage } : {},
74059
+ ...args.target !== void 0 ? { target: args.target } : {}
74060
+ });
74061
+ const requestedFilename = args.filename?.trim();
74062
+ const resolvedFilename = await context.resolveOutputFile(requestedFilename || `page-${(/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-")}.${args.type}`);
74063
+ await writeFile(resolvedFilename, Buffer.from(result.data, "base64"));
74064
+ if (requestedFilename) {
74065
+ response.addTextResult(`Screenshot saved to "${resolvedFilename}".`);
74066
+ return;
74067
+ }
74068
+ response.addTextResult(resolvedFilename);
74069
+ response.addImageResult(result.data, result.mimeType);
74070
+ }
74071
+ })];
74072
+ var tabs_default = [defineTool$1({
74073
+ capability: "core-tabs",
74074
+ schema: {
74075
+ name: "browser_tabs",
74076
+ title: "Browser Tabs",
74077
+ description: "List, create, select, and close browser tabs for the current MCP browser session.",
74078
+ inputSchema: object({
74079
+ action: _enum([
74080
+ "list",
74081
+ "new",
74082
+ "select",
74083
+ "close"
74084
+ ]).describe("Operation to perform"),
74085
+ index: number().optional().describe("Tab index, used for close/select. If omitted for close, current tab is closed."),
74086
+ url: string().optional().describe("URL to navigate to in the new tab, used for new.")
74087
+ }),
74088
+ type: "action"
74089
+ },
74090
+ handle: async (context, params, response) => {
74091
+ switch (params.action) {
74092
+ case "list":
74093
+ await context.ensureTab();
74094
+ break;
74095
+ case "new":
74096
+ await context.runtime.newTab(params.url);
74097
+ if (params.url) {
74098
+ response.setIncludeSnapshot();
74099
+ response.addCode(`await page.goto('${params.url}');`);
74100
+ }
74101
+ break;
74102
+ case "close":
74103
+ await context.runtime.closeTab(params.index ?? 0);
74104
+ break;
74105
+ case "select":
74106
+ if (params.index === void 0) throw new Error("Tab index is required");
74107
+ await context.runtime.selectTab(params.index);
74108
+ break;
74109
+ }
74110
+ const tabs = await context.runtime.listTabs();
74111
+ response.addTextResult(formatTabs(tabs));
74112
+ }
74113
+ })];
73651
74114
  //#endregion
73652
74115
  //#region src/mcp/backend/tools.ts
73653
74116
  var browserTools = [
73654
- ...common_default$1,
74117
+ ...common_default,
74118
+ ...console_default,
74119
+ ...connect_default,
74120
+ ...dialogs_default,
74121
+ ...evaluate_default,
73655
74122
  ...files_default,
73656
- ...keyboard_default$1,
73657
- ...snapshot_default
74123
+ ...keyboard_default,
74124
+ ...navigate_default,
74125
+ ...network_default,
74126
+ ...runCode_default,
74127
+ ...screenshot_default,
74128
+ ...snapshot_default,
74129
+ ...tabs_default
73658
74130
  ];
73659
74131
  //#endregion
73660
74132
  //#region src/mcp/errors.ts
@@ -73683,135 +74155,6 @@ function textResult(text, isError = false) {
73683
74155
  ...isError ? { isError: true } : {}
73684
74156
  };
73685
74157
  }
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
74158
  object({
73816
74159
  element: string().optional().describe("Human-readable element description used to obtain permission to interact with the element"),
73817
74160
  target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
@@ -73848,17 +74191,6 @@ object({
73848
74191
  deltaY: number().optional().describe("Vertical scroll delta in pixels (default 0)")
73849
74192
  });
73850
74193
  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
74194
  object({
73863
74195
  element: string().optional().describe("Human-readable element description used to obtain permission"),
73864
74196
  target: string().describe("Exact target element reference from the page snapshot, or a unique element selector")
@@ -73909,254 +74241,9 @@ var form_default = [defineTool({
73909
74241
  return textResult(formatSnapshot(snap));
73910
74242
  }
73911
74243
  })];
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
74244
  //#endregion
74144
74245
  //#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
- ];
74246
+ var allTools = [...mouse_default, ...form_default];
74160
74247
  //#endregion
74161
74248
  //#region src/mcp/connectedBrowser.ts
74162
74249
  function delay$1(ms) {
@@ -74595,13 +74682,13 @@ var CDP_KEY_MAP = {
74595
74682
  async function evaluateBiDi(client, contextId, functionSource, arg) {
74596
74683
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
74597
74684
  const response = await client.scriptEvaluate({
74598
- expression,
74685
+ expression: wrapWithSerializedEvaluationResult(expression),
74599
74686
  target: { context: contextId },
74600
74687
  awaitPromise: true,
74601
74688
  resultOwnership: "none"
74602
74689
  });
74603
74690
  if (response.type === "exception") throw new Error(response.exceptionDetails?.text || "BiDi runtime evaluation failed.");
74604
- return response.result?.value;
74691
+ return parseSerializedEvaluationResult(extractBiDiValue(response.result));
74605
74692
  }
74606
74693
  async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74607
74694
  const expression = arg === void 0 ? `(${functionSource})()` : `(${functionSource})(${JSON.stringify(arg)})`;
@@ -74623,6 +74710,16 @@ async function evaluateBiDiRef(client, contextId, functionSource, arg) {
74623
74710
  ...response.result?.value?.handle !== void 0 ? { handle: response.result.value.handle } : {}
74624
74711
  };
74625
74712
  }
74713
+ function extractBiDiValue(value) {
74714
+ if (!value) return;
74715
+ if (value.type === "array" && Array.isArray(value.value)) return value.value.map((entry) => extractBiDiValue(entry));
74716
+ if (value.type === "object" && Array.isArray(value.value)) {
74717
+ const obj = {};
74718
+ for (const [key, val] of value.value) obj[key] = extractBiDiValue(val);
74719
+ return obj;
74720
+ }
74721
+ return value.value;
74722
+ }
74626
74723
  function toAriaSnapshotPayload(request = {}) {
74627
74724
  return {
74628
74725
  options: normalizeAriaSnapshotOptions({
@@ -75490,6 +75587,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75490
75587
  bidiListeners = /* @__PURE__ */ new Map();
75491
75588
  responseDataCollector;
75492
75589
  activeTabId;
75590
+ ownsSession = false;
75493
75591
  constructor(client) {
75494
75592
  this.client = client;
75495
75593
  }
@@ -75497,10 +75595,12 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75497
75595
  if (args.browser && args.browser !== "firefox") throw new McpToolError("unsupported_protocol_input", "BiDi attach only supports browser \"firefox\" in v1.");
75498
75596
  const parsed = new URL(args.endpoint);
75499
75597
  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()({
75598
+ const client = await getBidiClientFactory()({
75501
75599
  browserName: "firefox",
75502
- webSocketUrl: args.endpoint
75503
- }));
75600
+ webSocketUrl: normalizeFirefoxBidiEndpoint(args.endpoint, args.sessionId)
75601
+ });
75602
+ const session = new BidiConnectedBrowserSession(client);
75603
+ session.ownsSession = await ensureMcpBiDiSession(client, args.endpoint, args.sessionId);
75504
75604
  await session.initialize();
75505
75605
  if ((await session.refreshTabs()).length === 0) await session.newTab();
75506
75606
  return session;
@@ -75545,7 +75645,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75545
75645
  }
75546
75646
  async snapshot(request = {}) {
75547
75647
  const tabId = await this.getActiveTabId();
75548
- return toBrowserSnapshot(await evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request)), request, {
75648
+ return toBrowserSnapshot(await retryUntilReady(() => evaluateBiDi(this.client, tabId, PLAYWRIGHT_ARIA_SNAPSHOT_EVALUATE_SOURCE, toAriaSnapshotPayload(request))), request, {
75549
75649
  console: this.consoleSummary(tabId),
75550
75650
  consoleLink: await this.takeConsoleLink(tabId)
75551
75651
  });
@@ -75747,7 +75847,7 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
75747
75847
  await this.client.networkRemoveDataCollector({ collector: this.responseDataCollector }).catch(() => {});
75748
75848
  this.responseDataCollector = void 0;
75749
75849
  }
75750
- await this.client.sessionEnd({}).catch(() => {});
75850
+ if (this.ownsSession) await this.client.sessionEnd({}).catch(() => {});
75751
75851
  this.client.close();
75752
75852
  }
75753
75853
  async navigate(url) {
@@ -76248,6 +76348,36 @@ var BidiConnectedBrowserSession = class BidiConnectedBrowserSession {
76248
76348
  return `${path.relative(process.cwd(), state.logFile)}${fromLine === state.logLine ? `#L${fromLine}` : `#L${fromLine}-L${state.logLine}`}`;
76249
76349
  }
76250
76350
  };
76351
+ function normalizeFirefoxBidiEndpoint(endpoint, sessionId) {
76352
+ const url = new URL(endpoint);
76353
+ if (sessionId) {
76354
+ url.pathname = `/session/${sessionId}`;
76355
+ return url.toString();
76356
+ }
76357
+ if (url.pathname === "/" || url.pathname === "") url.pathname = "/session";
76358
+ return url.toString();
76359
+ }
76360
+ async function ensureMcpBiDiSession(client, endpoint, sessionId) {
76361
+ await client.sessionStatus({});
76362
+ if (sessionId || isSessionSpecificFirefoxBidiEndpoint(endpoint)) return false;
76363
+ try {
76364
+ await client.browsingContextGetTree({});
76365
+ return false;
76366
+ } catch (error) {
76367
+ const message = String(error instanceof Error ? error.message : error);
76368
+ if (!message.includes("session does not exist") && !message.includes("invalid session id") && !message.includes("not active")) throw error;
76369
+ }
76370
+ try {
76371
+ await client.sessionNew({ capabilities: { alwaysMatch: { acceptInsecureCerts: true } } });
76372
+ return true;
76373
+ } catch (error) {
76374
+ 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.");
76375
+ throw error;
76376
+ }
76377
+ }
76378
+ function isSessionSpecificFirefoxBidiEndpoint(endpoint) {
76379
+ return /^\/session\/[^/]+$/.test(new URL(endpoint).pathname);
76380
+ }
76251
76381
  async function connectBrowserSession(args) {
76252
76382
  if (args.protocol === "cdp") return CdpConnectedBrowserSession.connect(args);
76253
76383
  return BidiConnectedBrowserSession.connect(args);
@@ -76279,7 +76409,7 @@ var McpRuntime = class {
76279
76409
  constructor(sessionFactory = connectBrowserSession, options = {}) {
76280
76410
  this.sessionFactory = sessionFactory;
76281
76411
  this.snapshotMode = options.snapshotMode ?? "full";
76282
- this.outputDir = configuredOutputDir({ outputDir: options.outputDir });
76412
+ this.outputDir = configuredOutputDir({ ...options.outputDir !== void 0 ? { outputDir: options.outputDir } : {} });
76283
76413
  }
76284
76414
  getOutputDir() {
76285
76415
  return this.outputDir;
@@ -76314,6 +76444,7 @@ var McpRuntime = class {
76314
76444
  this.invalidateSnapshot();
76315
76445
  this.pendingFileUploadTarget = void 0;
76316
76446
  this.tabs = await session.newTab(url);
76447
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76317
76448
  const snapshot = this.tabs.some((tab) => tab.active) ? await this.snapshot() : void 0;
76318
76449
  return snapshot ? {
76319
76450
  tabs: this.tabs,
@@ -76327,6 +76458,7 @@ var McpRuntime = class {
76327
76458
  this.invalidateSnapshot();
76328
76459
  this.pendingFileUploadTarget = void 0;
76329
76460
  this.tabs = await session.selectTab(tab.id);
76461
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76330
76462
  const snapshot = await this.snapshot();
76331
76463
  return {
76332
76464
  tabs: this.tabs,
@@ -76340,6 +76472,7 @@ var McpRuntime = class {
76340
76472
  this.invalidateSnapshot();
76341
76473
  this.pendingFileUploadTarget = void 0;
76342
76474
  this.tabs = await session.closeTab(tab.id);
76475
+ if (this.snapshotMode === "none") return { tabs: this.tabs };
76343
76476
  const snapshot = this.tabs.some((candidate) => candidate.active) ? await this.snapshot() : void 0;
76344
76477
  return snapshot ? {
76345
76478
  tabs: this.tabs,
@@ -76348,25 +76481,49 @@ var McpRuntime = class {
76348
76481
  }
76349
76482
  async snapshot(args = {}) {
76350
76483
  const session = this.requireConnected();
76351
- const activeTab = this.requireActiveTab();
76352
76484
  const requestKey = this.snapshotRequestKey(args);
76353
76485
  const request = {
76354
76486
  ...args.boxes !== void 0 ? { boxes: args.boxes } : {},
76355
76487
  ...args.depth !== void 0 ? { depth: args.depth } : {},
76356
76488
  ...args.target ? { target: this.resolveSnapshotTarget(args.target) } : {}
76357
76489
  };
76358
- const snapshot = await session.snapshot(request);
76490
+ const { activeTab, currentActiveTab, snapshot } = await this.captureStableSnapshot(session, request);
76359
76491
  this.snapshotCache = {
76360
- tabId: activeTab.id,
76492
+ tabId: currentActiveTab.id,
76361
76493
  requestKey,
76362
76494
  text: snapshot.text,
76363
76495
  refs: { ...snapshot.refs },
76364
- title: snapshot.title,
76365
- url: snapshot.url,
76496
+ title: currentActiveTab.title || snapshot.title,
76497
+ url: currentActiveTab.url || snapshot.url,
76366
76498
  ...snapshot.console ? { console: { ...snapshot.console } } : {},
76367
76499
  ...snapshot.consoleLink ? { consoleLink: snapshot.consoleLink } : {}
76368
76500
  };
76369
- return snapshot;
76501
+ return {
76502
+ ...snapshot,
76503
+ title: currentActiveTab.title || snapshot.title,
76504
+ url: currentActiveTab.url || snapshot.url
76505
+ };
76506
+ }
76507
+ async captureStableSnapshot(session, request) {
76508
+ let lastAttempt;
76509
+ for (let attempt = 0; attempt < 4; attempt += 1) {
76510
+ this.tabs = await session.listTabs();
76511
+ const activeTab = this.requireActiveTab();
76512
+ const snapshot = await session.snapshot(request);
76513
+ const refreshedTabs = await session.listTabs();
76514
+ this.tabs = refreshedTabs;
76515
+ const currentActiveTab = refreshedTabs.find((tab) => tab.active) ?? refreshedTabs.find((tab) => tab.id === activeTab.id) ?? activeTab;
76516
+ const captured = {
76517
+ activeTab,
76518
+ currentActiveTab,
76519
+ snapshot
76520
+ };
76521
+ lastAttempt = captured;
76522
+ if (snapshot.text.trim().length > 0 || currentActiveTab.url === "about:blank") return captured;
76523
+ await delay(150 * (attempt + 1));
76524
+ }
76525
+ if (!lastAttempt) throw new McpToolError("action_failed", "Unable to capture page snapshot.");
76526
+ return lastAttempt;
76370
76527
  }
76371
76528
  async click(target, opts) {
76372
76529
  const session = this.requireConnected();
@@ -78205,6 +78362,7 @@ async function createRoxyBrowserMcpInMemory(options = {}) {
78205
78362
  await bundle.server.connect(serverTransport);
78206
78363
  return {
78207
78364
  server: bundle.server,
78365
+ runtimeManager: bundle.runtimeManager,
78208
78366
  serverTransport,
78209
78367
  clientTransport,
78210
78368
  close: async () => {