@mozilla/firefox-devtools-mcp 0.9.4 → 0.9.6

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
@@ -128,8 +128,8 @@ var init_cli = __esm({
128
128
  },
129
129
  startUrl: {
130
130
  type: "string",
131
- description: "URL to open when Firefox starts (default: about:home)",
132
- default: process.env.START_URL ?? "about:home"
131
+ description: "URL to open when Firefox starts (default: about:blank)",
132
+ default: process.env.START_URL ?? "about:blank"
133
133
  },
134
134
  connectExisting: {
135
135
  type: "boolean",
@@ -164,9 +164,13 @@ var init_cli = __esm({
164
164
  description: "Android app package name (default: org.mozilla.firefox). Use org.mozilla.fenix for Nightly.",
165
165
  default: process.env.ANDROID_PACKAGE ?? "org.mozilla.firefox"
166
166
  },
167
+ logFile: {
168
+ type: "string",
169
+ description: "Path to a file where MCP server logs will be written. Set DEBUG=* to also enable verbose debug logs."
170
+ },
167
171
  enableScript: {
168
172
  type: "boolean",
169
- description: "Enable the evaluate_script tool, which allows executing arbitrary JavaScript in the page context.",
173
+ description: "Enable the script tools such as script evaluation and logpoints (Firefox 153+ required).",
170
174
  default: (process.env.ENABLE_SCRIPT ?? "false") === "true"
171
175
  },
172
176
  enablePrivilegedContext: {
@@ -17762,6 +17766,9 @@ var require_utils = __commonJS({
17762
17766
  "use strict";
17763
17767
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
17764
17768
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
17769
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
17770
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
17771
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
17765
17772
  function stringArrayToHexStripped(input) {
17766
17773
  let acc = "";
17767
17774
  let code = 0;
@@ -17954,27 +17961,77 @@ var require_utils = __commonJS({
17954
17961
  }
17955
17962
  return output.join("");
17956
17963
  }
17957
- function normalizeComponentEncoding(component, esc2) {
17958
- const func = esc2 !== true ? escape : unescape;
17959
- if (component.scheme !== void 0) {
17960
- component.scheme = func(component.scheme);
17961
- }
17962
- if (component.userinfo !== void 0) {
17963
- component.userinfo = func(component.userinfo);
17964
- }
17965
- if (component.host !== void 0) {
17966
- component.host = func(component.host);
17964
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
17965
+ var HOST_DELIM_RE = /[@/?#:]/g;
17966
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
17967
+ function reescapeHostDelimiters(host, isIP) {
17968
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
17969
+ re.lastIndex = 0;
17970
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
17971
+ }
17972
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
17973
+ if (input.indexOf("%") === -1) {
17974
+ return input;
17967
17975
  }
17968
- if (component.path !== void 0) {
17969
- component.path = func(component.path);
17976
+ let output = "";
17977
+ for (let i = 0; i < input.length; i++) {
17978
+ if (input[i] === "%" && i + 2 < input.length) {
17979
+ const hex3 = input.slice(i + 1, i + 3);
17980
+ if (isHexPair(hex3)) {
17981
+ const normalizedHex = hex3.toUpperCase();
17982
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
17983
+ if (decodeUnreserved && isUnreserved(decoded)) {
17984
+ output += decoded;
17985
+ } else {
17986
+ output += "%" + normalizedHex;
17987
+ }
17988
+ i += 2;
17989
+ continue;
17990
+ }
17991
+ }
17992
+ output += input[i];
17970
17993
  }
17971
- if (component.query !== void 0) {
17972
- component.query = func(component.query);
17994
+ return output;
17995
+ }
17996
+ function normalizePathEncoding(input) {
17997
+ let output = "";
17998
+ for (let i = 0; i < input.length; i++) {
17999
+ if (input[i] === "%" && i + 2 < input.length) {
18000
+ const hex3 = input.slice(i + 1, i + 3);
18001
+ if (isHexPair(hex3)) {
18002
+ const normalizedHex = hex3.toUpperCase();
18003
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
18004
+ if (decoded !== "." && isUnreserved(decoded)) {
18005
+ output += decoded;
18006
+ } else {
18007
+ output += "%" + normalizedHex;
18008
+ }
18009
+ i += 2;
18010
+ continue;
18011
+ }
18012
+ }
18013
+ if (isPathCharacter(input[i])) {
18014
+ output += input[i];
18015
+ } else {
18016
+ output += escape(input[i]);
18017
+ }
17973
18018
  }
17974
- if (component.fragment !== void 0) {
17975
- component.fragment = func(component.fragment);
18019
+ return output;
18020
+ }
18021
+ function escapePreservingEscapes(input) {
18022
+ let output = "";
18023
+ for (let i = 0; i < input.length; i++) {
18024
+ if (input[i] === "%" && i + 2 < input.length) {
18025
+ const hex3 = input.slice(i + 1, i + 3);
18026
+ if (isHexPair(hex3)) {
18027
+ output += "%" + hex3.toUpperCase();
18028
+ i += 2;
18029
+ continue;
18030
+ }
18031
+ }
18032
+ output += escape(input[i]);
17976
18033
  }
17977
- return component;
18034
+ return output;
17978
18035
  }
17979
18036
  function recomposeAuthority(component) {
17980
18037
  const uriTokens = [];
@@ -17989,7 +18046,7 @@ var require_utils = __commonJS({
17989
18046
  if (ipV6res.isIPV6 === true) {
17990
18047
  host = `[${ipV6res.escapedHost}]`;
17991
18048
  } else {
17992
- host = component.host;
18049
+ host = reescapeHostDelimiters(host, false);
17993
18050
  }
17994
18051
  }
17995
18052
  uriTokens.push(host);
@@ -18003,7 +18060,10 @@ var require_utils = __commonJS({
18003
18060
  module.exports = {
18004
18061
  nonSimpleDomain,
18005
18062
  recomposeAuthority,
18006
- normalizeComponentEncoding,
18063
+ reescapeHostDelimiters,
18064
+ normalizePercentEncoding,
18065
+ normalizePathEncoding,
18066
+ escapePreservingEscapes,
18007
18067
  removeDotSegments,
18008
18068
  isIPv4,
18009
18069
  isUUID,
@@ -18227,12 +18287,12 @@ var require_schemes = __commonJS({
18227
18287
  var require_fast_uri = __commonJS({
18228
18288
  "node_modules/fast-uri/index.js"(exports, module) {
18229
18289
  "use strict";
18230
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
18290
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
18231
18291
  var { SCHEMES, getSchemeHandler } = require_schemes();
18232
18292
  function normalize(uri, options) {
18233
18293
  if (typeof uri === "string") {
18234
18294
  uri = /** @type {T} */
18235
- serialize(parse3(uri, options), options);
18295
+ normalizeString(uri, options);
18236
18296
  } else if (typeof uri === "object") {
18237
18297
  uri = /** @type {T} */
18238
18298
  parse3(serialize(uri, options), options);
@@ -18299,19 +18359,9 @@ var require_fast_uri = __commonJS({
18299
18359
  return target;
18300
18360
  }
18301
18361
  function equal(uriA, uriB, options) {
18302
- if (typeof uriA === "string") {
18303
- uriA = unescape(uriA);
18304
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
18305
- } else if (typeof uriA === "object") {
18306
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
18307
- }
18308
- if (typeof uriB === "string") {
18309
- uriB = unescape(uriB);
18310
- uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
18311
- } else if (typeof uriB === "object") {
18312
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
18313
- }
18314
- return uriA.toLowerCase() === uriB.toLowerCase();
18362
+ const normalizedA = normalizeComparableURI(uriA, options);
18363
+ const normalizedB = normalizeComparableURI(uriB, options);
18364
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
18315
18365
  }
18316
18366
  function serialize(cmpts, opts) {
18317
18367
  const component = {
@@ -18336,12 +18386,12 @@ var require_fast_uri = __commonJS({
18336
18386
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
18337
18387
  if (component.path !== void 0) {
18338
18388
  if (!options.skipEscape) {
18339
- component.path = escape(component.path);
18389
+ component.path = escapePreservingEscapes(component.path);
18340
18390
  if (component.scheme !== void 0) {
18341
18391
  component.path = component.path.split("%3A").join(":");
18342
18392
  }
18343
18393
  } else {
18344
- component.path = unescape(component.path);
18394
+ component.path = normalizePercentEncoding(component.path);
18345
18395
  }
18346
18396
  }
18347
18397
  if (options.reference !== "suffix" && component.scheme) {
@@ -18376,7 +18426,16 @@ var require_fast_uri = __commonJS({
18376
18426
  return uriTokens.join("");
18377
18427
  }
18378
18428
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
18379
- function parse3(uri, opts) {
18429
+ function getParseError(parsed, matches) {
18430
+ if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
18431
+ return 'URI path must start with "/" when authority is present.';
18432
+ }
18433
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
18434
+ return "URI port is malformed.";
18435
+ }
18436
+ return void 0;
18437
+ }
18438
+ function parseWithStatus(uri, opts) {
18380
18439
  const options = Object.assign({}, opts);
18381
18440
  const parsed = {
18382
18441
  scheme: void 0,
@@ -18387,6 +18446,7 @@ var require_fast_uri = __commonJS({
18387
18446
  query: void 0,
18388
18447
  fragment: void 0
18389
18448
  };
18449
+ let malformedAuthorityOrPort = false;
18390
18450
  let isIP = false;
18391
18451
  if (options.reference === "suffix") {
18392
18452
  if (options.scheme) {
@@ -18407,6 +18467,11 @@ var require_fast_uri = __commonJS({
18407
18467
  if (isNaN(parsed.port)) {
18408
18468
  parsed.port = matches[5];
18409
18469
  }
18470
+ const parseError = getParseError(parsed, matches);
18471
+ if (parseError !== void 0) {
18472
+ parsed.error = parsed.error || parseError;
18473
+ malformedAuthorityOrPort = true;
18474
+ }
18410
18475
  if (parsed.host) {
18411
18476
  const ipv4result = isIPv4(parsed.host);
18412
18477
  if (ipv4result === false) {
@@ -18445,14 +18510,18 @@ var require_fast_uri = __commonJS({
18445
18510
  parsed.scheme = unescape(parsed.scheme);
18446
18511
  }
18447
18512
  if (parsed.host !== void 0) {
18448
- parsed.host = unescape(parsed.host);
18513
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
18449
18514
  }
18450
18515
  }
18451
18516
  if (parsed.path) {
18452
- parsed.path = escape(unescape(parsed.path));
18517
+ parsed.path = normalizePathEncoding(parsed.path);
18453
18518
  }
18454
18519
  if (parsed.fragment) {
18455
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
18520
+ try {
18521
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
18522
+ } catch {
18523
+ parsed.error = parsed.error || "URI malformed";
18524
+ }
18456
18525
  }
18457
18526
  }
18458
18527
  if (schemeHandler && schemeHandler.parse) {
@@ -18461,7 +18530,29 @@ var require_fast_uri = __commonJS({
18461
18530
  } else {
18462
18531
  parsed.error = parsed.error || "URI can not be parsed.";
18463
18532
  }
18464
- return parsed;
18533
+ return { parsed, malformedAuthorityOrPort };
18534
+ }
18535
+ function parse3(uri, opts) {
18536
+ return parseWithStatus(uri, opts).parsed;
18537
+ }
18538
+ function normalizeString(uri, opts) {
18539
+ return normalizeStringWithStatus(uri, opts).normalized;
18540
+ }
18541
+ function normalizeStringWithStatus(uri, opts) {
18542
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
18543
+ return {
18544
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
18545
+ malformedAuthorityOrPort
18546
+ };
18547
+ }
18548
+ function normalizeComparableURI(uri, opts) {
18549
+ if (typeof uri === "string") {
18550
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
18551
+ return malformedAuthorityOrPort ? void 0 : normalized;
18552
+ }
18553
+ if (typeof uri === "object") {
18554
+ return serialize(uri, opts);
18555
+ }
18465
18556
  }
18466
18557
  var fastUri = {
18467
18558
  SCHEMES,
@@ -27137,12 +27228,12 @@ var require_dist = __commonJS({
27137
27228
  throw new Error(`Unknown format "${name}"`);
27138
27229
  return f;
27139
27230
  };
27140
- function addFormats(ajv, list, fs, exportName) {
27231
+ function addFormats(ajv, list, fs2, exportName) {
27141
27232
  var _a2;
27142
27233
  var _b;
27143
27234
  (_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
27144
27235
  for (const f of list)
27145
- ajv.addFormat(f, fs[f]);
27236
+ ajv.addFormat(f, fs2[f]);
27146
27237
  }
27147
27238
  module.exports = exports = formatsPlugin;
27148
27239
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -27991,27 +28082,76 @@ var init_constants = __esm({
27991
28082
  });
27992
28083
 
27993
28084
  // src/utils/logger.ts
27994
- function log(message, ...args2) {
27995
- console.error(`[firefox-devtools-mcp] ${message}`, ...args2);
28085
+ import fs from "fs";
28086
+ function formatArgs(args2) {
28087
+ if (args2.length === 0) {
28088
+ return "";
28089
+ }
28090
+ return " " + args2.map((a) => {
28091
+ if (typeof a === "string") {
28092
+ return a;
28093
+ }
28094
+ try {
28095
+ return JSON.stringify(a);
28096
+ } catch {
28097
+ return String(a);
28098
+ }
28099
+ }).join(" ");
27996
28100
  }
27997
- function logError(message, error2) {
27998
- if (error2 instanceof Error) {
27999
- console.error(`[firefox-devtools-mcp] ERROR: ${message}`, error2.message);
28000
- if (error2.stack) {
28001
- console.error(error2.stack);
28101
+ function flushLogs(timeoutMs = 2e3) {
28102
+ if (!logStream) {
28103
+ return Promise.resolve();
28104
+ }
28105
+ return new Promise((resolve4, reject) => {
28106
+ const timeout = setTimeout(reject, timeoutMs);
28107
+ logStream.end(() => {
28108
+ clearTimeout(timeout);
28109
+ resolve4();
28110
+ });
28111
+ });
28112
+ }
28113
+ function write(message, args2, body) {
28114
+ if (logStream) {
28115
+ logStream.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${message}${formatArgs(args2)}
28116
+ `);
28117
+ if (body) {
28118
+ logStream.write(`${body}
28119
+ `);
28002
28120
  }
28003
28121
  } else {
28004
- console.error(`[firefox-devtools-mcp] ERROR: ${message}`, error2);
28122
+ console.error(message, ...args2);
28123
+ if (body) {
28124
+ console.error(body);
28125
+ }
28005
28126
  }
28006
28127
  }
28128
+ function log(message, ...args2) {
28129
+ write(`[firefox-devtools-mcp] ${message}`, args2);
28130
+ }
28007
28131
  function logDebug(message, ...args2) {
28008
28132
  if (process.env.DEBUG === "*" || process.env.DEBUG?.includes("firefox-devtools")) {
28009
- console.error(`[firefox-devtools-mcp] DEBUG: ${message}`, ...args2);
28133
+ write(`[firefox-devtools-mcp] DEBUG: ${message}`, args2);
28010
28134
  }
28011
28135
  }
28136
+ function logError(message, error2) {
28137
+ if (error2 instanceof Error) {
28138
+ write(`[firefox-devtools-mcp] ERROR: ${message}`, [error2.message], error2.stack);
28139
+ } else {
28140
+ write(`[firefox-devtools-mcp] ERROR: ${message}`, [error2]);
28141
+ }
28142
+ }
28143
+ function setupLogFile(filePath) {
28144
+ logStream = fs.createWriteStream(filePath, { flags: "a" });
28145
+ logStream.on("error", (error2) => {
28146
+ console.error(`[firefox-devtools-mcp] Error writing to log file: ${error2.message}`);
28147
+ logStream = null;
28148
+ });
28149
+ }
28150
+ var logStream;
28012
28151
  var init_logger = __esm({
28013
28152
  "src/utils/logger.ts"() {
28014
28153
  "use strict";
28154
+ logStream = null;
28015
28155
  }
28016
28156
  });
28017
28157
 
@@ -28123,11 +28263,12 @@ var init_core3 = __esm({
28123
28263
  constructor(options) {
28124
28264
  this.options = options;
28125
28265
  }
28126
- driver = null;
28127
28266
  currentContextId = null;
28128
- originalEnv = {};
28129
- logFilePath;
28267
+ driver = null;
28268
+ firefoxVersion = null;
28130
28269
  logFileFd;
28270
+ logFilePath;
28271
+ originalEnv = {};
28131
28272
  profileWarning = null;
28132
28273
  /**
28133
28274
  * Launch Firefox (or connect to an existing instance) and establish BiDi connection
@@ -28219,6 +28360,9 @@ var init_core3 = __esm({
28219
28360
  for (const [name, value] of Object.entries(this.options.prefs)) {
28220
28361
  firefoxOptions.setPreference(name, value);
28221
28362
  }
28363
+ if (this.options.prefs["remote.prefs.recommended"] === false && !("app.update.disabledForTesting" in this.options.prefs)) {
28364
+ firefoxOptions.setPreference("app.update.disabledForTesting", true);
28365
+ }
28222
28366
  }
28223
28367
  let serviceBuilder;
28224
28368
  if (process.platform === "win32") {
@@ -28233,11 +28377,18 @@ var init_core3 = __esm({
28233
28377
  serviceBuilder.setStdio(["ignore", this.logFileFd, this.logFileFd]);
28234
28378
  log(`Capturing Firefox output to: ${this.logFilePath}`);
28235
28379
  }
28380
+ const remoteLogLevel = this.options.prefs?.["remote.log.level"];
28381
+ if (remoteLogLevel && typeof remoteLogLevel === "string") {
28382
+ serviceBuilder.addArguments("--log", remoteLogLevel.toLowerCase());
28383
+ }
28236
28384
  this.driver = await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(firefoxOptions).setFirefoxService(serviceBuilder).build();
28237
28385
  }
28238
28386
  log(
28239
28387
  this.options.connectExisting ? "Connected to existing Firefox" : "Firefox launched with BiDi"
28240
28388
  );
28389
+ const driverCapabilities = await this.driver.getCapabilities();
28390
+ this.firefoxVersion = driverCapabilities.get("browserVersion") ?? null;
28391
+ logDebug(`Browser version: ${this.firefoxVersion}`);
28241
28392
  this.currentContextId = await this.driver.getWindowHandle();
28242
28393
  logDebug(`Browsing context ID: ${this.currentContextId}`);
28243
28394
  if (this.options.startUrl && !this.options.connectExisting) {
@@ -28271,24 +28422,6 @@ var init_core3 = __esm({
28271
28422
  return false;
28272
28423
  }
28273
28424
  }
28274
- /**
28275
- * Reset driver state (used when Firefox is detected as closed)
28276
- */
28277
- reset() {
28278
- if (this.driver) {
28279
- const d = this.driver;
28280
- if (d._bidiConnection) {
28281
- d._bidiConnection.close();
28282
- d._bidiConnection = void 0;
28283
- }
28284
- if ("quit" in this.driver) {
28285
- void this.driver.quit();
28286
- }
28287
- }
28288
- this.driver = null;
28289
- this.currentContextId = null;
28290
- logDebug("Driver state reset");
28291
- }
28292
28425
  /**
28293
28426
  * Get current browsing context ID
28294
28427
  */
@@ -28301,6 +28434,12 @@ var init_core3 = __esm({
28301
28434
  setCurrentContextId(contextId) {
28302
28435
  this.currentContextId = contextId;
28303
28436
  }
28437
+ /**
28438
+ * Get the current firefox version, as a string (eg "153.0a1")
28439
+ */
28440
+ getFirefoxVersion() {
28441
+ return this.firefoxVersion;
28442
+ }
28304
28443
  /**
28305
28444
  * Get log file path
28306
28445
  */
@@ -28386,20 +28525,47 @@ var init_core3 = __esm({
28386
28525
  }
28387
28526
  /**
28388
28527
  * Close driver and cleanup.
28389
- * When connected to an existing Firefox instance, only kills geckodriver
28390
- * without closing the browser.
28528
+ * - Tries graceful quit() with a timeout; on timeout, force-kills via onQuit_().
28529
+ * - Restores env vars, closes log fd, clears all state.
28530
+ * - Never throws — callers can rely on cleanup completing.
28391
28531
  */
28392
28532
  async close() {
28393
- if (this.driver) {
28394
- const d = this.driver;
28395
- if (d._bidiConnection) {
28396
- d._bidiConnection.close();
28397
- d._bidiConnection = void 0;
28533
+ if (!this.driver) {
28534
+ return;
28535
+ }
28536
+ const webdriver = this.driver;
28537
+ const webdriverQuitTimeout = 5e3;
28538
+ this.driver = null;
28539
+ this.currentContextId = null;
28540
+ this.logFilePath = void 0;
28541
+ this.profileWarning = null;
28542
+ if (webdriver._bidiConnection) {
28543
+ try {
28544
+ webdriver._bidiConnection.close();
28545
+ } catch {
28546
+ } finally {
28547
+ webdriver._bidiConnection = void 0;
28398
28548
  }
28399
- if ("quit" in this.driver) {
28400
- await this.driver.quit();
28549
+ }
28550
+ if ("quit" in webdriver) {
28551
+ let timer;
28552
+ try {
28553
+ await Promise.race([
28554
+ webdriver.quit(),
28555
+ new Promise((_, reject) => {
28556
+ timer = setTimeout(() => reject(new Error("close timeout")), webdriverQuitTimeout);
28557
+ })
28558
+ ]);
28559
+ } catch {
28560
+ const webdriverHasOnQuit = typeof webdriver.onQuit_ === "function";
28561
+ logDebug("WebDriver.quit() timed out or failed - force killing geckodriver");
28562
+ if (webdriverHasOnQuit) {
28563
+ void webdriver.onQuit_().catch(() => {
28564
+ });
28565
+ }
28566
+ } finally {
28567
+ clearTimeout(timer);
28401
28568
  }
28402
- this.driver = null;
28403
28569
  }
28404
28570
  if (this.logFileFd !== void 0) {
28405
28571
  try {
@@ -28789,12 +28955,135 @@ var init_network = __esm({
28789
28955
  }
28790
28956
  });
28791
28957
 
28958
+ // src/firefox/events/debugging.ts
28959
+ var MAX_LOGPOINT_RESULTS, DebuggingEvents;
28960
+ var init_debugging = __esm({
28961
+ "src/firefox/events/debugging.ts"() {
28962
+ "use strict";
28963
+ init_logger();
28964
+ MAX_LOGPOINT_RESULTS = 100;
28965
+ DebuggingEvents = class {
28966
+ constructor(driver, sendBiDiCommand) {
28967
+ this.driver = driver;
28968
+ this.sendBiDiCommand = sendBiDiCommand;
28969
+ }
28970
+ logpoints = /* @__PURE__ */ new Map();
28971
+ subscribed = false;
28972
+ /**
28973
+ * Subscribe to moz:debugging events
28974
+ */
28975
+ async subscribe(contextId) {
28976
+ if (this.subscribed) {
28977
+ return;
28978
+ }
28979
+ const bidi = await this.driver.getBidi();
28980
+ try {
28981
+ await bidi.subscribe("moz:debugging.paused", contextId ? [contextId] : void 0);
28982
+ await bidi.subscribe("moz:debugging.resumed", contextId ? [contextId] : void 0);
28983
+ } catch {
28984
+ logDebug(
28985
+ "Debugging events subscription skipped (may not be available in this Firefox version)"
28986
+ );
28987
+ }
28988
+ const ws = bidi.socket;
28989
+ ws.on("message", (data) => {
28990
+ try {
28991
+ const payload = JSON.parse(data.toString());
28992
+ if (payload?.method === "moz:debugging.paused") {
28993
+ const { context, url: url2, line, column } = payload.params;
28994
+ const logpointId = this.findLogpointByLocation(url2, line);
28995
+ if (logpointId) {
28996
+ void this.handleLogpointPause(context, logpointId);
28997
+ return;
28998
+ }
28999
+ logDebug(`moz:Debugging paused in context: ${context} at ${url2}:${line}:${column}`);
29000
+ }
29001
+ if (payload?.method === "moz:debugging.resumed") {
29002
+ logDebug(`moz:Debugging resumed in context: ${payload.params.context}`);
29003
+ }
29004
+ } catch {
29005
+ }
29006
+ });
29007
+ this.subscribed = true;
29008
+ logDebug("moz:debugging listener active");
29009
+ }
29010
+ addLogpoint(logpointId, url2, line, expression) {
29011
+ this.logpoints.set(logpointId, {
29012
+ location: { url: url2, line },
29013
+ expression,
29014
+ results: [],
29015
+ capped: false
29016
+ });
29017
+ }
29018
+ removeLogpoint(logpointId) {
29019
+ this.logpoints.delete(logpointId);
29020
+ }
29021
+ getLogpointResults(logpointId) {
29022
+ return this.logpoints.get(logpointId)?.results ?? null;
29023
+ }
29024
+ findLogpointByLocation(url2, line) {
29025
+ for (const [logpointId, entry] of this.logpoints) {
29026
+ if (entry.location.url === url2 && entry.location.line === line) {
29027
+ return logpointId;
29028
+ }
29029
+ }
29030
+ return null;
29031
+ }
29032
+ async handleLogpointPause(contextId, logpointId) {
29033
+ const entry = this.logpoints.get(logpointId);
29034
+ if (!entry) {
29035
+ return;
29036
+ }
29037
+ logDebug(`Logpoint hit: ${logpointId} in context ${contextId}`);
29038
+ try {
29039
+ const result = await this.sendBiDiCommand("script.evaluate", {
29040
+ expression: entry.expression,
29041
+ target: { context: contextId },
29042
+ awaitPromise: false
29043
+ });
29044
+ const evalResult = result;
29045
+ if (evalResult.type === "exception") {
29046
+ entry.results.push({
29047
+ value: null,
29048
+ error: evalResult.exceptionDetails?.text ?? "Unknown error",
29049
+ timestamp: Date.now()
29050
+ });
29051
+ } else {
29052
+ entry.results.push({
29053
+ value: evalResult.result,
29054
+ timestamp: Date.now()
29055
+ });
29056
+ }
29057
+ } catch (error2) {
29058
+ entry.results.push({
29059
+ value: null,
29060
+ error: String(error2),
29061
+ timestamp: Date.now()
29062
+ });
29063
+ } finally {
29064
+ if (entry.results.length > MAX_LOGPOINT_RESULTS) {
29065
+ entry.results.splice(0, entry.results.length - MAX_LOGPOINT_RESULTS);
29066
+ if (!entry.capped) {
29067
+ entry.capped = true;
29068
+ logDebug(`Logpoint ${logpointId}: result buffer capped at ${MAX_LOGPOINT_RESULTS}`);
29069
+ }
29070
+ }
29071
+ await this.sendBiDiCommand("moz:debugging.resume", { context: contextId }).catch((err) => {
29072
+ logDebug(`Failed to resume after logpoint: ${String(err)}`);
29073
+ });
29074
+ }
29075
+ }
29076
+ };
29077
+ }
29078
+ });
29079
+
28792
29080
  // src/firefox/events/index.ts
28793
29081
  var init_events = __esm({
28794
29082
  "src/firefox/events/index.ts"() {
28795
29083
  "use strict";
28796
29084
  init_console();
28797
29085
  init_network();
29086
+ init_debugging();
28798
29087
  }
28799
29088
  });
28800
29089
 
@@ -29706,6 +29995,7 @@ var init_firefox = __esm({
29706
29995
  core;
29707
29996
  consoleEvents = null;
29708
29997
  networkEvents = null;
29998
+ debuggingEvents = null;
29709
29999
  dom = null;
29710
30000
  pages = null;
29711
30001
  snapshot = null;
@@ -29734,6 +30024,10 @@ var init_firefox = __esm({
29734
30024
  onNavigate,
29735
30025
  autoClearOnNavigate: false
29736
30026
  });
30027
+ this.debuggingEvents = new DebuggingEvents(
30028
+ driver,
30029
+ (method, params) => this.core.sendBiDiCommand(method, params)
30030
+ );
29737
30031
  }
29738
30032
  this.dom = new DomInteractions(
29739
30033
  driver,
@@ -29761,6 +30055,14 @@ var init_firefox = __esm({
29761
30055
  this.networkEvents = null;
29762
30056
  }
29763
30057
  }
30058
+ if (this.debuggingEvents) {
30059
+ try {
30060
+ await this.debuggingEvents.subscribe();
30061
+ } catch {
30062
+ logDebug("Debugging events unavailable (BiDi not supported by this Firefox session)");
30063
+ this.debuggingEvents = null;
30064
+ }
30065
+ }
29764
30066
  }
29765
30067
  // ============================================================================
29766
30068
  // DOM / Evaluate
@@ -30054,6 +30356,48 @@ var init_firefox = __esm({
30054
30356
  async isConnected() {
30055
30357
  return await this.core.isConnected();
30056
30358
  }
30359
+ /**
30360
+ * Get current browser version (eg "153.0a1").
30361
+ * @internal
30362
+ */
30363
+ getFirefoxVersion() {
30364
+ return this.core.getFirefoxVersion();
30365
+ }
30366
+ /**
30367
+ * @internal
30368
+ */
30369
+ async setLogpoint(url2, line, expression) {
30370
+ if (!this.debuggingEvents) {
30371
+ throw new Error("Debugging events not available");
30372
+ }
30373
+ const result = await this.core.sendBiDiCommand("moz:debugging.setBreakpoint", {
30374
+ location: { url: url2, line }
30375
+ });
30376
+ const logpointId = result.breakpoint;
30377
+ this.debuggingEvents.addLogpoint(logpointId, url2, line, expression);
30378
+ return logpointId;
30379
+ }
30380
+ /**
30381
+ * @internal
30382
+ */
30383
+ async removeLogpoint(logpointId) {
30384
+ if (!this.debuggingEvents) {
30385
+ throw new Error("Debugging events not available");
30386
+ }
30387
+ await this.core.sendBiDiCommand("moz:debugging.removeBreakpoint", {
30388
+ breakpoint: logpointId
30389
+ });
30390
+ this.debuggingEvents.removeLogpoint(logpointId);
30391
+ }
30392
+ /**
30393
+ * @internal
30394
+ */
30395
+ getLogpointResults(logpointId) {
30396
+ if (!this.debuggingEvents) {
30397
+ return null;
30398
+ }
30399
+ return this.debuggingEvents.getLogpointResults(logpointId);
30400
+ }
30057
30401
  /**
30058
30402
  * Get log file path (if logging is enabled)
30059
30403
  */
@@ -30073,23 +30417,22 @@ var init_firefox = __esm({
30073
30417
  getOptions() {
30074
30418
  return this.core.getOptions();
30075
30419
  }
30076
- /**
30077
- * Reset all internal state (used when Firefox is detected as closed)
30078
- */
30079
- reset() {
30080
- this.core.reset();
30420
+ // ============================================================================
30421
+ // Cleanup
30422
+ // ============================================================================
30423
+ async close() {
30424
+ try {
30425
+ await this.core.close();
30426
+ } catch (error2) {
30427
+ logDebug(`close() failed: ${error2 instanceof Error ? error2.message : String(error2)}`);
30428
+ }
30081
30429
  this.consoleEvents = null;
30082
30430
  this.networkEvents = null;
30431
+ this.debuggingEvents = null;
30083
30432
  this.dom = null;
30084
30433
  this.pages = null;
30085
30434
  this.snapshot = null;
30086
30435
  }
30087
- // ============================================================================
30088
- // Cleanup
30089
- // ============================================================================
30090
- async close() {
30091
- await this.core.close();
30092
- }
30093
30436
  };
30094
30437
  }
30095
30438
  });
@@ -30371,7 +30714,68 @@ var init_pages2 = __esm({
30371
30714
  }
30372
30715
  });
30373
30716
 
30374
- // src/tools/script.ts
30717
+ // src/utils/remote-value.ts
30718
+ function remoteValueToNative(rv) {
30719
+ if (!rv || typeof rv !== "object") {
30720
+ return rv;
30721
+ }
30722
+ const { type, value } = rv;
30723
+ switch (type) {
30724
+ case "undefined":
30725
+ return void 0;
30726
+ case "null":
30727
+ return null;
30728
+ case "string":
30729
+ case "boolean":
30730
+ return value;
30731
+ case "number":
30732
+ if (value === "NaN") {
30733
+ return "NaN";
30734
+ }
30735
+ if (value === "Infinity") {
30736
+ return "Infinity";
30737
+ }
30738
+ if (value === "-Infinity") {
30739
+ return "-Infinity";
30740
+ }
30741
+ if (value === "-0") {
30742
+ return "-0";
30743
+ }
30744
+ return value;
30745
+ case "bigint":
30746
+ return `${value}n`;
30747
+ case "array":
30748
+ return value.map(remoteValueToNative);
30749
+ case "object":
30750
+ return Object.fromEntries(
30751
+ value.map(([k, v]) => [k, remoteValueToNative(v)])
30752
+ );
30753
+ case "map":
30754
+ return Object.fromEntries(
30755
+ value.map(([k, v]) => [
30756
+ typeof k === "object" ? JSON.stringify(remoteValueToNative(k)) : String(k),
30757
+ remoteValueToNative(v)
30758
+ ])
30759
+ );
30760
+ case "set":
30761
+ return value.map(remoteValueToNative);
30762
+ case "regexp": {
30763
+ const { pattern, flags } = value;
30764
+ return `/${pattern}/${flags ?? ""}`;
30765
+ }
30766
+ case "date":
30767
+ return value;
30768
+ default:
30769
+ return `[${type}]`;
30770
+ }
30771
+ }
30772
+ var init_remote_value = __esm({
30773
+ "src/utils/remote-value.ts"() {
30774
+ "use strict";
30775
+ }
30776
+ });
30777
+
30778
+ // src/utils/js-validation.ts
30375
30779
  function validateFunction(fnString) {
30376
30780
  if (!fnString || typeof fnString !== "string") {
30377
30781
  throw new Error("function parameter is required and must be a string");
@@ -30395,6 +30799,15 @@ Valid examples:
30395
30799
  );
30396
30800
  }
30397
30801
  }
30802
+ var MAX_FUNCTION_SIZE;
30803
+ var init_js_validation = __esm({
30804
+ "src/utils/js-validation.ts"() {
30805
+ "use strict";
30806
+ MAX_FUNCTION_SIZE = 16 * 1024;
30807
+ }
30808
+ });
30809
+
30810
+ // src/tools/script.ts
30398
30811
  async function handleEvaluateScript(args2) {
30399
30812
  try {
30400
30813
  const {
@@ -30405,17 +30818,13 @@ async function handleEvaluateScript(args2) {
30405
30818
  validateFunction(fnString);
30406
30819
  const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
30407
30820
  const firefox3 = await getFirefox2();
30408
- const driver = firefox3.getDriver();
30409
- if (!driver) {
30410
- throw new Error("WebDriver not available");
30411
- }
30412
30821
  const scriptTimeout = timeout ?? DEFAULT_TIMEOUT;
30413
30822
  const resolvedArgs = [];
30414
30823
  if (fnArgs && fnArgs.length > 0) {
30415
30824
  for (const arg of fnArgs) {
30416
30825
  try {
30417
30826
  const element = await firefox3.resolveUidToElement(arg.uid);
30418
- resolvedArgs.push(element);
30827
+ resolvedArgs.push({ sharedId: await element.getId() });
30419
30828
  } catch (error2) {
30420
30829
  const errorMsg = error2.message;
30421
30830
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
@@ -30430,40 +30839,55 @@ Please call take_snapshot to get fresh UIDs and try again.`
30430
30839
  }
30431
30840
  }
30432
30841
  }
30433
- const evalCode = `
30434
- const fn = ${fnString};
30435
- const args = Array.from(arguments);
30436
- const result = fn(...args);
30437
- return result instanceof Promise ? result : Promise.resolve(result);
30438
- `;
30439
- await driver.manage().setTimeouts({ script: scriptTimeout });
30440
- const result = await driver.executeScript(evalCode, ...resolvedArgs);
30441
- let output = "Script ran on page and returned:\n";
30442
- output += "```json\n";
30443
- output += JSON.stringify(result, null, 2);
30444
- output += "\n```";
30445
- return successResponse(output);
30446
- } catch (error2) {
30447
- const errorMsg = error2.message;
30448
- if (errorMsg.includes("timeout") || errorMsg.includes("Timeout")) {
30449
- const timeoutValue = args2?.timeout ?? DEFAULT_TIMEOUT;
30842
+ const callFunctionPromise = firefox3.sendBiDiCommand("script.callFunction", {
30843
+ functionDeclaration: fnString,
30844
+ awaitPromise: true,
30845
+ arguments: resolvedArgs,
30846
+ target: { context: firefox3.getCurrentContextId() }
30847
+ });
30848
+ const result = await Promise.race([
30849
+ new Promise((r) => setTimeout(() => r(TIMEOUT), scriptTimeout)),
30850
+ callFunctionPromise
30851
+ ]);
30852
+ if (result === TIMEOUT) {
30450
30853
  return errorResponse(
30451
30854
  new Error(
30452
- `Script execution timed out (exceeded ${timeoutValue}ms).
30855
+ `Script execution timed out (exceeded ${scriptTimeout}ms).
30453
30856
 
30454
30857
  The function may contain an infinite loop or be waiting for a slow operation.
30455
30858
  Try simplifying the script or increasing the timeout parameter.`
30456
30859
  )
30457
30860
  );
30861
+ } else if (result.type === EvaluateResultType.Success) {
30862
+ let output = "Script ran on page and returned:\n";
30863
+ output += "```json\n";
30864
+ output += JSON.stringify(remoteValueToNative(result.result), null, 2);
30865
+ output += "\n```";
30866
+ return successResponse(output);
30867
+ } else if (result.type === EvaluateResultType.Exception) {
30868
+ const exceptionDetails = result.exceptionDetails;
30869
+ return errorResponse(
30870
+ new Error(
30871
+ `Script execution failed: ${exceptionDetails.text}
30872
+
30873
+ \`\`\`json
30874
+ ` + JSON.stringify(remoteValueToNative(exceptionDetails.exception), null, 2) + "\n```"
30875
+ )
30876
+ );
30877
+ } else {
30878
+ return errorResponse(`Unexpected script.callFunction result type: ${result.type}`);
30458
30879
  }
30880
+ } catch (error2) {
30459
30881
  return errorResponse(error2);
30460
30882
  }
30461
30883
  }
30462
- var evaluateScriptTool, MAX_FUNCTION_SIZE, DEFAULT_TIMEOUT;
30884
+ var evaluateScriptTool, DEFAULT_TIMEOUT, TIMEOUT, EvaluateResultType;
30463
30885
  var init_script = __esm({
30464
30886
  "src/tools/script.ts"() {
30465
30887
  "use strict";
30466
30888
  init_response_helpers();
30889
+ init_remote_value();
30890
+ init_js_validation();
30467
30891
  evaluateScriptTool = {
30468
30892
  name: "evaluate_script",
30469
30893
  description: "Execute JS function in page. Prefer UID tools for interactions.",
@@ -30496,8 +30920,12 @@ var init_script = __esm({
30496
30920
  required: ["function"]
30497
30921
  }
30498
30922
  };
30499
- MAX_FUNCTION_SIZE = 16 * 1024;
30500
30923
  DEFAULT_TIMEOUT = 5e3;
30924
+ TIMEOUT = Symbol("Timeout");
30925
+ EvaluateResultType = {
30926
+ Exception: "exception",
30927
+ Success: "success"
30928
+ };
30501
30929
  }
30502
30930
  });
30503
30931
 
@@ -31710,10 +32138,12 @@ async function handleGetFirefoxInfo(_input) {
31710
32138
  const firefox3 = await getFirefox();
31711
32139
  const options = firefox3.getOptions();
31712
32140
  const logFilePath = firefox3.getLogFilePath();
32141
+ const version3 = firefox3.getFirefoxVersion();
31713
32142
  const info = [];
31714
32143
  info.push("Firefox Instance Configuration");
31715
32144
  info.push("");
31716
32145
  info.push(`Binary: ${options.firefoxPath ?? "System Firefox (default)"}`);
32146
+ info.push(`Firefox version: ${version3 ?? "(unknown)"}`);
31717
32147
  info.push(`Headless: ${options.headless ? "Yes" : "No"}`);
31718
32148
  if (options.viewport) {
31719
32149
  info.push(`Viewport: ${options.viewport.width}x${options.viewport.height}`);
@@ -31782,15 +32212,11 @@ async function handleRestartFirefox(input) {
31782
32212
  profilePath: profilePath ?? currentOptions.profilePath,
31783
32213
  env: newEnv !== void 0 ? newEnv : currentOptions.env,
31784
32214
  headless: headless !== void 0 ? headless : currentOptions.headless,
31785
- startUrl: startUrl ?? currentOptions.startUrl ?? "about:home",
32215
+ startUrl: startUrl ?? currentOptions.startUrl ?? "about:blank",
31786
32216
  prefs: mergedPrefs
31787
32217
  };
31788
32218
  setNextLaunchOptions(newOptions);
31789
- try {
31790
- await currentFirefox.close();
31791
- } catch {
31792
- }
31793
- resetFirefox();
32219
+ await resetFirefox();
31794
32220
  const changes = [];
31795
32221
  if (firefoxPath && firefoxPath !== currentOptions.firefoxPath) {
31796
32222
  changes.push(`Binary: ${firefoxPath}`);
@@ -31821,7 +32247,7 @@ ${changes.join("\n")}`
31821
32247
  );
31822
32248
  } else {
31823
32249
  if (currentFirefox) {
31824
- resetFirefox();
32250
+ await resetFirefox();
31825
32251
  }
31826
32252
  const resolvedFirefoxPath = firefoxPath ?? args.firefoxPath ?? void 0;
31827
32253
  if (!resolvedFirefoxPath) {
@@ -31836,7 +32262,7 @@ ${changes.join("\n")}`
31836
32262
  profilePath: profilePath ?? args.profilePath ?? void 0,
31837
32263
  env: newEnv,
31838
32264
  headless: headless ?? false,
31839
- startUrl: startUrl ?? "about:home"
32265
+ startUrl: startUrl ?? "about:blank"
31840
32266
  };
31841
32267
  setNextLaunchOptions(newOptions);
31842
32268
  const config2 = [`Binary: ${resolvedFirefoxPath}`];
@@ -31927,7 +32353,7 @@ var init_firefox_management = __esm({
31927
32353
  },
31928
32354
  startUrl: {
31929
32355
  type: "string",
31930
- description: "URL to navigate to after restart (optional, uses about:home if not specified)"
32356
+ description: "URL to navigate to after restart (optional, uses about:blank if not specified)"
31931
32357
  },
31932
32358
  prefs: {
31933
32359
  type: "object",
@@ -31943,10 +32369,6 @@ var init_firefox_management = __esm({
31943
32369
  });
31944
32370
 
31945
32371
  // src/tools/privileged-context.ts
31946
- function isLikelyStatement(input) {
31947
- const trimmed = input.trim();
31948
- return /^(const|let|var)\s/.test(trimmed);
31949
- }
31950
32372
  function formatContextList(contexts) {
31951
32373
  if (contexts.length === 0) {
31952
32374
  return "No privileged contexts found";
@@ -32009,41 +32431,46 @@ async function handleSelectPrivilegedContext(args2) {
32009
32431
  }
32010
32432
  async function handleEvaluatePrivilegedScript(args2) {
32011
32433
  try {
32012
- const { expression } = args2;
32013
- if (!expression || typeof expression !== "string") {
32014
- throw new Error("expression parameter is required and must be a string");
32015
- }
32016
- if (isLikelyStatement(expression)) {
32017
- return errorResponse(
32018
- new Error(
32019
- `Cannot evaluate statement: "${expression.substring(0, 50)}${expression.length > 50 ? "..." : ""}". This tool expects an expression, not a statement (const/let/var declarations are statements). To use statements, wrap them in an IIFE: (function() { const x = 1; return x; })()`
32020
- )
32021
- );
32022
- }
32434
+ const { function: fnString } = args2;
32435
+ validateFunction(fnString);
32023
32436
  const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
32024
32437
  const firefox3 = await getFirefox2();
32025
- const driver = firefox3.getDriver();
32026
- try {
32027
- const result = await driver.executeScript(`return (${expression});`);
32028
- const resultText = typeof result === "string" ? result : result === null ? "null" : result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
32029
- return successResponse(`Result:
32030
- ${resultText}`);
32031
- } catch (executeError) {
32438
+ const result = await firefox3.sendBiDiCommand("script.callFunction", {
32439
+ functionDeclaration: fnString,
32440
+ awaitPromise: true,
32441
+ arguments: [],
32442
+ target: { context: firefox3.getCurrentContextId() }
32443
+ });
32444
+ if (result.type === EvaluateResultType2.Success) {
32445
+ let output = "Script ran in chrome context and returned:\n";
32446
+ output += "```json\n";
32447
+ output += JSON.stringify(remoteValueToNative(result.result), null, 2);
32448
+ output += "\n```";
32449
+ return successResponse(output);
32450
+ } else if (result.type === EvaluateResultType2.Exception) {
32451
+ const exceptionDetails = result.exceptionDetails;
32032
32452
  return errorResponse(
32033
32453
  new Error(
32034
- `Script execution failed: ${executeError instanceof Error ? executeError.message : String(executeError)}`
32454
+ `Script execution failed: ${exceptionDetails.text}
32455
+
32456
+ \`\`\`json
32457
+ ` + JSON.stringify(remoteValueToNative(exceptionDetails.exception), null, 2) + "\n```"
32035
32458
  )
32036
32459
  );
32460
+ } else {
32461
+ return errorResponse(`Unexpected script.callFunction result type: ${result.type}`);
32037
32462
  }
32038
32463
  } catch (error2) {
32039
32464
  return errorResponse(error2);
32040
32465
  }
32041
32466
  }
32042
- var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool;
32467
+ var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool, EvaluateResultType2;
32043
32468
  var init_privileged_context = __esm({
32044
32469
  "src/tools/privileged-context.ts"() {
32045
32470
  "use strict";
32046
32471
  init_response_helpers();
32472
+ init_js_validation();
32473
+ init_remote_value();
32047
32474
  listPrivilegedContextsTool = {
32048
32475
  name: "list_privileged_contexts",
32049
32476
  description: "List privileged (privileged) browsing contexts. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Use restart_firefox with env parameter to enable.",
@@ -32068,18 +32495,22 @@ var init_privileged_context = __esm({
32068
32495
  };
32069
32496
  evaluatePrivilegedScriptTool = {
32070
32497
  name: "evaluate_privileged_script",
32071
- description: "Evaluate JavaScript in the current privileged context. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Returns the result of the expression. IMPORTANT: Only provide expressions, not statements. Do not use const, let, or var declarations as they will cause syntax errors. For complex logic, wrap in an IIFE: (function() { const x = 1; return x; })()",
32498
+ description: "Execute JS function in the current privileged context. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Use select_privileged_context first to target a chrome context.",
32072
32499
  inputSchema: {
32073
32500
  type: "object",
32074
32501
  properties: {
32075
- expression: {
32502
+ function: {
32076
32503
  type: "string",
32077
- description: "JavaScript expression to evaluate in the privileged context"
32504
+ description: 'JS function string, e.g. () => Services.prefs.getBoolPref("foo")'
32078
32505
  }
32079
32506
  },
32080
- required: ["expression"]
32507
+ required: ["function"]
32081
32508
  }
32082
32509
  };
32510
+ EvaluateResultType2 = {
32511
+ Exception: "exception",
32512
+ Success: "success"
32513
+ };
32083
32514
  }
32084
32515
  });
32085
32516
 
@@ -32554,6 +32985,354 @@ var init_webextension = __esm({
32554
32985
  }
32555
32986
  });
32556
32987
 
32988
+ // src/utils/version.ts
32989
+ function getMajorVersion(version3) {
32990
+ const [major2, _rhs] = version3.split(".");
32991
+ if (!major2) {
32992
+ throw new Error(`Unable to parse Firefox version ${version3}`);
32993
+ }
32994
+ return Number.parseInt(major2, 10);
32995
+ }
32996
+ function compareVersions(versionA, versionB) {
32997
+ const majorA = getMajorVersion(versionA);
32998
+ const majorB = getMajorVersion(versionB);
32999
+ if (majorA < majorB) {
33000
+ return -1;
33001
+ }
33002
+ if (majorA > majorB) {
33003
+ return 1;
33004
+ }
33005
+ return 0;
33006
+ }
33007
+ var init_version = __esm({
33008
+ "src/utils/version.ts"() {
33009
+ "use strict";
33010
+ }
33011
+ });
33012
+
33013
+ // src/tools/debugging.ts
33014
+ function requireDebuggingSupport(firefox3) {
33015
+ const version3 = firefox3.getFirefoxVersion();
33016
+ if (version3 !== null && compareVersions(version3, MIN_VERSION) < 0) {
33017
+ throw new Error(
33018
+ `moz:debugging requires Firefox ${MIN_VERSION}+, current version is ${version3}`
33019
+ );
33020
+ }
33021
+ }
33022
+ function requireContext(contextId) {
33023
+ if (!contextId) {
33024
+ throw new Error("No active browsing context");
33025
+ }
33026
+ return contextId;
33027
+ }
33028
+ async function handleEnableDebugger(_args) {
33029
+ try {
33030
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33031
+ const firefox3 = await getFirefox2();
33032
+ requireDebuggingSupport(firefox3);
33033
+ await firefox3.sendBiDiCommand("moz:debugging.setDebuggerEnabled", { enabled: true });
33034
+ return successResponse("Debugger enabled");
33035
+ } catch (error2) {
33036
+ return errorResponse(error2);
33037
+ }
33038
+ }
33039
+ async function handleListScripts(_args) {
33040
+ try {
33041
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33042
+ const firefox3 = await getFirefox2();
33043
+ requireDebuggingSupport(firefox3);
33044
+ const contextId = requireContext(firefox3.getCurrentContextId());
33045
+ const result = await firefox3.sendBiDiCommand("moz:debugging.listScripts", {
33046
+ context: contextId
33047
+ });
33048
+ const scripts = result.scripts;
33049
+ if (scripts.length === 0) {
33050
+ return successResponse("No scripts found");
33051
+ }
33052
+ return successResponse(scripts.join("\n"));
33053
+ } catch (error2) {
33054
+ return errorResponse(error2);
33055
+ }
33056
+ }
33057
+ async function handleGetScriptSource(args2) {
33058
+ try {
33059
+ const { scriptUrl } = args2;
33060
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33061
+ const firefox3 = await getFirefox2();
33062
+ requireDebuggingSupport(firefox3);
33063
+ const contextId = requireContext(firefox3.getCurrentContextId());
33064
+ const result = await firefox3.sendBiDiCommand("moz:debugging.getScriptSource", {
33065
+ context: contextId,
33066
+ scriptUrl
33067
+ });
33068
+ return successResponse(result.source);
33069
+ } catch (error2) {
33070
+ return errorResponse(error2);
33071
+ }
33072
+ }
33073
+ async function handleSetLogpoint(args2) {
33074
+ try {
33075
+ const { url: url2, line, expression } = args2;
33076
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33077
+ const firefox3 = await getFirefox2();
33078
+ requireDebuggingSupport(firefox3);
33079
+ const logpointId = await firefox3.setLogpoint(url2, line, expression);
33080
+ return successResponse(`Logpoint set (id: ${logpointId})`);
33081
+ } catch (error2) {
33082
+ return errorResponse(error2);
33083
+ }
33084
+ }
33085
+ async function handleRemoveLogpoint(args2) {
33086
+ try {
33087
+ const { logpoint } = args2;
33088
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33089
+ const firefox3 = await getFirefox2();
33090
+ requireDebuggingSupport(firefox3);
33091
+ await firefox3.removeLogpoint(logpoint);
33092
+ return successResponse("Logpoint removed");
33093
+ } catch (error2) {
33094
+ return errorResponse(error2);
33095
+ }
33096
+ }
33097
+ async function handleGetLogpointResults(args2) {
33098
+ try {
33099
+ const { logpoint } = args2;
33100
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33101
+ const firefox3 = await getFirefox2();
33102
+ requireDebuggingSupport(firefox3);
33103
+ const results = firefox3.getLogpointResults(logpoint);
33104
+ if (results === null) {
33105
+ return errorResponse(new Error(`Logpoint ${logpoint} not found`));
33106
+ }
33107
+ if (results.length === 0) {
33108
+ return successResponse("No results collected yet");
33109
+ }
33110
+ const lines = results.map((r, i) => {
33111
+ if (r.error) {
33112
+ return `[${i + 1}] Error: ${r.error}`;
33113
+ }
33114
+ return `[${i + 1}] ${JSON.stringify(remoteValueToNative(r.value))}`;
33115
+ });
33116
+ return successResponse(lines.join("\n"));
33117
+ } catch (error2) {
33118
+ return errorResponse(error2);
33119
+ }
33120
+ }
33121
+ var MIN_VERSION, enableDebuggerTool, listScriptsTool, getScriptSourceTool, setLogpointTool, removeLogpointTool, getLogpointResultsTool;
33122
+ var init_debugging2 = __esm({
33123
+ "src/tools/debugging.ts"() {
33124
+ "use strict";
33125
+ init_response_helpers();
33126
+ init_version();
33127
+ init_remote_value();
33128
+ MIN_VERSION = "153";
33129
+ enableDebuggerTool = {
33130
+ name: "enable_debugger",
33131
+ description: "Enable the JS debugger for the current page. Required before set_logpoint works. Requires Firefox 153+.",
33132
+ inputSchema: { type: "object", properties: {} }
33133
+ };
33134
+ listScriptsTool = {
33135
+ name: "list_scripts",
33136
+ description: "List all JavaScript files currently loaded in the page. Requires enable_debugger to have been called.",
33137
+ inputSchema: { type: "object", properties: {} }
33138
+ };
33139
+ getScriptSourceTool = {
33140
+ name: "get_script_source",
33141
+ description: "Get the source code of a JavaScript file loaded in the page. Requires enable_debugger to have been called.",
33142
+ inputSchema: {
33143
+ type: "object",
33144
+ properties: {
33145
+ scriptUrl: { type: "string", description: "URL of the script to retrieve." }
33146
+ },
33147
+ required: ["scriptUrl"]
33148
+ }
33149
+ };
33150
+ setLogpointTool = {
33151
+ name: "set_logpoint",
33152
+ description: "Set a logpoint at a specific location. When execution reaches that line, the expression is evaluated and the result is stored without pausing. Use get_logpoint_results to retrieve collected values. Requires enable_debugger to have been called.",
33153
+ inputSchema: {
33154
+ type: "object",
33155
+ properties: {
33156
+ url: { type: "string", description: "URL of the script." },
33157
+ line: { type: "number", description: "Line number (1-based)." },
33158
+ expression: {
33159
+ type: "string",
33160
+ description: "JavaScript expression to evaluate each time the logpoint is hit."
33161
+ }
33162
+ },
33163
+ required: ["url", "line", "expression"]
33164
+ }
33165
+ };
33166
+ removeLogpointTool = {
33167
+ name: "remove_logpoint",
33168
+ description: "Remove a previously set logpoint.",
33169
+ inputSchema: {
33170
+ type: "object",
33171
+ properties: {
33172
+ logpoint: { type: "string", description: "Logpoint id returned by set_logpoint." }
33173
+ },
33174
+ required: ["logpoint"]
33175
+ }
33176
+ };
33177
+ getLogpointResultsTool = {
33178
+ name: "get_logpoint_results",
33179
+ description: "Get the results collected by a logpoint since it was set.",
33180
+ inputSchema: {
33181
+ type: "object",
33182
+ properties: {
33183
+ logpoint: { type: "string", description: "Logpoint id returned by set_logpoint." }
33184
+ },
33185
+ required: ["logpoint"]
33186
+ }
33187
+ };
33188
+ }
33189
+ });
33190
+
33191
+ // src/tools/profiler.ts
33192
+ function checkProfilerSupported(firefox3) {
33193
+ const version3 = firefox3.getFirefoxVersion();
33194
+ if (version3 !== null && compareVersions(version3, MIN_FIREFOX_VERSION) < 0) {
33195
+ throw new Error(
33196
+ `moz:profiler requires Firefox ${MIN_FIREFOX_VERSION.split(".")[0]} or later (connected: ${version3})`
33197
+ );
33198
+ }
33199
+ }
33200
+ async function handleProfilerIsActive(_args) {
33201
+ try {
33202
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33203
+ const firefox3 = await getFirefox2();
33204
+ checkProfilerSupported(firefox3);
33205
+ const result = await firefox3.sendBiDiCommand("moz:profiler.isActive", {});
33206
+ return successResponse(`Profiler is ${result.active ? "active" : "inactive"}`);
33207
+ } catch (error2) {
33208
+ return errorResponse(error2);
33209
+ }
33210
+ }
33211
+ async function handleProfilerStart(args2) {
33212
+ try {
33213
+ const { preset, entries, interval, features, threads, activeContext } = args2;
33214
+ const params = {};
33215
+ if (preset !== void 0) {
33216
+ params.preset = preset;
33217
+ } else {
33218
+ if (entries === void 0 || interval === void 0 || features === void 0 || threads === void 0) {
33219
+ throw new Error(
33220
+ "When no preset is given, entries, interval, features, and threads are all required."
33221
+ );
33222
+ }
33223
+ params.entries = entries;
33224
+ params.interval = interval;
33225
+ params.features = features;
33226
+ params.threads = threads;
33227
+ }
33228
+ if (activeContext !== void 0) {
33229
+ params.activeContext = activeContext;
33230
+ }
33231
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33232
+ const firefox3 = await getFirefox2();
33233
+ checkProfilerSupported(firefox3);
33234
+ await firefox3.sendBiDiCommand("moz:profiler.start", params);
33235
+ return successResponse("Profiler started");
33236
+ } catch (error2) {
33237
+ return errorResponse(error2);
33238
+ }
33239
+ }
33240
+ async function handleProfilerStop(args2) {
33241
+ try {
33242
+ const { discard } = args2;
33243
+ const params = {};
33244
+ if (discard !== void 0) {
33245
+ params.discard = discard;
33246
+ }
33247
+ const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
33248
+ const firefox3 = await getFirefox2();
33249
+ checkProfilerSupported(firefox3);
33250
+ const result = await firefox3.sendBiDiCommand("moz:profiler.stop", params);
33251
+ if (result.path) {
33252
+ return successResponse(`Profile saved to: ${result.path}`);
33253
+ }
33254
+ return successResponse("Profiler stopped. No profile was saved.");
33255
+ } catch (error2) {
33256
+ return errorResponse(error2);
33257
+ }
33258
+ }
33259
+ var MIN_FIREFOX_VERSION, VALID_PRESETS, profilerIsActiveTool, profilerStartTool, profilerStopTool;
33260
+ var init_profiler = __esm({
33261
+ "src/tools/profiler.ts"() {
33262
+ "use strict";
33263
+ init_response_helpers();
33264
+ init_version();
33265
+ MIN_FIREFOX_VERSION = "154.0";
33266
+ VALID_PRESETS = [
33267
+ "web-developer",
33268
+ "firefox-platform",
33269
+ "graphics",
33270
+ "media",
33271
+ "ml",
33272
+ "networking",
33273
+ "power",
33274
+ "debug"
33275
+ ];
33276
+ profilerIsActiveTool = {
33277
+ name: "profiler_is_active",
33278
+ description: "Check whether the Firefox profiler is currently recording.",
33279
+ inputSchema: {
33280
+ type: "object",
33281
+ properties: {}
33282
+ }
33283
+ };
33284
+ profilerStartTool = {
33285
+ name: "profiler_start",
33286
+ description: `Start the Firefox profiler. Provide either a preset name or explicit recording options (entries, interval, features, threads). Cannot combine both. Valid presets: ${VALID_PRESETS.join(", ")}.`,
33287
+ inputSchema: {
33288
+ type: "object",
33289
+ properties: {
33290
+ preset: {
33291
+ type: "string",
33292
+ enum: VALID_PRESETS,
33293
+ description: "Profiler preset name. Cannot be combined with entries, interval, features, or threads."
33294
+ },
33295
+ entries: {
33296
+ type: "integer",
33297
+ description: "Number of entries to keep in the sampling buffer. Required when no preset is given."
33298
+ },
33299
+ interval: {
33300
+ type: "number",
33301
+ description: "Sampling interval in milliseconds. Required when no preset is given."
33302
+ },
33303
+ features: {
33304
+ type: "array",
33305
+ items: { type: "string" },
33306
+ description: "Profiler features to enable. Required when no preset is given."
33307
+ },
33308
+ threads: {
33309
+ type: "array",
33310
+ items: { type: "string" },
33311
+ description: "Thread names to profile. Required when no preset is given."
33312
+ },
33313
+ activeContext: {
33314
+ type: "string",
33315
+ description: "Id of the top-level navigable to mark as the active tab in the profile. Does not restrict profiling to that tab."
33316
+ }
33317
+ }
33318
+ }
33319
+ };
33320
+ profilerStopTool = {
33321
+ name: "profiler_stop",
33322
+ description: "Stop the Firefox profiler and save the recorded profile to a file in the downloads directory. Returns the path to the saved file, or null when nothing was saved.",
33323
+ inputSchema: {
33324
+ type: "object",
33325
+ properties: {
33326
+ discard: {
33327
+ type: "boolean",
33328
+ description: "If true, stop the profiler and discard the recording instead of saving it to disk. Defaults to false."
33329
+ }
33330
+ }
33331
+ }
33332
+ };
33333
+ }
33334
+ });
33335
+
32557
33336
  // src/tools/index.ts
32558
33337
  var init_tools = __esm({
32559
33338
  "src/tools/index.ts"() {
@@ -32570,6 +33349,8 @@ var init_tools = __esm({
32570
33349
  init_privileged_context();
32571
33350
  init_firefox_prefs();
32572
33351
  init_webextension();
33352
+ init_debugging2();
33353
+ init_profiler();
32573
33354
  }
32574
33355
  });
32575
33356
 
@@ -32588,9 +33369,9 @@ import { version as version2 } from "process";
32588
33369
  import { fileURLToPath as fileURLToPath2 } from "url";
32589
33370
  import { resolve as resolve3 } from "path";
32590
33371
  import { realpathSync } from "fs";
32591
- function resetFirefox() {
33372
+ async function resetFirefox() {
32592
33373
  if (firefox2) {
32593
- firefox2.reset();
33374
+ await firefox2.close();
32594
33375
  firefox2 = null;
32595
33376
  }
32596
33377
  pendingWarning = null;
@@ -32611,7 +33392,7 @@ async function getFirefox() {
32611
33392
  const isConnected = await firefox2.isConnected();
32612
33393
  if (!isConnected) {
32613
33394
  log("Firefox connection lost, reconnecting...");
32614
- resetFirefox();
33395
+ await resetFirefox();
32615
33396
  } else {
32616
33397
  return firefox2;
32617
33398
  }
@@ -32659,8 +33440,7 @@ async function getFirefox() {
32659
33440
  pendingWarning = firefox2.getAndClearProfileWarning();
32660
33441
  return firefox2;
32661
33442
  } catch (error2) {
32662
- await firefox2.close().catch(() => {
32663
- });
33443
+ await firefox2.close();
32664
33444
  firefox2 = null;
32665
33445
  throw error2;
32666
33446
  }
@@ -32680,6 +33460,9 @@ async function run(parseArgsFn, importMetaUrl) {
32680
33460
  return;
32681
33461
  }
32682
33462
  args = parseArgsFn(SERVER_VERSION);
33463
+ if (args.logFile) {
33464
+ setupLogFile(args.logFile);
33465
+ }
32683
33466
  const toolHandlers = new Map([
32684
33467
  // Pages
32685
33468
  ["list_pages", handleListPages],
@@ -32719,8 +33502,21 @@ async function run(parseArgsFn, importMetaUrl) {
32719
33502
  // WebExtensions (install/uninstall use standard BiDi, no privileged context required)
32720
33503
  ["install_extension", handleInstallExtension],
32721
33504
  ["uninstall_extension", handleUninstallExtension],
33505
+ // Profiler
33506
+ ["profiler_is_active", handleProfilerIsActive],
33507
+ ["profiler_start", handleProfilerStart],
33508
+ ["profiler_stop", handleProfilerStop],
32722
33509
  // Script evaluation — requires --enable-script
32723
33510
  ...args.enableScript ? [["evaluate_script", handleEvaluateScript]] : [],
33511
+ // Debugging tools — requires --enable-script
33512
+ ...args.enableScript ? [
33513
+ ["enable_debugger", handleEnableDebugger],
33514
+ ["list_scripts", handleListScripts],
33515
+ ["get_script_source", handleGetScriptSource],
33516
+ ["set_logpoint", handleSetLogpoint],
33517
+ ["remove_logpoint", handleRemoveLogpoint],
33518
+ ["get_logpoint_results", handleGetLogpointResults]
33519
+ ] : [],
32724
33520
  // Privileged context tools — requires --enable-privileged-context
32725
33521
  ...args.enablePrivilegedContext ? [
32726
33522
  ["list_privileged_contexts", handleListPrivilegedContexts],
@@ -32761,8 +33557,20 @@ async function run(parseArgsFn, importMetaUrl) {
32761
33557
  restartFirefoxTool,
32762
33558
  installExtensionTool,
32763
33559
  uninstallExtensionTool,
33560
+ profilerIsActiveTool,
33561
+ profilerStartTool,
33562
+ profilerStopTool,
32764
33563
  // Script evaluation — requires --enable-script
32765
33564
  ...args.enableScript ? [evaluateScriptTool] : [],
33565
+ // Debugging tools — requires --enable-script
33566
+ ...args.enableScript ? [
33567
+ enableDebuggerTool,
33568
+ listScriptsTool,
33569
+ getScriptSourceTool,
33570
+ setLogpointTool,
33571
+ removeLogpointTool,
33572
+ getLogpointResultsTool
33573
+ ] : [],
32766
33574
  // Privileged context tools — requires --enable-privileged-context
32767
33575
  ...args.enablePrivilegedContext ? [
32768
33576
  listPrivilegedContextsTool,
@@ -32835,13 +33643,10 @@ async function run(parseArgsFn, importMetaUrl) {
32835
33643
  log("Firefox DevTools MCP server running on stdio");
32836
33644
  log("Ready to accept tool requests");
32837
33645
  const cleanup = async () => {
32838
- if (firefox2) {
32839
- try {
32840
- await firefox2.close();
32841
- } catch {
32842
- }
32843
- }
33646
+ await resetFirefox();
32844
33647
  await server.close();
33648
+ await flushLogs().catch(() => {
33649
+ });
32845
33650
  process.exit(0);
32846
33651
  };
32847
33652
  const onSignal = () => void cleanup();
@@ -32947,7 +33752,7 @@ var require_package = __commonJS({
32947
33752
  var require_main = __commonJS({
32948
33753
  "node_modules/dotenv/lib/main.js"(exports, module) {
32949
33754
  "use strict";
32950
- var fs = __require("fs");
33755
+ var fs2 = __require("fs");
32951
33756
  var path = __require("path");
32952
33757
  var os = __require("os");
32953
33758
  var crypto = __require("crypto");
@@ -33086,7 +33891,7 @@ var require_main = __commonJS({
33086
33891
  if (options && options.path && options.path.length > 0) {
33087
33892
  if (Array.isArray(options.path)) {
33088
33893
  for (const filepath of options.path) {
33089
- if (fs.existsSync(filepath)) {
33894
+ if (fs2.existsSync(filepath)) {
33090
33895
  possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
33091
33896
  }
33092
33897
  }
@@ -33096,7 +33901,7 @@ var require_main = __commonJS({
33096
33901
  } else {
33097
33902
  possibleVaultPath = path.resolve(process.cwd(), ".env.vault");
33098
33903
  }
33099
- if (fs.existsSync(possibleVaultPath)) {
33904
+ if (fs2.existsSync(possibleVaultPath)) {
33100
33905
  return possibleVaultPath;
33101
33906
  }
33102
33907
  return null;
@@ -33149,7 +33954,7 @@ var require_main = __commonJS({
33149
33954
  const parsedAll = {};
33150
33955
  for (const path2 of optionPaths) {
33151
33956
  try {
33152
- const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding }));
33957
+ const parsed = DotenvModule.parse(fs2.readFileSync(path2, { encoding }));
33153
33958
  DotenvModule.populate(parsedAll, parsed, options);
33154
33959
  } catch (e) {
33155
33960
  if (debug) {