firefox-devtools-mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -12211,6 +12211,30 @@ var init_core = __esm({
12211
12211
  }
12212
12212
  return this.driver;
12213
12213
  }
12214
+ /**
12215
+ * Check if Firefox is still connected and responsive
12216
+ * Returns false if Firefox was closed or connection is broken
12217
+ */
12218
+ async isConnected() {
12219
+ if (!this.driver) {
12220
+ return false;
12221
+ }
12222
+ try {
12223
+ await this.driver.getWindowHandle();
12224
+ return true;
12225
+ } catch (error) {
12226
+ logDebug("Connection check failed: Firefox is not responsive");
12227
+ return false;
12228
+ }
12229
+ }
12230
+ /**
12231
+ * Reset driver state (used when Firefox is detected as closed)
12232
+ */
12233
+ reset() {
12234
+ this.driver = null;
12235
+ this.currentContextId = null;
12236
+ logDebug("Driver state reset");
12237
+ }
12214
12238
  /**
12215
12239
  * Get current browsing context ID
12216
12240
  */
@@ -12986,6 +13010,7 @@ var init_pages = __esm({
12986
13010
  if (index >= 0 && index < handles.length) {
12987
13011
  await this.driver.switchTo().window(handles[index]);
12988
13012
  this.setCurrentContextId(handles[index]);
13013
+ this.cachedSelectedIdx = index;
12989
13014
  }
12990
13015
  }
12991
13016
  /**
@@ -12996,6 +13021,7 @@ var init_pages = __esm({
12996
13021
  const handles = await this.driver.getAllWindowHandles();
12997
13022
  const newIdx = handles.length - 1;
12998
13023
  this.setCurrentContextId(handles[newIdx]);
13024
+ this.cachedSelectedIdx = newIdx;
12999
13025
  await this.driver.get(url);
13000
13026
  return newIdx;
13001
13027
  }
@@ -13300,10 +13326,12 @@ var init_manager = __esm({
13300
13326
  const currentFilePath = fileURLToPath(currentFileUrl);
13301
13327
  const currentDir = dirname(currentFilePath);
13302
13328
  const possiblePaths = [
13303
- // Production: relative to compiled dist/index.js location
13304
- resolve(currentDir, "../../snapshot.injected.global.js"),
13305
- // Alternative: relative to current working directory
13306
- resolve(process.cwd(), "dist/snapshot.injected.global.js")
13329
+ // Production: relative to bundled dist/index.js (same directory)
13330
+ resolve(currentDir, "snapshot.injected.global.js"),
13331
+ // Development: relative to current working directory
13332
+ resolve(process.cwd(), "dist/snapshot.injected.global.js"),
13333
+ // npx: package is in node_modules, try to find it relative to the binary
13334
+ resolve(currentDir, "../snapshot.injected.global.js")
13307
13335
  ];
13308
13336
  const attemptedPaths = [];
13309
13337
  for (const path of possiblePaths) {
@@ -13358,7 +13386,8 @@ ${attemptedPaths.map((p) => ` - ${p}`).join("\n")}`
13358
13386
  root: result.tree,
13359
13387
  snapshotId,
13360
13388
  timestamp: Date.now(),
13361
- truncated: result.truncated || false
13389
+ truncated: result.truncated || false,
13390
+ uidMap: result.uidMap
13362
13391
  };
13363
13392
  const snapshot = {
13364
13393
  text: formatSnapshotTree(result.tree),
@@ -13726,6 +13755,24 @@ var init_firefox = __esm({
13726
13755
  getDriver() {
13727
13756
  return this.core.getDriver();
13728
13757
  }
13758
+ /**
13759
+ * Check if Firefox is still connected and responsive
13760
+ * Returns false if Firefox was closed or connection is broken
13761
+ */
13762
+ async isConnected() {
13763
+ return await this.core.isConnected();
13764
+ }
13765
+ /**
13766
+ * Reset all internal state (used when Firefox is detected as closed)
13767
+ */
13768
+ reset() {
13769
+ this.core.reset();
13770
+ this.consoleEvents = null;
13771
+ this.networkEvents = null;
13772
+ this.dom = null;
13773
+ this.pages = null;
13774
+ this.snapshot = null;
13775
+ }
13729
13776
  // ============================================================================
13730
13777
  // Cleanup
13731
13778
  // ============================================================================
@@ -13737,6 +13784,33 @@ var init_firefox = __esm({
13737
13784
  });
13738
13785
 
13739
13786
  // src/utils/response-helpers.ts
13787
+ function estimateTokens(text) {
13788
+ return Math.ceil(text.length / 4);
13789
+ }
13790
+ function truncateText(text, maxChars, suffix = "\n\n[... truncated - exceeded size limit]") {
13791
+ if (text.length <= maxChars) {
13792
+ return text;
13793
+ }
13794
+ return text.slice(0, maxChars - suffix.length) + suffix;
13795
+ }
13796
+ function truncateHeaders(headers) {
13797
+ if (!headers) {
13798
+ return null;
13799
+ }
13800
+ const result = {};
13801
+ let totalChars = 0;
13802
+ for (const [key, value] of Object.entries(headers)) {
13803
+ const truncatedValue = value.length > TOKEN_LIMITS.MAX_HEADER_VALUE_CHARS ? value.slice(0, TOKEN_LIMITS.MAX_HEADER_VALUE_CHARS) + "...[truncated]" : value;
13804
+ const entrySize = key.length + truncatedValue.length;
13805
+ if (totalChars + entrySize > TOKEN_LIMITS.MAX_HEADERS_TOTAL_CHARS) {
13806
+ result["__truncated__"] = "Headers truncated due to size limit";
13807
+ break;
13808
+ }
13809
+ result[key] = truncatedValue;
13810
+ totalChars += entrySize;
13811
+ }
13812
+ return result;
13813
+ }
13740
13814
  function successResponse(message) {
13741
13815
  return {
13742
13816
  content: [
@@ -13769,9 +13843,26 @@ function jsonResponse(data) {
13769
13843
  ]
13770
13844
  };
13771
13845
  }
13846
+ var TOKEN_LIMITS;
13772
13847
  var init_response_helpers = __esm({
13773
13848
  "src/utils/response-helpers.ts"() {
13774
13849
  "use strict";
13850
+ TOKEN_LIMITS = {
13851
+ /** Maximum characters for a single response (~12.5k tokens at ~4 chars/token) */
13852
+ MAX_RESPONSE_CHARS: 5e4,
13853
+ /** Maximum characters for screenshot base64 data (~10k tokens) */
13854
+ MAX_SCREENSHOT_CHARS: 4e4,
13855
+ /** Maximum characters per console message text */
13856
+ MAX_CONSOLE_MESSAGE_CHARS: 2e3,
13857
+ /** Maximum characters for network header values (per header) */
13858
+ MAX_HEADER_VALUE_CHARS: 500,
13859
+ /** Maximum total characters for all headers combined */
13860
+ MAX_HEADERS_TOTAL_CHARS: 5e3,
13861
+ /** Hard cap on snapshot lines (even if user requests more) */
13862
+ MAX_SNAPSHOT_LINES_CAP: 500,
13863
+ /** Warning threshold - show warning when response exceeds this */
13864
+ WARNING_THRESHOLD_CHARS: 3e4
13865
+ };
13775
13866
  }
13776
13867
  });
13777
13868
 
@@ -14034,6 +14125,10 @@ async function handleListConsoleMessages(args2) {
14034
14125
  if (source) {
14035
14126
  messages = messages.filter((msg) => msg.source?.toLowerCase() === source.toLowerCase());
14036
14127
  }
14128
+ messages = messages.map((msg) => ({
14129
+ ...msg,
14130
+ text: truncateText(msg.text, TOKEN_LIMITS.MAX_CONSOLE_MESSAGE_CHARS, "...[truncated]")
14131
+ }));
14037
14132
  const maxLimit = limit ?? DEFAULT_LIMIT;
14038
14133
  const filteredCount = messages.length;
14039
14134
  const truncated = messages.length > maxLimit;
@@ -14292,8 +14387,8 @@ async function handleListNetworkRequests(args2) {
14292
14387
  resourceType: req.resourceType,
14293
14388
  isXHR: req.isXHR,
14294
14389
  timings: req.timings || null,
14295
- requestHeaders: req.requestHeaders || null,
14296
- responseHeaders: req.responseHeaders || null
14390
+ requestHeaders: truncateHeaders(req.requestHeaders),
14391
+ responseHeaders: truncateHeaders(req.responseHeaders)
14297
14392
  }));
14298
14393
  }
14299
14394
  return jsonResponse(responseData);
@@ -14338,8 +14433,8 @@ async function handleListNetworkRequests(args2) {
14338
14433
  resourceType: req.resourceType,
14339
14434
  isXHR: req.isXHR,
14340
14435
  timings: req.timings || null,
14341
- requestHeaders: req.requestHeaders || null,
14342
- responseHeaders: req.responseHeaders || null
14436
+ requestHeaders: truncateHeaders(req.requestHeaders),
14437
+ responseHeaders: truncateHeaders(req.responseHeaders)
14343
14438
  }));
14344
14439
  return successResponse(
14345
14440
  `Found ${requests.length} requests${hasMore ? ` (showing first ${limit})` : ""}
@@ -14411,8 +14506,8 @@ Please use one of these IDs with the "id" parameter:
14411
14506
  isXHR: request.isXHR ?? false,
14412
14507
  timestamp: request.timestamp ?? null,
14413
14508
  timings: request.timings ?? null,
14414
- requestHeaders: request.requestHeaders ?? null,
14415
- responseHeaders: request.responseHeaders ?? null
14509
+ requestHeaders: truncateHeaders(request.requestHeaders),
14510
+ responseHeaders: truncateHeaders(request.responseHeaders)
14416
14511
  };
14417
14512
  if (format === "json") {
14418
14513
  return jsonResponse(details);
@@ -14518,11 +14613,13 @@ var init_network2 = __esm({
14518
14613
  async function handleTakeSnapshot(args2) {
14519
14614
  try {
14520
14615
  const {
14521
- maxLines = MAX_SNAPSHOT_LINES,
14616
+ maxLines: requestedMaxLines = DEFAULT_SNAPSHOT_LINES,
14522
14617
  includeAttributes = false,
14523
14618
  includeText = true,
14524
14619
  maxDepth
14525
14620
  } = args2 || {};
14621
+ const maxLines = Math.min(Math.max(1, requestedMaxLines), TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP);
14622
+ const wasCapped = requestedMaxLines > TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP;
14526
14623
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
14527
14624
  const firefox3 = await getFirefox2();
14528
14625
  const snapshot = await firefox3.takeSnapshot();
@@ -14539,6 +14636,11 @@ async function handleTakeSnapshot(args2) {
14539
14636
  const truncated = lines.length > maxLines;
14540
14637
  const displayLines = truncated ? lines.slice(0, maxLines) : lines;
14541
14638
  let output = "\u{1F4F8} Snapshot taken\n\n";
14639
+ if (wasCapped) {
14640
+ output += `\u26A0\uFE0F maxLines capped at ${TOKEN_LIMITS.MAX_SNAPSHOT_LINES_CAP} (requested: ${requestedMaxLines}) to prevent token overflow
14641
+
14642
+ `;
14643
+ }
14542
14644
  output += "\u2550\u2550\u2550 HOW TO USE THIS SNAPSHOT \u2550\u2550\u2550\n";
14543
14645
  output += "\u2022 To interact with elements: use click_by_uid, hover_by_uid, or fill_by_uid with the UID\n";
14544
14646
  output += "\u2022 After navigation: always call take_snapshot again (UIDs become stale)\n";
@@ -14608,12 +14710,12 @@ async function handleClearSnapshot(_args) {
14608
14710
  return errorResponse(error);
14609
14711
  }
14610
14712
  }
14611
- var MAX_SNAPSHOT_LINES, takeSnapshotTool, resolveUidToSelectorTool, clearSnapshotTool;
14713
+ var DEFAULT_SNAPSHOT_LINES, takeSnapshotTool, resolveUidToSelectorTool, clearSnapshotTool;
14612
14714
  var init_snapshot2 = __esm({
14613
14715
  "src/tools/snapshot.ts"() {
14614
14716
  "use strict";
14615
14717
  init_response_helpers();
14616
- MAX_SNAPSHOT_LINES = 100;
14718
+ DEFAULT_SNAPSHOT_LINES = 100;
14617
14719
  takeSnapshotTool = {
14618
14720
  name: "take_snapshot",
14619
14721
  description: "Capture a textual page snapshot with stable UIDs for elements. Always take a fresh snapshot after navigation or major DOM changes. TIP: Use the UIDs with click_by_uid / fill_by_uid / hover_by_uid. The output may be truncated for readability.",
@@ -14972,6 +15074,37 @@ var init_input = __esm({
14972
15074
  });
14973
15075
 
14974
15076
  // src/tools/screenshot.ts
15077
+ function buildScreenshotResponse(base64Png, context) {
15078
+ const sizeKB = Math.round(base64Png.length / 1024);
15079
+ const estimatedTokens = estimateTokens(base64Png);
15080
+ if (base64Png.length > TOKEN_LIMITS.MAX_SCREENSHOT_CHARS) {
15081
+ const truncatedData = base64Png.slice(0, TOKEN_LIMITS.MAX_SCREENSHOT_CHARS);
15082
+ return successResponse(
15083
+ `\u{1F4F8} ${context} (${sizeKB}KB)
15084
+
15085
+ \u26A0\uFE0F Screenshot truncated (~${Math.round(estimatedTokens / 1e3)}k tokens exceeds limit)
15086
+ Only first ${Math.round(TOKEN_LIMITS.MAX_SCREENSHOT_CHARS / 1024)}KB shown.
15087
+ TIP: For full screenshots, use a dedicated screenshot tool or save to file.
15088
+
15089
+ Base64 PNG data (truncated):
15090
+ ${truncatedData}
15091
+
15092
+ [...truncated]`
15093
+ );
15094
+ }
15095
+ let warning = "";
15096
+ if (base64Png.length > TOKEN_LIMITS.WARNING_THRESHOLD_CHARS) {
15097
+ warning = `\u26A0\uFE0F Large screenshot (~${Math.round(estimatedTokens / 1e3)}k tokens) - may fill context quickly
15098
+
15099
+ `;
15100
+ }
15101
+ return successResponse(
15102
+ `\u{1F4F8} ${context} (${sizeKB}KB)
15103
+
15104
+ ` + warning + `Base64 PNG data:
15105
+ ${base64Png}`
15106
+ );
15107
+ }
14975
15108
  async function handleScreenshotPage(_args) {
14976
15109
  try {
14977
15110
  const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
@@ -14980,12 +15113,7 @@ async function handleScreenshotPage(_args) {
14980
15113
  if (!base64Png || typeof base64Png !== "string") {
14981
15114
  throw new Error("Failed to capture screenshot: invalid data returned");
14982
15115
  }
14983
- return successResponse(
14984
- `\u{1F4F8} Page screenshot captured (${Math.round(base64Png.length / 1024)}KB)
14985
-
14986
- Base64 PNG data:
14987
- ${base64Png}`
14988
- );
15116
+ return buildScreenshotResponse(base64Png, "Page screenshot captured");
14989
15117
  } catch (error) {
14990
15118
  return errorResponse(
14991
15119
  new Error(
@@ -15009,12 +15137,7 @@ async function handleScreenshotByUid(args2) {
15009
15137
  if (!base64Png || typeof base64Png !== "string") {
15010
15138
  throw new Error("Failed to capture screenshot: invalid data returned");
15011
15139
  }
15012
- return successResponse(
15013
- `\u{1F4F8} Element screenshot captured for UID "${uid}" (${Math.round(base64Png.length / 1024)}KB)
15014
-
15015
- Base64 PNG data:
15016
- ${base64Png}`
15017
- );
15140
+ return buildScreenshotResponse(base64Png, `Element screenshot captured for UID "${uid}"`);
15018
15141
  } catch (error) {
15019
15142
  const errorMsg = error.message;
15020
15143
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID") || errorMsg.includes("not found")) {
@@ -15238,6 +15361,33 @@ var init_tools = __esm({
15238
15361
  }
15239
15362
  });
15240
15363
 
15364
+ // src/utils/errors.ts
15365
+ function isDisconnectionError(error) {
15366
+ if (error instanceof FirefoxDisconnectedError) {
15367
+ return true;
15368
+ }
15369
+ if (error instanceof Error) {
15370
+ const message = error.message.toLowerCase();
15371
+ return message.includes("session deleted") || message.includes("session not created") || message.includes("no such window") || message.includes("no such session") || message.includes("target window already closed") || message.includes("unable to connect") || message.includes("connection refused") || message.includes("not connected") || message.includes("driver not connected") || message.includes("invalid session id") || message.includes("browsing context has been discarded");
15372
+ }
15373
+ return false;
15374
+ }
15375
+ var FirefoxDisconnectedError;
15376
+ var init_errors2 = __esm({
15377
+ "src/utils/errors.ts"() {
15378
+ "use strict";
15379
+ FirefoxDisconnectedError = class extends Error {
15380
+ constructor(reason) {
15381
+ const baseMessage = "Firefox browser is not connected";
15382
+ const instruction = "The Firefox browser window was closed by the user. To continue browser automation, ask the user to restart the firefox-devtools-mcp server (they need to restart Claude Code or the MCP connection). This will launch a new Firefox instance.";
15383
+ const fullMessage = reason ? `${baseMessage}: ${reason}. ${instruction}` : `${baseMessage}. ${instruction}`;
15384
+ super(fullMessage);
15385
+ this.name = "FirefoxDisconnectedError";
15386
+ }
15387
+ };
15388
+ }
15389
+ });
15390
+
15241
15391
  // node_modules/dotenv/package.json
15242
15392
  var require_package = __commonJS({
15243
15393
  "node_modules/dotenv/package.json"(exports, module) {
@@ -15640,28 +15790,45 @@ var require_main = __commonJS({
15640
15790
  var index_exports = {};
15641
15791
  __export(index_exports, {
15642
15792
  FirefoxDevTools: () => FirefoxClient,
15793
+ FirefoxDisconnectedError: () => FirefoxDisconnectedError,
15643
15794
  args: () => args,
15644
- getFirefox: () => getFirefox
15795
+ getFirefox: () => getFirefox,
15796
+ isDisconnectionError: () => isDisconnectionError,
15797
+ resetFirefox: () => resetFirefox
15645
15798
  });
15646
15799
  import { version } from "process";
15647
15800
  import { fileURLToPath as fileURLToPath2 } from "url";
15648
15801
  import { resolve as resolve2 } from "path";
15649
15802
  import { realpathSync } from "fs";
15650
- async function getFirefox() {
15651
- if (!firefox2) {
15652
- log("Initializing Firefox DevTools connection...");
15653
- const options = {
15654
- firefoxPath: args.firefoxPath ?? void 0,
15655
- headless: args.headless,
15656
- profilePath: args.profilePath ?? void 0,
15657
- viewport: args.viewport ?? void 0,
15658
- args: args.firefoxArg ?? void 0,
15659
- startUrl: args.startUrl ?? void 0
15660
- };
15661
- firefox2 = new FirefoxClient(options);
15662
- await firefox2.connect();
15663
- log("Firefox DevTools connection established");
15803
+ function resetFirefox() {
15804
+ if (firefox2) {
15805
+ firefox2.reset();
15806
+ firefox2 = null;
15664
15807
  }
15808
+ log("Firefox instance reset - will reconnect on next tool call");
15809
+ }
15810
+ async function getFirefox() {
15811
+ if (firefox2) {
15812
+ const isConnected = await firefox2.isConnected();
15813
+ if (!isConnected) {
15814
+ log("Firefox connection lost - browser was closed or disconnected");
15815
+ resetFirefox();
15816
+ throw new FirefoxDisconnectedError("Browser was closed");
15817
+ }
15818
+ return firefox2;
15819
+ }
15820
+ log("Initializing Firefox DevTools connection...");
15821
+ const options = {
15822
+ firefoxPath: args.firefoxPath ?? void 0,
15823
+ headless: args.headless,
15824
+ profilePath: args.profilePath ?? void 0,
15825
+ viewport: args.viewport ?? void 0,
15826
+ args: args.firefoxArg ?? void 0,
15827
+ startUrl: args.startUrl ?? void 0
15828
+ };
15829
+ firefox2 = new FirefoxClient(options);
15830
+ await firefox2.connect();
15831
+ log("Firefox DevTools connection established");
15665
15832
  return firefox2;
15666
15833
  }
15667
15834
  async function main() {
@@ -15729,7 +15896,9 @@ var init_index = __esm({
15729
15896
  init_cli();
15730
15897
  init_firefox();
15731
15898
  init_tools();
15899
+ init_errors2();
15732
15900
  init_firefox();
15901
+ init_errors2();
15733
15902
  if (process.env.NODE_ENV !== "production") {
15734
15903
  try {
15735
15904
  const { config } = await Promise.resolve().then(() => __toESM(require_main(), 1));
@@ -15829,8 +15998,11 @@ var init_index = __esm({
15829
15998
  await init_index();
15830
15999
  export {
15831
16000
  FirefoxClient as FirefoxDevTools,
16001
+ FirefoxDisconnectedError,
15832
16002
  args,
15833
- getFirefox
16003
+ getFirefox,
16004
+ isDisconnectionError,
16005
+ resetFirefox
15834
16006
  };
15835
16007
  /*! Bundled license information:
15836
16008
 
@@ -1 +1 @@
1
- "use strict";var __SnapshotInjected=(()=>{var h=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var q=(t,r)=>{for(var e in r)h(t,e,{get:r[e],enumerable:!0})},z=(t,r,e,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of V(r))!U.call(t,o)&&o!==e&&h(t,o,{get:()=>r[o],enumerable:!(i=W(r,o))||i.enumerable});return t};var B=t=>z(h({},"__esModule",{value:!0}),t);var rt={};q(rt,{createSnapshot:()=>P});var v=["a","button","input","select","textarea","img","video","audio","iframe"],J=["nav","main","section","article","header","footer"],K=["div","span","p","li","ul","ol"];function L(t){if(!t||t.nodeType!==Node.ELEMENT_NODE)return!1;try{let e=window.getComputedStyle(t);if(e.display==="none"||e.visibility==="hidden"||e.opacity==="0")return!1}catch{return!1}let r=t.tagName.toLowerCase();if(v.indexOf(r)!==-1||t.hasAttribute("role")||t.hasAttribute("aria-label")||/^h[1-6]$/.test(r)||J.indexOf(r)!==-1)return!0;if(K.indexOf(r)!==-1){let e=(t.textContent||"").trim();if(e.length>0&&e.length<500||t.id||t.className)return!0}return!1}function M(t){if(t.tabIndex>=0)return!0;let e=t.tagName.toLowerCase();return["a","button","input","select","textarea"].indexOf(e)!==-1}function O(t){let r=t.tagName.toLowerCase();if(v.indexOf(r)!==-1)return!0;let e=t.getAttribute("role");return!!(e&&["button","link","menuitem","tab"].indexOf(e)!==-1||t.hasAttribute("onclick"))}var Q=100;function k(t){if(t.hasAttribute("aria-label"))return t.getAttribute("aria-label")||void 0;let e=t.id;if(e){let o=document.querySelector(`label[for="${e}"]`);if(o?.textContent)return o.textContent.trim()}if(t.hasAttribute("placeholder"))return t.getAttribute("placeholder")||void 0;if(t.hasAttribute("title"))return t.getAttribute("title")||void 0;if(t.hasAttribute("alt"))return t.getAttribute("alt")||void 0;let i=t.tagName.toLowerCase();if(["button","a","h1","h2","h3","h4","h5","h6"].indexOf(i)!==-1)return g(t)}function g(t){let r="";for(let i=0;i<t.childNodes.length;i++){let o=t.childNodes[i];o&&o.nodeType===Node.TEXT_NODE&&(r+=o.textContent||"")}let e=r.trim();if(e)return e.substring(0,Q)}function $(t){let r={},e=!1,i=["disabled","hidden","selected","expanded"];for(let a of i){let n=t.getAttribute(`aria-${a}`);n!==null&&(r[a]=n==="true",e=!0)}let o=["checked","pressed"];for(let a of o){let n=t.getAttribute(`aria-${a}`);n!==null&&(n==="mixed"?r[a]="mixed":r[a]=n==="true",e=!0)}let l=["autocomplete","haspopup","invalid","label","labelledby","describedby","controls"];for(let a of l){let n=t.getAttribute(`aria-${a}`);n&&(r[a]=n,e=!0)}let s=t.getAttribute("aria-level");if(s){let a=parseInt(s,10);isNaN(a)||(r.level=a,e=!0)}return e?r:void 0}function I(t){let r={};try{let e=window.getComputedStyle(t);r.visible=e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"}catch{r.visible=!1}return r.accessible=r.visible&&!t.getAttribute("aria-hidden"),r.focusable=M(t),r.interactive=O(t),r}var Y=["id","data-testid","data-test-id"];function R(t){let r=[],e=t;for(;e&&e.nodeType===Node.ELEMENT_NODE;){let i=e.nodeName.toLowerCase(),o=!1;for(let n of Y){let u=e.getAttribute(n);if(u){n==="id"?i+="#"+CSS.escape(u):i+=`[${n}="${X(u)}"]`,r.unshift(i),o=!0;break}}if(o)break;let l=e.getAttribute("aria-label"),s=e.getAttribute("role");if(l&&s){i+=`[role="${s}"][aria-label="${X(l)}"]`,r.unshift(i),e=e.parentElement;continue}let a=e.parentElement?.children;if(a&&a.length>1){let n=1;for(let u=0;u<a.length;u++){let d=a[u];if(d){if(d===e)break;d.nodeName===e.nodeName&&n++}}(n>1||a.length>1&&a[0]!==e)&&(i+=`:nth-of-type(${n})`)}if(r.unshift(tt(i)),e=e.parentElement,e&&e.nodeName.toLowerCase()==="body"){r.unshift("body");break}}return r.join(" > ")}function G(t){let r=t.id;if(r)return`//*[@id="${Z(r)}"]`;let e=[],i=t;for(;i&&i.nodeType===Node.ELEMENT_NODE;){let o=i.nodeName.toLowerCase(),l=1,s=i.previousElementSibling;for(;s;)s.nodeName.toLowerCase()===o&&l++,s=s.previousElementSibling;let a=i.parentElement,n=!1;a&&(n=Array.from(a.children).filter(E=>E.nodeName.toLowerCase()===o).length>1);let u=n?`${o}[${l}]`:o;if(e.unshift(u),i=i.parentElement,i&&i.nodeName.toLowerCase()==="html"){e.unshift("html");break}}return"/"+e.join("/")}function X(t){return t.replace(/"/g,'\\"').substring(0,64)}function Z(t){return t.indexOf('"')===-1||t.indexOf("'")===-1?t:`concat(${t.split('"').map((e,i,o)=>i===o.length-1?e?`"${e}"`:"":e?`"${e}",'"'`:`"'"`).filter(e=>e).join(",")})`}function tt(t){return t.length<=64?t:t.substring(0,64)}var et=10,H=1e3;function D(t,r,e=!0){let i=0,o=[],l=!1;function s(n,u){if(u>et||i>=H)return l=!0,null;let d=n.tagName.toLowerCase();if(!(d==="body"||d==="html")&&!L(n))return null;let A=`${r}_${i++}`,j=R(n),F=G(n);o.push({uid:A,css:j,xpath:F});let b=n,N=n.getAttribute("role"),T=k(n),y=g(n),x=b.value,C=b.href,S=b.src,w=$(n),_=I(n),c={uid:A,tag:d,...N&&{role:N},...T&&{name:T},...x&&{value:x},...C&&{href:C},...S&&{src:S},...y&&{text:y},...w&&{aria:w},..._&&{computed:_},children:[]};if(d==="iframe"&&e){try{let f=n,p=f.contentDocument||f.contentWindow?.document;if(p?.body){let m=s(p.body,u+1);m&&(m.isIframe=!0,m.frameSrc=f.src,c.children.push(m))}else c.isIframe=!0,c.frameSrc=f.src,c.crossOrigin=!0}catch{c.isIframe=!0,c.frameSrc=n.src,c.crossOrigin=!0}return c}for(let f=0;f<n.children.length;f++){if(i>=H){l=!0;break}let p=n.children[f];if(!p)continue;let m=s(p,u+1);m&&c.children.push(m)}return c}return{tree:s(t,0),uidMap:o,truncated:l}}function P(t){try{let r=D(document.body,t,!0);if(!r.tree)throw new Error("Failed to generate tree");return r}catch{return{tree:null,uidMap:[],truncated:!1}}}typeof window<"u"&&(window.__createSnapshot=P);return B(rt);})();
1
+ "use strict";var __SnapshotInjected=(()=>{var h=Object.defineProperty;var W=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var q=(t,r)=>{for(var e in r)h(t,e,{get:r[e],enumerable:!0})},z=(t,r,e,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of V(r))!U.call(t,o)&&o!==e&&h(t,o,{get:()=>r[o],enumerable:!(i=W(r,o))||i.enumerable});return t};var B=t=>z(h({},"__esModule",{value:!0}),t);var rt={};q(rt,{createSnapshot:()=>P});var v=["a","button","input","select","textarea","img","video","audio","iframe"],J=["nav","main","section","article","header","footer","form"],K=["div","span","p","li","ul","ol"];function L(t){if(!t||t.nodeType!==Node.ELEMENT_NODE)return!1;try{let e=window.getComputedStyle(t);if(e.display==="none"||e.visibility==="hidden"||e.opacity==="0")return!1}catch{return!1}let r=t.tagName.toLowerCase();if(v.indexOf(r)!==-1||t.hasAttribute("role")||t.hasAttribute("aria-label")||/^h[1-6]$/.test(r)||J.indexOf(r)!==-1)return!0;if(K.indexOf(r)!==-1){let e=(t.textContent||"").trim();if(e.length>0&&e.length<500||t.id||t.className)return!0}return!1}function M(t){if(t.tabIndex>=0)return!0;let e=t.tagName.toLowerCase();return["a","button","input","select","textarea"].indexOf(e)!==-1}function O(t){let r=t.tagName.toLowerCase();if(v.indexOf(r)!==-1)return!0;let e=t.getAttribute("role");return!!(e&&["button","link","menuitem","tab"].indexOf(e)!==-1||t.hasAttribute("onclick"))}var Q=100;function k(t){if(t.hasAttribute("aria-label"))return t.getAttribute("aria-label")||void 0;let e=t.id;if(e){let o=document.querySelector(`label[for="${e}"]`);if(o?.textContent)return o.textContent.trim()}if(t.hasAttribute("placeholder"))return t.getAttribute("placeholder")||void 0;if(t.hasAttribute("title"))return t.getAttribute("title")||void 0;if(t.hasAttribute("alt"))return t.getAttribute("alt")||void 0;let i=t.tagName.toLowerCase();if(["button","a","h1","h2","h3","h4","h5","h6"].indexOf(i)!==-1)return g(t)}function g(t){let r="";for(let i=0;i<t.childNodes.length;i++){let o=t.childNodes[i];o&&o.nodeType===Node.TEXT_NODE&&(r+=o.textContent||"")}let e=r.trim();if(e)return e.substring(0,Q)}function $(t){let r={},e=!1,i=["disabled","hidden","selected","expanded"];for(let a of i){let n=t.getAttribute(`aria-${a}`);n!==null&&(r[a]=n==="true",e=!0)}let o=["checked","pressed"];for(let a of o){let n=t.getAttribute(`aria-${a}`);n!==null&&(n==="mixed"?r[a]="mixed":r[a]=n==="true",e=!0)}let l=["autocomplete","haspopup","invalid","label","labelledby","describedby","controls"];for(let a of l){let n=t.getAttribute(`aria-${a}`);n&&(r[a]=n,e=!0)}let s=t.getAttribute("aria-level");if(s){let a=parseInt(s,10);isNaN(a)||(r.level=a,e=!0)}return e?r:void 0}function I(t){let r={};try{let e=window.getComputedStyle(t);r.visible=e.display!=="none"&&e.visibility!=="hidden"&&e.opacity!=="0"}catch{r.visible=!1}return r.accessible=r.visible&&!t.getAttribute("aria-hidden"),r.focusable=M(t),r.interactive=O(t),r}var Y=["id","data-testid","data-test-id"];function R(t){let r=[],e=t;for(;e&&e.nodeType===Node.ELEMENT_NODE;){let i=e.nodeName.toLowerCase(),o=!1;for(let n of Y){let u=e.getAttribute(n);if(u){n==="id"?i+="#"+CSS.escape(u):i+=`[${n}="${X(u)}"]`,r.unshift(i),o=!0;break}}if(o)break;let l=e.getAttribute("aria-label"),s=e.getAttribute("role");if(l&&s){i+=`[role="${s}"][aria-label="${X(l)}"]`,r.unshift(i),e=e.parentElement;continue}let a=e.parentElement?.children;if(a&&a.length>1){let n=1;for(let u=0;u<a.length;u++){let d=a[u];if(d){if(d===e)break;d.nodeName===e.nodeName&&n++}}(n>1||a.length>1&&a[0]!==e)&&(i+=`:nth-of-type(${n})`)}if(r.unshift(tt(i)),e=e.parentElement,e&&e.nodeName.toLowerCase()==="body"){r.unshift("body");break}}return r.join(" > ")}function G(t){let r=t.id;if(r)return`//*[@id="${Z(r)}"]`;let e=[],i=t;for(;i&&i.nodeType===Node.ELEMENT_NODE;){let o=i.nodeName.toLowerCase(),l=1,s=i.previousElementSibling;for(;s;)s.nodeName.toLowerCase()===o&&l++,s=s.previousElementSibling;let a=i.parentElement,n=!1;a&&(n=Array.from(a.children).filter(E=>E.nodeName.toLowerCase()===o).length>1);let u=n?`${o}[${l}]`:o;if(e.unshift(u),i=i.parentElement,i&&i.nodeName.toLowerCase()==="html"){e.unshift("html");break}}return"/"+e.join("/")}function X(t){return t.replace(/"/g,'\\"').substring(0,64)}function Z(t){return t.indexOf('"')===-1||t.indexOf("'")===-1?t:`concat(${t.split('"').map((e,i,o)=>i===o.length-1?e?`"${e}"`:"":e?`"${e}",'"'`:`"'"`).filter(e=>e).join(",")})`}function tt(t){return t.length<=64?t:t.substring(0,64)}var et=10,H=1e3;function D(t,r,e=!0){let i=0,o=[],l=!1;function s(n,u){if(u>et||i>=H)return l=!0,null;let d=n.tagName.toLowerCase();if(!(d==="body"||d==="html")&&!L(n))return null;let A=`${r}_${i++}`,j=R(n),F=G(n);o.push({uid:A,css:j,xpath:F});let b=n,N=n.getAttribute("role"),T=k(n),y=g(n),x=b.value,C=b.href,S=b.src,w=$(n),_=I(n),c={uid:A,tag:d,...N&&{role:N},...T&&{name:T},...x&&{value:x},...C&&{href:C},...S&&{src:S},...y&&{text:y},...w&&{aria:w},..._&&{computed:_},children:[]};if(d==="iframe"&&e){try{let f=n,p=f.contentDocument||f.contentWindow?.document;if(p?.body){let m=s(p.body,u+1);m&&(m.isIframe=!0,m.frameSrc=f.src,c.children.push(m))}else c.isIframe=!0,c.frameSrc=f.src,c.crossOrigin=!0}catch{c.isIframe=!0,c.frameSrc=n.src,c.crossOrigin=!0}return c}for(let f=0;f<n.children.length;f++){if(i>=H){l=!0;break}let p=n.children[f];if(!p)continue;let m=s(p,u+1);m&&c.children.push(m)}return c}return{tree:s(t,0),uidMap:o,truncated:l}}function P(t){try{let r=D(document.body,t,!0);if(!r.tree)throw new Error("Failed to generate tree");return r}catch{return{tree:null,uidMap:[],truncated:!1}}}typeof window<"u"&&(window.__createSnapshot=P);return B(rt);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firefox-devtools-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Model Context Protocol (MCP) server for Firefox DevTools automation",
5
5
  "author": "freema",
6
6
  "license": "MIT",