@mozilla/firefox-devtools-mcp 0.9.5 → 0.9.7

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
@@ -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: {
@@ -27224,12 +27228,12 @@ var require_dist = __commonJS({
27224
27228
  throw new Error(`Unknown format "${name}"`);
27225
27229
  return f;
27226
27230
  };
27227
- function addFormats(ajv, list, fs, exportName) {
27231
+ function addFormats(ajv, list, fs2, exportName) {
27228
27232
  var _a2;
27229
27233
  var _b;
27230
27234
  (_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
27231
27235
  for (const f of list)
27232
- ajv.addFormat(f, fs[f]);
27236
+ ajv.addFormat(f, fs2[f]);
27233
27237
  }
27234
27238
  module.exports = exports = formatsPlugin;
27235
27239
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -28078,27 +28082,76 @@ var init_constants = __esm({
28078
28082
  });
28079
28083
 
28080
28084
  // src/utils/logger.ts
28081
- function log(message, ...args2) {
28082
- 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(" ");
28083
28100
  }
28084
- function logError(message, error2) {
28085
- if (error2 instanceof Error) {
28086
- console.error(`[firefox-devtools-mcp] ERROR: ${message}`, error2.message);
28087
- if (error2.stack) {
28088
- 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
+ `);
28089
28120
  }
28090
28121
  } else {
28091
- console.error(`[firefox-devtools-mcp] ERROR: ${message}`, error2);
28122
+ console.error(message, ...args2);
28123
+ if (body) {
28124
+ console.error(body);
28125
+ }
28092
28126
  }
28093
28127
  }
28128
+ function log(message, ...args2) {
28129
+ write(`[firefox-devtools-mcp] ${message}`, args2);
28130
+ }
28094
28131
  function logDebug(message, ...args2) {
28095
28132
  if (process.env.DEBUG === "*" || process.env.DEBUG?.includes("firefox-devtools")) {
28096
- console.error(`[firefox-devtools-mcp] DEBUG: ${message}`, ...args2);
28133
+ write(`[firefox-devtools-mcp] DEBUG: ${message}`, args2);
28134
+ }
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]);
28097
28141
  }
28098
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;
28099
28151
  var init_logger = __esm({
28100
28152
  "src/utils/logger.ts"() {
28101
28153
  "use strict";
28154
+ logStream = null;
28102
28155
  }
28103
28156
  });
28104
28157
 
@@ -28210,11 +28263,12 @@ var init_core3 = __esm({
28210
28263
  constructor(options) {
28211
28264
  this.options = options;
28212
28265
  }
28213
- driver = null;
28214
28266
  currentContextId = null;
28215
- originalEnv = {};
28216
- logFilePath;
28267
+ driver = null;
28268
+ firefoxVersion = null;
28217
28269
  logFileFd;
28270
+ logFilePath;
28271
+ originalEnv = {};
28218
28272
  profileWarning = null;
28219
28273
  /**
28220
28274
  * Launch Firefox (or connect to an existing instance) and establish BiDi connection
@@ -28306,6 +28360,9 @@ var init_core3 = __esm({
28306
28360
  for (const [name, value] of Object.entries(this.options.prefs)) {
28307
28361
  firefoxOptions.setPreference(name, value);
28308
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
+ }
28309
28366
  }
28310
28367
  let serviceBuilder;
28311
28368
  if (process.platform === "win32") {
@@ -28320,11 +28377,18 @@ var init_core3 = __esm({
28320
28377
  serviceBuilder.setStdio(["ignore", this.logFileFd, this.logFileFd]);
28321
28378
  log(`Capturing Firefox output to: ${this.logFilePath}`);
28322
28379
  }
28380
+ const remoteLogLevel = this.options.prefs?.["remote.log.level"];
28381
+ if (remoteLogLevel && typeof remoteLogLevel === "string") {
28382
+ serviceBuilder.addArguments("--log", remoteLogLevel.toLowerCase());
28383
+ }
28323
28384
  this.driver = await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(firefoxOptions).setFirefoxService(serviceBuilder).build();
28324
28385
  }
28325
28386
  log(
28326
28387
  this.options.connectExisting ? "Connected to existing Firefox" : "Firefox launched with BiDi"
28327
28388
  );
28389
+ const driverCapabilities = await this.driver.getCapabilities();
28390
+ this.firefoxVersion = driverCapabilities.get("browserVersion") ?? null;
28391
+ logDebug(`Browser version: ${this.firefoxVersion}`);
28328
28392
  this.currentContextId = await this.driver.getWindowHandle();
28329
28393
  logDebug(`Browsing context ID: ${this.currentContextId}`);
28330
28394
  if (this.options.startUrl && !this.options.connectExisting) {
@@ -28358,24 +28422,6 @@ var init_core3 = __esm({
28358
28422
  return false;
28359
28423
  }
28360
28424
  }
28361
- /**
28362
- * Reset driver state (used when Firefox is detected as closed)
28363
- */
28364
- reset() {
28365
- if (this.driver) {
28366
- const d = this.driver;
28367
- if (d._bidiConnection) {
28368
- d._bidiConnection.close();
28369
- d._bidiConnection = void 0;
28370
- }
28371
- if ("quit" in this.driver) {
28372
- void this.driver.quit();
28373
- }
28374
- }
28375
- this.driver = null;
28376
- this.currentContextId = null;
28377
- logDebug("Driver state reset");
28378
- }
28379
28425
  /**
28380
28426
  * Get current browsing context ID
28381
28427
  */
@@ -28388,6 +28434,12 @@ var init_core3 = __esm({
28388
28434
  setCurrentContextId(contextId) {
28389
28435
  this.currentContextId = contextId;
28390
28436
  }
28437
+ /**
28438
+ * Get the current firefox version, as a string (eg "153.0a1")
28439
+ */
28440
+ getFirefoxVersion() {
28441
+ return this.firefoxVersion;
28442
+ }
28391
28443
  /**
28392
28444
  * Get log file path
28393
28445
  */
@@ -28473,20 +28525,47 @@ var init_core3 = __esm({
28473
28525
  }
28474
28526
  /**
28475
28527
  * Close driver and cleanup.
28476
- * When connected to an existing Firefox instance, only kills geckodriver
28477
- * 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.
28478
28531
  */
28479
28532
  async close() {
28480
- if (this.driver) {
28481
- const d = this.driver;
28482
- if (d._bidiConnection) {
28483
- d._bidiConnection.close();
28484
- 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;
28485
28548
  }
28486
- if ("quit" in this.driver) {
28487
- 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);
28488
28568
  }
28489
- this.driver = null;
28490
28569
  }
28491
28570
  if (this.logFileFd !== void 0) {
28492
28571
  try {
@@ -28876,12 +28955,135 @@ var init_network = __esm({
28876
28955
  }
28877
28956
  });
28878
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
+
28879
29080
  // src/firefox/events/index.ts
28880
29081
  var init_events = __esm({
28881
29082
  "src/firefox/events/index.ts"() {
28882
29083
  "use strict";
28883
29084
  init_console();
28884
29085
  init_network();
29086
+ init_debugging();
28885
29087
  }
28886
29088
  });
28887
29089
 
@@ -29793,6 +29995,7 @@ var init_firefox = __esm({
29793
29995
  core;
29794
29996
  consoleEvents = null;
29795
29997
  networkEvents = null;
29998
+ debuggingEvents = null;
29796
29999
  dom = null;
29797
30000
  pages = null;
29798
30001
  snapshot = null;
@@ -29821,6 +30024,10 @@ var init_firefox = __esm({
29821
30024
  onNavigate,
29822
30025
  autoClearOnNavigate: false
29823
30026
  });
30027
+ this.debuggingEvents = new DebuggingEvents(
30028
+ driver,
30029
+ (method, params) => this.core.sendBiDiCommand(method, params)
30030
+ );
29824
30031
  }
29825
30032
  this.dom = new DomInteractions(
29826
30033
  driver,
@@ -29848,6 +30055,14 @@ var init_firefox = __esm({
29848
30055
  this.networkEvents = null;
29849
30056
  }
29850
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
+ }
29851
30066
  }
29852
30067
  // ============================================================================
29853
30068
  // DOM / Evaluate
@@ -30141,6 +30356,48 @@ var init_firefox = __esm({
30141
30356
  async isConnected() {
30142
30357
  return await this.core.isConnected();
30143
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
+ }
30144
30401
  /**
30145
30402
  * Get log file path (if logging is enabled)
30146
30403
  */
@@ -30160,23 +30417,22 @@ var init_firefox = __esm({
30160
30417
  getOptions() {
30161
30418
  return this.core.getOptions();
30162
30419
  }
30163
- /**
30164
- * Reset all internal state (used when Firefox is detected as closed)
30165
- */
30166
- reset() {
30167
- 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
+ }
30168
30429
  this.consoleEvents = null;
30169
30430
  this.networkEvents = null;
30431
+ this.debuggingEvents = null;
30170
30432
  this.dom = null;
30171
30433
  this.pages = null;
30172
30434
  this.snapshot = null;
30173
30435
  }
30174
- // ============================================================================
30175
- // Cleanup
30176
- // ============================================================================
30177
- async close() {
30178
- await this.core.close();
30179
- }
30180
30436
  };
30181
30437
  }
30182
30438
  });
@@ -30458,7 +30714,68 @@ var init_pages2 = __esm({
30458
30714
  }
30459
30715
  });
30460
30716
 
30461
- // 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
30462
30779
  function validateFunction(fnString) {
30463
30780
  if (!fnString || typeof fnString !== "string") {
30464
30781
  throw new Error("function parameter is required and must be a string");
@@ -30482,6 +30799,15 @@ Valid examples:
30482
30799
  );
30483
30800
  }
30484
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
30485
30811
  async function handleEvaluateScript(args2) {
30486
30812
  try {
30487
30813
  const {
@@ -30492,17 +30818,13 @@ async function handleEvaluateScript(args2) {
30492
30818
  validateFunction(fnString);
30493
30819
  const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
30494
30820
  const firefox3 = await getFirefox2();
30495
- const driver = firefox3.getDriver();
30496
- if (!driver) {
30497
- throw new Error("WebDriver not available");
30498
- }
30499
30821
  const scriptTimeout = timeout ?? DEFAULT_TIMEOUT;
30500
30822
  const resolvedArgs = [];
30501
30823
  if (fnArgs && fnArgs.length > 0) {
30502
30824
  for (const arg of fnArgs) {
30503
30825
  try {
30504
30826
  const element = await firefox3.resolveUidToElement(arg.uid);
30505
- resolvedArgs.push(element);
30827
+ resolvedArgs.push({ sharedId: await element.getId() });
30506
30828
  } catch (error2) {
30507
30829
  const errorMsg = error2.message;
30508
30830
  if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
@@ -30517,40 +30839,55 @@ Please call take_snapshot to get fresh UIDs and try again.`
30517
30839
  }
30518
30840
  }
30519
30841
  }
30520
- const evalCode = `
30521
- const fn = ${fnString};
30522
- const args = Array.from(arguments);
30523
- const result = fn(...args);
30524
- return result instanceof Promise ? result : Promise.resolve(result);
30525
- `;
30526
- await driver.manage().setTimeouts({ script: scriptTimeout });
30527
- const result = await driver.executeScript(evalCode, ...resolvedArgs);
30528
- let output = "Script ran on page and returned:\n";
30529
- output += "```json\n";
30530
- output += JSON.stringify(result, null, 2);
30531
- output += "\n```";
30532
- return successResponse(output);
30533
- } catch (error2) {
30534
- const errorMsg = error2.message;
30535
- if (errorMsg.includes("timeout") || errorMsg.includes("Timeout")) {
30536
- 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) {
30537
30853
  return errorResponse(
30538
30854
  new Error(
30539
- `Script execution timed out (exceeded ${timeoutValue}ms).
30855
+ `Script execution timed out (exceeded ${scriptTimeout}ms).
30540
30856
 
30541
30857
  The function may contain an infinite loop or be waiting for a slow operation.
30542
30858
  Try simplifying the script or increasing the timeout parameter.`
30543
30859
  )
30544
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}`);
30545
30879
  }
30880
+ } catch (error2) {
30546
30881
  return errorResponse(error2);
30547
30882
  }
30548
30883
  }
30549
- var evaluateScriptTool, MAX_FUNCTION_SIZE, DEFAULT_TIMEOUT;
30884
+ var evaluateScriptTool, DEFAULT_TIMEOUT, TIMEOUT, EvaluateResultType;
30550
30885
  var init_script = __esm({
30551
30886
  "src/tools/script.ts"() {
30552
30887
  "use strict";
30553
30888
  init_response_helpers();
30889
+ init_remote_value();
30890
+ init_js_validation();
30554
30891
  evaluateScriptTool = {
30555
30892
  name: "evaluate_script",
30556
30893
  description: "Execute JS function in page. Prefer UID tools for interactions.",
@@ -30583,8 +30920,12 @@ var init_script = __esm({
30583
30920
  required: ["function"]
30584
30921
  }
30585
30922
  };
30586
- MAX_FUNCTION_SIZE = 16 * 1024;
30587
30923
  DEFAULT_TIMEOUT = 5e3;
30924
+ TIMEOUT = Symbol("Timeout");
30925
+ EvaluateResultType = {
30926
+ Exception: "exception",
30927
+ Success: "success"
30928
+ };
30588
30929
  }
30589
30930
  });
30590
30931
 
@@ -31797,10 +32138,12 @@ async function handleGetFirefoxInfo(_input) {
31797
32138
  const firefox3 = await getFirefox();
31798
32139
  const options = firefox3.getOptions();
31799
32140
  const logFilePath = firefox3.getLogFilePath();
32141
+ const version3 = firefox3.getFirefoxVersion();
31800
32142
  const info = [];
31801
32143
  info.push("Firefox Instance Configuration");
31802
32144
  info.push("");
31803
32145
  info.push(`Binary: ${options.firefoxPath ?? "System Firefox (default)"}`);
32146
+ info.push(`Firefox version: ${version3 ?? "(unknown)"}`);
31804
32147
  info.push(`Headless: ${options.headless ? "Yes" : "No"}`);
31805
32148
  if (options.viewport) {
31806
32149
  info.push(`Viewport: ${options.viewport.width}x${options.viewport.height}`);
@@ -31873,11 +32216,7 @@ async function handleRestartFirefox(input) {
31873
32216
  prefs: mergedPrefs
31874
32217
  };
31875
32218
  setNextLaunchOptions(newOptions);
31876
- try {
31877
- await currentFirefox.close();
31878
- } catch {
31879
- }
31880
- resetFirefox();
32219
+ await resetFirefox();
31881
32220
  const changes = [];
31882
32221
  if (firefoxPath && firefoxPath !== currentOptions.firefoxPath) {
31883
32222
  changes.push(`Binary: ${firefoxPath}`);
@@ -31908,7 +32247,7 @@ ${changes.join("\n")}`
31908
32247
  );
31909
32248
  } else {
31910
32249
  if (currentFirefox) {
31911
- resetFirefox();
32250
+ await resetFirefox();
31912
32251
  }
31913
32252
  const resolvedFirefoxPath = firefoxPath ?? args.firefoxPath ?? void 0;
31914
32253
  if (!resolvedFirefoxPath) {
@@ -32030,10 +32369,6 @@ var init_firefox_management = __esm({
32030
32369
  });
32031
32370
 
32032
32371
  // src/tools/privileged-context.ts
32033
- function isLikelyStatement(input) {
32034
- const trimmed = input.trim();
32035
- return /^(const|let|var)\s/.test(trimmed);
32036
- }
32037
32372
  function formatContextList(contexts) {
32038
32373
  if (contexts.length === 0) {
32039
32374
  return "No privileged contexts found";
@@ -32096,41 +32431,46 @@ async function handleSelectPrivilegedContext(args2) {
32096
32431
  }
32097
32432
  async function handleEvaluatePrivilegedScript(args2) {
32098
32433
  try {
32099
- const { expression } = args2;
32100
- if (!expression || typeof expression !== "string") {
32101
- throw new Error("expression parameter is required and must be a string");
32102
- }
32103
- if (isLikelyStatement(expression)) {
32104
- return errorResponse(
32105
- new Error(
32106
- `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; })()`
32107
- )
32108
- );
32109
- }
32434
+ const { function: fnString } = args2;
32435
+ validateFunction(fnString);
32110
32436
  const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
32111
32437
  const firefox3 = await getFirefox2();
32112
- const driver = firefox3.getDriver();
32113
- try {
32114
- const result = await driver.executeScript(`return (${expression});`);
32115
- const resultText = typeof result === "string" ? result : result === null ? "null" : result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
32116
- return successResponse(`Result:
32117
- ${resultText}`);
32118
- } 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;
32119
32452
  return errorResponse(
32120
32453
  new Error(
32121
- `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```"
32122
32458
  )
32123
32459
  );
32460
+ } else {
32461
+ return errorResponse(`Unexpected script.callFunction result type: ${result.type}`);
32124
32462
  }
32125
32463
  } catch (error2) {
32126
32464
  return errorResponse(error2);
32127
32465
  }
32128
32466
  }
32129
- var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool;
32467
+ var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool, EvaluateResultType2;
32130
32468
  var init_privileged_context = __esm({
32131
32469
  "src/tools/privileged-context.ts"() {
32132
32470
  "use strict";
32133
32471
  init_response_helpers();
32472
+ init_js_validation();
32473
+ init_remote_value();
32134
32474
  listPrivilegedContextsTool = {
32135
32475
  name: "list_privileged_contexts",
32136
32476
  description: "List privileged (privileged) browsing contexts. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Use restart_firefox with env parameter to enable.",
@@ -32155,18 +32495,22 @@ var init_privileged_context = __esm({
32155
32495
  };
32156
32496
  evaluatePrivilegedScriptTool = {
32157
32497
  name: "evaluate_privileged_script",
32158
- 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.",
32159
32499
  inputSchema: {
32160
32500
  type: "object",
32161
32501
  properties: {
32162
- expression: {
32502
+ function: {
32163
32503
  type: "string",
32164
- description: "JavaScript expression to evaluate in the privileged context"
32504
+ description: 'JS function string, e.g. () => Services.prefs.getBoolPref("foo")'
32165
32505
  }
32166
32506
  },
32167
- required: ["expression"]
32507
+ required: ["function"]
32168
32508
  }
32169
32509
  };
32510
+ EvaluateResultType2 = {
32511
+ Exception: "exception",
32512
+ Success: "success"
32513
+ };
32170
32514
  }
32171
32515
  });
32172
32516
 
@@ -32641,6 +32985,354 @@ var init_webextension = __esm({
32641
32985
  }
32642
32986
  });
32643
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
+
32644
33336
  // src/tools/index.ts
32645
33337
  var init_tools = __esm({
32646
33338
  "src/tools/index.ts"() {
@@ -32657,6 +33349,8 @@ var init_tools = __esm({
32657
33349
  init_privileged_context();
32658
33350
  init_firefox_prefs();
32659
33351
  init_webextension();
33352
+ init_debugging2();
33353
+ init_profiler();
32660
33354
  }
32661
33355
  });
32662
33356
 
@@ -32675,9 +33369,9 @@ import { version as version2 } from "process";
32675
33369
  import { fileURLToPath as fileURLToPath2 } from "url";
32676
33370
  import { resolve as resolve3 } from "path";
32677
33371
  import { realpathSync } from "fs";
32678
- function resetFirefox() {
33372
+ async function resetFirefox() {
32679
33373
  if (firefox2) {
32680
- firefox2.reset();
33374
+ await firefox2.close();
32681
33375
  firefox2 = null;
32682
33376
  }
32683
33377
  pendingWarning = null;
@@ -32698,7 +33392,7 @@ async function getFirefox() {
32698
33392
  const isConnected = await firefox2.isConnected();
32699
33393
  if (!isConnected) {
32700
33394
  log("Firefox connection lost, reconnecting...");
32701
- resetFirefox();
33395
+ await resetFirefox();
32702
33396
  } else {
32703
33397
  return firefox2;
32704
33398
  }
@@ -32746,8 +33440,7 @@ async function getFirefox() {
32746
33440
  pendingWarning = firefox2.getAndClearProfileWarning();
32747
33441
  return firefox2;
32748
33442
  } catch (error2) {
32749
- await firefox2.close().catch(() => {
32750
- });
33443
+ await firefox2.close();
32751
33444
  firefox2 = null;
32752
33445
  throw error2;
32753
33446
  }
@@ -32767,6 +33460,9 @@ async function run(parseArgsFn, importMetaUrl) {
32767
33460
  return;
32768
33461
  }
32769
33462
  args = parseArgsFn(SERVER_VERSION);
33463
+ if (args.logFile) {
33464
+ setupLogFile(args.logFile);
33465
+ }
32770
33466
  const toolHandlers = new Map([
32771
33467
  // Pages
32772
33468
  ["list_pages", handleListPages],
@@ -32806,8 +33502,21 @@ async function run(parseArgsFn, importMetaUrl) {
32806
33502
  // WebExtensions (install/uninstall use standard BiDi, no privileged context required)
32807
33503
  ["install_extension", handleInstallExtension],
32808
33504
  ["uninstall_extension", handleUninstallExtension],
33505
+ // Profiler
33506
+ ["profiler_is_active", handleProfilerIsActive],
33507
+ ["profiler_start", handleProfilerStart],
33508
+ ["profiler_stop", handleProfilerStop],
32809
33509
  // Script evaluation — requires --enable-script
32810
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
+ ] : [],
32811
33520
  // Privileged context tools — requires --enable-privileged-context
32812
33521
  ...args.enablePrivilegedContext ? [
32813
33522
  ["list_privileged_contexts", handleListPrivilegedContexts],
@@ -32848,8 +33557,20 @@ async function run(parseArgsFn, importMetaUrl) {
32848
33557
  restartFirefoxTool,
32849
33558
  installExtensionTool,
32850
33559
  uninstallExtensionTool,
33560
+ profilerIsActiveTool,
33561
+ profilerStartTool,
33562
+ profilerStopTool,
32851
33563
  // Script evaluation — requires --enable-script
32852
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
+ ] : [],
32853
33574
  // Privileged context tools — requires --enable-privileged-context
32854
33575
  ...args.enablePrivilegedContext ? [
32855
33576
  listPrivilegedContextsTool,
@@ -32922,13 +33643,10 @@ async function run(parseArgsFn, importMetaUrl) {
32922
33643
  log("Firefox DevTools MCP server running on stdio");
32923
33644
  log("Ready to accept tool requests");
32924
33645
  const cleanup = async () => {
32925
- if (firefox2) {
32926
- try {
32927
- await firefox2.close();
32928
- } catch {
32929
- }
32930
- }
33646
+ await resetFirefox();
32931
33647
  await server.close();
33648
+ await flushLogs().catch(() => {
33649
+ });
32932
33650
  process.exit(0);
32933
33651
  };
32934
33652
  const onSignal = () => void cleanup();
@@ -33034,7 +33752,7 @@ var require_package = __commonJS({
33034
33752
  var require_main = __commonJS({
33035
33753
  "node_modules/dotenv/lib/main.js"(exports, module) {
33036
33754
  "use strict";
33037
- var fs = __require("fs");
33755
+ var fs2 = __require("fs");
33038
33756
  var path = __require("path");
33039
33757
  var os = __require("os");
33040
33758
  var crypto = __require("crypto");
@@ -33173,7 +33891,7 @@ var require_main = __commonJS({
33173
33891
  if (options && options.path && options.path.length > 0) {
33174
33892
  if (Array.isArray(options.path)) {
33175
33893
  for (const filepath of options.path) {
33176
- if (fs.existsSync(filepath)) {
33894
+ if (fs2.existsSync(filepath)) {
33177
33895
  possibleVaultPath = filepath.endsWith(".vault") ? filepath : `${filepath}.vault`;
33178
33896
  }
33179
33897
  }
@@ -33183,7 +33901,7 @@ var require_main = __commonJS({
33183
33901
  } else {
33184
33902
  possibleVaultPath = path.resolve(process.cwd(), ".env.vault");
33185
33903
  }
33186
- if (fs.existsSync(possibleVaultPath)) {
33904
+ if (fs2.existsSync(possibleVaultPath)) {
33187
33905
  return possibleVaultPath;
33188
33906
  }
33189
33907
  return null;
@@ -33236,7 +33954,7 @@ var require_main = __commonJS({
33236
33954
  const parsedAll = {};
33237
33955
  for (const path2 of optionPaths) {
33238
33956
  try {
33239
- const parsed = DotenvModule.parse(fs.readFileSync(path2, { encoding }));
33957
+ const parsed = DotenvModule.parse(fs2.readFileSync(path2, { encoding }));
33240
33958
  DotenvModule.populate(parsedAll, parsed, options);
33241
33959
  } catch (e) {
33242
33960
  if (debug) {