firefox-devtools-mcp 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16821,10 +16821,10 @@ var require_validate = __commonJS({
16821
16821
  return;
16822
16822
  }
16823
16823
  }
16824
- validateFunction(it, () => (0, boolSchema_1.topBoolOrEmptySchema)(it));
16824
+ validateFunction2(it, () => (0, boolSchema_1.topBoolOrEmptySchema)(it));
16825
16825
  }
16826
16826
  exports.validateFunctionCode = validateFunctionCode;
16827
- function validateFunction({ gen, validateName, schema, schemaEnv, opts }, body) {
16827
+ function validateFunction2({ gen, validateName, schema, schemaEnv, opts }, body) {
16828
16828
  if (opts.code.es5) {
16829
16829
  gen.func(validateName, (0, codegen_1._)`${names_1.default.data}, ${names_1.default.valCxt}`, schemaEnv.$async, () => {
16830
16830
  gen.code((0, codegen_1._)`"use strict"; ${funcSourceUrl(schema, opts)}`);
@@ -16857,7 +16857,7 @@ var require_validate = __commonJS({
16857
16857
  }
16858
16858
  function topSchemaObjCode(it) {
16859
16859
  const { schema, opts, gen } = it;
16860
- validateFunction(it, () => {
16860
+ validateFunction2(it, () => {
16861
16861
  if (opts.$comment && schema.$comment)
16862
16862
  commentKeyword(it);
16863
16863
  checkNoDefault(it);
@@ -21995,6 +21995,32 @@ var init_logger = __esm({
21995
21995
  // src/cli.ts
21996
21996
  import yargs from "yargs";
21997
21997
  import { hideBin } from "yargs/helpers";
21998
+ function parsePrefs(prefs) {
21999
+ const result = {};
22000
+ if (!prefs || prefs.length === 0) {
22001
+ return result;
22002
+ }
22003
+ for (const pref of prefs) {
22004
+ const eqIndex = pref.indexOf("=");
22005
+ if (eqIndex === -1) {
22006
+ continue;
22007
+ }
22008
+ const name = pref.slice(0, eqIndex);
22009
+ const rawValue = pref.slice(eqIndex + 1);
22010
+ let value;
22011
+ if (rawValue === "true") {
22012
+ value = true;
22013
+ } else if (rawValue === "false") {
22014
+ value = false;
22015
+ } else if (/^-?\d+$/.test(rawValue)) {
22016
+ value = parseInt(rawValue, 10);
22017
+ } else {
22018
+ value = rawValue;
22019
+ }
22020
+ result[name] = value;
22021
+ }
22022
+ return result;
22023
+ }
21998
22024
  function parseArguments(version3, argv = process.argv) {
21999
22025
  const yargsInstance = yargs(hideBin(argv)).scriptName("npx firefox-devtools-mcp@latest").options(cliOptions).example([
22000
22026
  [
@@ -22066,6 +22092,30 @@ var init_cli = __esm({
22066
22092
  type: "number",
22067
22093
  description: "Marionette port to connect to when using --connect-existing (default: 2828)",
22068
22094
  default: Number(process.env.MARIONETTE_PORT ?? "2828")
22095
+ },
22096
+ env: {
22097
+ type: "array",
22098
+ description: "Environment variables for Firefox in KEY=VALUE format. Can be specified multiple times. Example: --env MOZ_LOG=HTMLMediaElement:4"
22099
+ },
22100
+ outputFile: {
22101
+ type: "string",
22102
+ description: "Path to file where Firefox output (stdout/stderr) will be written. If not specified, output is written to ~/.firefox-devtools-mcp/output/"
22103
+ },
22104
+ pref: {
22105
+ type: "array",
22106
+ string: true,
22107
+ description: "Set Firefox preference at startup via moz:firefoxOptions (format: name=value). Can be specified multiple times.",
22108
+ alias: "p"
22109
+ },
22110
+ enableScript: {
22111
+ type: "boolean",
22112
+ description: "Enable the evaluate_script tool, which allows executing arbitrary JavaScript in the page context.",
22113
+ default: (process.env.ENABLE_SCRIPT ?? "false") === "true"
22114
+ },
22115
+ enablePrivilegedContext: {
22116
+ type: "boolean",
22117
+ description: "Enable privileged context tools: list/select privileged contexts, evaluate privileged scripts, get/set Firefox prefs, and list extensions. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1.",
22118
+ default: (process.env.ENABLE_PRIVILEGED_CONTEXT ?? "false") === "true"
22069
22119
  }
22070
22120
  };
22071
22121
  }
@@ -22075,6 +22125,9 @@ var init_cli = __esm({
22075
22125
  import { Builder, Browser } from "selenium-webdriver";
22076
22126
  import firefox from "selenium-webdriver/firefox.js";
22077
22127
  import { spawn } from "child_process";
22128
+ import { mkdirSync, openSync, closeSync } from "fs";
22129
+ import { homedir } from "os";
22130
+ import { join } from "path";
22078
22131
  function findGeckodriverInCache(fs, path, cacheBase) {
22079
22132
  const ext = process.platform === "win32" ? ".exe" : "";
22080
22133
  const binaryName = `geckodriver${ext}`;
@@ -22387,6 +22440,9 @@ var init_core3 = __esm({
22387
22440
  kill() {
22388
22441
  this.gdProcess.kill();
22389
22442
  }
22443
+ getBidi() {
22444
+ throw new Error("BiDi not available in connect-existing mode");
22445
+ }
22390
22446
  };
22391
22447
  FirefoxCore = class {
22392
22448
  constructor(options) {
@@ -22394,6 +22450,9 @@ var init_core3 = __esm({
22394
22450
  }
22395
22451
  driver = null;
22396
22452
  currentContextId = null;
22453
+ originalEnv = {};
22454
+ logFilePath;
22455
+ logFileFd;
22397
22456
  /**
22398
22457
  * Launch Firefox (or connect to an existing instance) and establish BiDi connection
22399
22458
  */
@@ -22407,6 +22466,24 @@ var init_core3 = __esm({
22407
22466
  const port = this.options.marionettePort ?? 2828;
22408
22467
  this.driver = await GeckodriverHttpDriver.connect(port);
22409
22468
  } else {
22469
+ if (this.options.logFile) {
22470
+ this.logFilePath = this.options.logFile;
22471
+ } else if (this.options.env && Object.keys(this.options.env).length > 0) {
22472
+ const outputDir = join(homedir(), ".firefox-devtools-mcp", "output");
22473
+ mkdirSync(outputDir, { recursive: true });
22474
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
22475
+ this.logFilePath = join(outputDir, `firefox-${timestamp}.log`);
22476
+ }
22477
+ if (this.options.env) {
22478
+ for (const [key, value] of Object.entries(this.options.env)) {
22479
+ this.originalEnv[key] = process.env[key];
22480
+ process.env[key] = value;
22481
+ logDebug(`Set env ${key}=${value}`);
22482
+ }
22483
+ if (this.options.env.MOZ_LOG_FILE) {
22484
+ logDebug("Note: MOZ_LOG_FILE in env will be used, but may be blocked by sandbox");
22485
+ }
22486
+ }
22410
22487
  const firefoxOptions = new firefox.Options();
22411
22488
  firefoxOptions.enableBidi();
22412
22489
  if (this.options.headless) {
@@ -22425,12 +22502,24 @@ var init_core3 = __esm({
22425
22502
  firefoxOptions.addArguments(...this.options.args);
22426
22503
  }
22427
22504
  if (this.options.profilePath) {
22428
- firefoxOptions.setProfile(this.options.profilePath);
22505
+ firefoxOptions.addArguments("--profile", this.options.profilePath);
22506
+ log(`\u{1F4C1} Using Firefox profile: ${this.options.profilePath}`);
22429
22507
  }
22430
22508
  if (this.options.acceptInsecureCerts) {
22431
22509
  firefoxOptions.setAcceptInsecureCerts(true);
22432
22510
  }
22433
- this.driver = await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(firefoxOptions).build();
22511
+ if (this.options.prefs) {
22512
+ for (const [name, value] of Object.entries(this.options.prefs)) {
22513
+ firefoxOptions.setPreference(name, value);
22514
+ }
22515
+ }
22516
+ const serviceBuilder = new firefox.ServiceBuilder();
22517
+ if (this.logFilePath) {
22518
+ this.logFileFd = openSync(this.logFilePath, "a");
22519
+ serviceBuilder.setStdio(["ignore", this.logFileFd, this.logFileFd]);
22520
+ log(`\u{1F4DD} Capturing Firefox output to: ${this.logFilePath}`);
22521
+ }
22522
+ this.driver = await new Builder().forBrowser(Browser.FIREFOX).setFirefoxOptions(firefoxOptions).setFirefoxService(serviceBuilder).build();
22434
22523
  }
22435
22524
  log(
22436
22525
  this.options.connectExisting ? "\u2705 Connected to existing Firefox" : "\u2705 Firefox launched with BiDi"
@@ -22491,6 +22580,80 @@ var init_core3 = __esm({
22491
22580
  setCurrentContextId(contextId) {
22492
22581
  this.currentContextId = contextId;
22493
22582
  }
22583
+ /**
22584
+ * Get log file path
22585
+ */
22586
+ getLogFilePath() {
22587
+ return this.logFilePath;
22588
+ }
22589
+ /**
22590
+ * Get current launch options
22591
+ */
22592
+ getOptions() {
22593
+ return this.options;
22594
+ }
22595
+ /**
22596
+ * Wait for WebSocket to be in OPEN state
22597
+ */
22598
+ async waitForWebSocketOpen(ws, timeout = 5e3) {
22599
+ if (ws.readyState === 1) {
22600
+ return;
22601
+ }
22602
+ if (ws.readyState === 0) {
22603
+ return new Promise((resolve4, reject) => {
22604
+ const timeoutId = setTimeout(() => {
22605
+ ws.off("open", onOpen);
22606
+ reject(new Error("Timeout waiting for WebSocket to open"));
22607
+ }, timeout);
22608
+ const onOpen = () => {
22609
+ clearTimeout(timeoutId);
22610
+ ws.off("open", onOpen);
22611
+ resolve4();
22612
+ };
22613
+ ws.on("open", onOpen);
22614
+ });
22615
+ }
22616
+ throw new Error(`WebSocket is not open: readyState ${ws.readyState}`);
22617
+ }
22618
+ /**
22619
+ * Send raw BiDi command and get response
22620
+ */
22621
+ async sendBiDiCommand(method, params = {}) {
22622
+ if (!this.driver) {
22623
+ throw new Error("Driver not connected");
22624
+ }
22625
+ const bidi = await this.driver.getBidi();
22626
+ const ws = bidi.socket;
22627
+ await this.waitForWebSocketOpen(ws);
22628
+ const id = Math.floor(Math.random() * 1e6);
22629
+ return new Promise((resolve4, reject) => {
22630
+ const messageHandler = (data) => {
22631
+ try {
22632
+ const payload = JSON.parse(data.toString());
22633
+ if (payload.id === id) {
22634
+ ws.off("message", messageHandler);
22635
+ if (payload.error) {
22636
+ reject(new Error(`BiDi error: ${JSON.stringify(payload.error)}`));
22637
+ } else {
22638
+ resolve4(payload.result);
22639
+ }
22640
+ }
22641
+ } catch (err) {
22642
+ }
22643
+ };
22644
+ ws.on("message", messageHandler);
22645
+ const command = {
22646
+ id,
22647
+ method,
22648
+ params
22649
+ };
22650
+ ws.send(JSON.stringify(command));
22651
+ setTimeout(() => {
22652
+ ws.off("message", messageHandler);
22653
+ reject(new Error(`BiDi command timeout: ${method}`));
22654
+ }, 1e4);
22655
+ });
22656
+ }
22494
22657
  /**
22495
22658
  * Close driver and cleanup.
22496
22659
  * When connected to an existing Firefox instance, only kills geckodriver
@@ -22505,6 +22668,25 @@ var init_core3 = __esm({
22505
22668
  }
22506
22669
  this.driver = null;
22507
22670
  }
22671
+ if (this.logFileFd !== void 0) {
22672
+ try {
22673
+ closeSync(this.logFileFd);
22674
+ logDebug("Log file closed");
22675
+ } catch (error2) {
22676
+ logDebug(
22677
+ `Error closing log file: ${error2 instanceof Error ? error2.message : String(error2)}`
22678
+ );
22679
+ }
22680
+ this.logFileFd = void 0;
22681
+ }
22682
+ for (const [key, value] of Object.entries(this.originalEnv)) {
22683
+ if (value === void 0) {
22684
+ delete process.env[key];
22685
+ } else {
22686
+ process.env[key] = value;
22687
+ }
22688
+ }
22689
+ this.originalEnv = {};
22508
22690
  log("\u2705 Firefox DevTools closed");
22509
22691
  }
22510
22692
  };
@@ -24060,6 +24242,13 @@ var init_firefox = __esm({
24060
24242
  // ============================================================================
24061
24243
  // Internal / Advanced
24062
24244
  // ============================================================================
24245
+ /**
24246
+ * Send raw BiDi command (for advanced operations)
24247
+ * @internal
24248
+ */
24249
+ async sendBiDiCommand(method, params = {}) {
24250
+ return await this.core.sendBiDiCommand(method, params);
24251
+ }
24063
24252
  /**
24064
24253
  * Get WebDriver instance (for advanced operations)
24065
24254
  * @internal
@@ -24067,6 +24256,13 @@ var init_firefox = __esm({
24067
24256
  getDriver() {
24068
24257
  return this.core.getDriver();
24069
24258
  }
24259
+ /**
24260
+ * Get current browsing context ID (for advanced operations)
24261
+ * @internal
24262
+ */
24263
+ getCurrentContextId() {
24264
+ return this.core.getCurrentContextId();
24265
+ }
24070
24266
  /**
24071
24267
  * Check if Firefox is still connected and responsive
24072
24268
  * Returns false if Firefox was closed or connection is broken
@@ -24074,6 +24270,18 @@ var init_firefox = __esm({
24074
24270
  async isConnected() {
24075
24271
  return await this.core.isConnected();
24076
24272
  }
24273
+ /**
24274
+ * Get log file path (if logging is enabled)
24275
+ */
24276
+ getLogFilePath() {
24277
+ return this.core.getLogFilePath();
24278
+ }
24279
+ /**
24280
+ * Get current launch options
24281
+ */
24282
+ getOptions() {
24283
+ return this.core.getOptions();
24284
+ }
24077
24285
  /**
24078
24286
  * Reset all internal state (used when Firefox is detected as closed)
24079
24287
  */
@@ -24372,6 +24580,136 @@ var init_pages2 = __esm({
24372
24580
  }
24373
24581
  });
24374
24582
 
24583
+ // src/tools/script.ts
24584
+ function validateFunction(fnString) {
24585
+ if (!fnString || typeof fnString !== "string") {
24586
+ throw new Error("function parameter is required and must be a string");
24587
+ }
24588
+ if (fnString.length > MAX_FUNCTION_SIZE) {
24589
+ throw new Error(
24590
+ `Function too large (${fnString.length} bytes, max ${MAX_FUNCTION_SIZE} bytes). This tool is not designed for massive scripts.`
24591
+ );
24592
+ }
24593
+ const trimmed = fnString.trim();
24594
+ const isFunctionLike = trimmed.startsWith("function") || trimmed.startsWith("async function") || trimmed.startsWith("(") || trimmed.startsWith("async (");
24595
+ if (!isFunctionLike) {
24596
+ throw new Error(
24597
+ `Invalid function format. Expected a function or arrow function, got: "${trimmed.substring(0, 50)}...".
24598
+
24599
+ Valid examples:
24600
+ () => document.title
24601
+ async () => { return await fetch("/api") }
24602
+ (el) => el.innerText
24603
+ function() { return window.location.href }`
24604
+ );
24605
+ }
24606
+ }
24607
+ async function handleEvaluateScript(args2) {
24608
+ try {
24609
+ const {
24610
+ function: fnString,
24611
+ args: fnArgs,
24612
+ timeout
24613
+ } = args2;
24614
+ validateFunction(fnString);
24615
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
24616
+ const firefox3 = await getFirefox2();
24617
+ const driver = firefox3.getDriver();
24618
+ if (!driver) {
24619
+ throw new Error("WebDriver not available");
24620
+ }
24621
+ const scriptTimeout = timeout ?? DEFAULT_TIMEOUT;
24622
+ const resolvedArgs = [];
24623
+ if (fnArgs && fnArgs.length > 0) {
24624
+ for (const arg of fnArgs) {
24625
+ try {
24626
+ const element = await firefox3.resolveUidToElement(arg.uid);
24627
+ resolvedArgs.push(element);
24628
+ } catch (error2) {
24629
+ const errorMsg = error2.message;
24630
+ if (errorMsg.includes("stale") || errorMsg.includes("Snapshot") || errorMsg.includes("UID")) {
24631
+ throw new Error(
24632
+ `UID "${arg.uid}" is invalid or from an old snapshot.
24633
+
24634
+ The page may have changed since the snapshot was taken.
24635
+ Please call take_snapshot to get fresh UIDs and try again.`
24636
+ );
24637
+ }
24638
+ throw new Error(`Failed to resolve UID "${arg.uid}": ${errorMsg}`);
24639
+ }
24640
+ }
24641
+ }
24642
+ const evalCode = `
24643
+ const fn = ${fnString};
24644
+ const args = Array.from(arguments);
24645
+ const result = fn(...args);
24646
+ return result instanceof Promise ? result : Promise.resolve(result);
24647
+ `;
24648
+ await driver.manage().setTimeouts({ script: scriptTimeout });
24649
+ const result = await driver.executeScript(evalCode, ...resolvedArgs);
24650
+ let output = "Script ran on page and returned:\n";
24651
+ output += "```json\n";
24652
+ output += JSON.stringify(result, null, 2);
24653
+ output += "\n```";
24654
+ return successResponse(output);
24655
+ } catch (error2) {
24656
+ const errorMsg = error2.message;
24657
+ if (errorMsg.includes("timeout") || errorMsg.includes("Timeout")) {
24658
+ const timeoutValue = args2?.timeout ?? DEFAULT_TIMEOUT;
24659
+ return errorResponse(
24660
+ new Error(
24661
+ `Script execution timed out (exceeded ${timeoutValue}ms).
24662
+
24663
+ The function may contain an infinite loop or be waiting for a slow operation.
24664
+ Try simplifying the script or increasing the timeout parameter.`
24665
+ )
24666
+ );
24667
+ }
24668
+ return errorResponse(error2);
24669
+ }
24670
+ }
24671
+ var evaluateScriptTool, MAX_FUNCTION_SIZE, DEFAULT_TIMEOUT;
24672
+ var init_script = __esm({
24673
+ "src/tools/script.ts"() {
24674
+ "use strict";
24675
+ init_response_helpers();
24676
+ evaluateScriptTool = {
24677
+ name: "evaluate_script",
24678
+ description: "Execute JS function in page. Prefer UID tools for interactions.",
24679
+ inputSchema: {
24680
+ type: "object",
24681
+ properties: {
24682
+ function: {
24683
+ type: "string",
24684
+ description: "JS function string, e.g. () => document.title"
24685
+ },
24686
+ args: {
24687
+ type: "array",
24688
+ description: "UIDs to pass as function arguments",
24689
+ items: {
24690
+ type: "object",
24691
+ properties: {
24692
+ uid: {
24693
+ type: "string",
24694
+ description: "Element UID from snapshot"
24695
+ }
24696
+ },
24697
+ required: ["uid"]
24698
+ }
24699
+ },
24700
+ timeout: {
24701
+ type: "number",
24702
+ description: "Timeout in ms (default: 5000)"
24703
+ }
24704
+ },
24705
+ required: ["function"]
24706
+ }
24707
+ };
24708
+ MAX_FUNCTION_SIZE = 16 * 1024;
24709
+ DEFAULT_TIMEOUT = 5e3;
24710
+ }
24711
+ });
24712
+
24375
24713
  // src/tools/console.ts
24376
24714
  async function handleListConsoleMessages(args2) {
24377
24715
  try {
@@ -25533,17 +25871,909 @@ var init_utilities = __esm({
25533
25871
  }
25534
25872
  });
25535
25873
 
25874
+ // src/tools/firefox-management.ts
25875
+ import { readFileSync as readFileSync2, existsSync, statSync } from "fs";
25876
+ async function handleGetFirefoxLogs(input) {
25877
+ try {
25878
+ const {
25879
+ lines = 100,
25880
+ grep,
25881
+ since
25882
+ } = input;
25883
+ const firefox3 = await getFirefox();
25884
+ const logFilePath = firefox3.getLogFilePath();
25885
+ if (!logFilePath) {
25886
+ return successResponse(
25887
+ "No output capture configured. Use --env to set environment variables or --output-file to enable output capture."
25888
+ );
25889
+ }
25890
+ if (!existsSync(logFilePath)) {
25891
+ return successResponse(`Output file not found: ${logFilePath}`);
25892
+ }
25893
+ if (since !== void 0) {
25894
+ const stats = statSync(logFilePath);
25895
+ const ageSeconds = (Date.now() - stats.mtimeMs) / 1e3;
25896
+ if (ageSeconds > since) {
25897
+ return successResponse(
25898
+ `Output file is ${Math.floor(ageSeconds)}s old, but only output from last ${since}s was requested. File may not have recent entries.`
25899
+ );
25900
+ }
25901
+ }
25902
+ const content = readFileSync2(logFilePath, "utf-8");
25903
+ let allLines = content.split("\n").filter((line) => line.trim().length > 0);
25904
+ if (grep) {
25905
+ const grepLower = grep.toLowerCase();
25906
+ allLines = allLines.filter((line) => line.toLowerCase().includes(grepLower));
25907
+ }
25908
+ const maxLines = Math.min(lines, 1e4);
25909
+ const recentLines = allLines.slice(-maxLines);
25910
+ const result = [
25911
+ `\u{1F4CB} Firefox Output File: ${logFilePath}`,
25912
+ `Total lines in file: ${allLines.length}`,
25913
+ grep ? `Lines matching "${grep}": ${allLines.length}` : "",
25914
+ `Showing last ${recentLines.length} lines:`,
25915
+ "",
25916
+ "\u2500".repeat(80),
25917
+ recentLines.join("\n")
25918
+ ].filter(Boolean).join("\n");
25919
+ return successResponse(result);
25920
+ } catch (error2) {
25921
+ return errorResponse(error2);
25922
+ }
25923
+ }
25924
+ async function handleGetFirefoxInfo(_input) {
25925
+ try {
25926
+ const firefox3 = await getFirefox();
25927
+ const options = firefox3.getOptions();
25928
+ const logFilePath = firefox3.getLogFilePath();
25929
+ const info = [];
25930
+ info.push("\u{1F98A} Firefox Instance Configuration");
25931
+ info.push("");
25932
+ info.push(`Binary: ${options.firefoxPath ?? "System Firefox (default)"}`);
25933
+ info.push(`Headless: ${options.headless ? "Yes" : "No"}`);
25934
+ if (options.viewport) {
25935
+ info.push(`Viewport: ${options.viewport.width}x${options.viewport.height}`);
25936
+ }
25937
+ if (options.profilePath) {
25938
+ info.push(`Profile: ${options.profilePath}`);
25939
+ }
25940
+ if (options.startUrl) {
25941
+ info.push(`Start URL: ${options.startUrl}`);
25942
+ }
25943
+ if (options.args && options.args.length > 0) {
25944
+ info.push(`Arguments: ${options.args.join(" ")}`);
25945
+ }
25946
+ if (options.env && Object.keys(options.env).length > 0) {
25947
+ info.push("");
25948
+ info.push("Environment Variables:");
25949
+ for (const [key, value] of Object.entries(options.env)) {
25950
+ info.push(` ${key}=${value}`);
25951
+ }
25952
+ }
25953
+ if (options.prefs && Object.keys(options.prefs).length > 0) {
25954
+ info.push("");
25955
+ info.push("Preferences:");
25956
+ for (const [key, value] of Object.entries(options.prefs)) {
25957
+ info.push(` ${key} = ${JSON.stringify(value)}`);
25958
+ }
25959
+ }
25960
+ if (logFilePath) {
25961
+ info.push("");
25962
+ info.push(`Output File: ${logFilePath}`);
25963
+ if (existsSync(logFilePath)) {
25964
+ const stats = statSync(logFilePath);
25965
+ const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
25966
+ info.push(` Size: ${sizeMB} MB`);
25967
+ info.push(` Last Modified: ${stats.mtime.toISOString()}`);
25968
+ } else {
25969
+ info.push(" (file not created yet)");
25970
+ }
25971
+ }
25972
+ return successResponse(info.join("\n"));
25973
+ } catch (error2) {
25974
+ return errorResponse(error2);
25975
+ }
25976
+ }
25977
+ async function handleRestartFirefox(input) {
25978
+ try {
25979
+ const { firefoxPath, profilePath, env, headless, startUrl, prefs } = input;
25980
+ let newEnv;
25981
+ if (env && Array.isArray(env) && env.length > 0) {
25982
+ newEnv = {};
25983
+ for (const envStr of env) {
25984
+ const [key, ...valueParts] = envStr.split("=");
25985
+ if (key && valueParts.length > 0) {
25986
+ newEnv[key] = valueParts.join("=");
25987
+ }
25988
+ }
25989
+ }
25990
+ const currentFirefox = getFirefoxIfRunning();
25991
+ const isConnected = currentFirefox ? await currentFirefox.isConnected() : false;
25992
+ if (currentFirefox && isConnected) {
25993
+ const currentOptions = currentFirefox.getOptions();
25994
+ const mergedPrefs = prefs !== void 0 ? { ...currentOptions.prefs || {}, ...prefs } : currentOptions.prefs;
25995
+ const newOptions = {
25996
+ ...currentOptions,
25997
+ firefoxPath: firefoxPath ?? currentOptions.firefoxPath,
25998
+ profilePath: profilePath ?? currentOptions.profilePath,
25999
+ env: newEnv !== void 0 ? newEnv : currentOptions.env,
26000
+ headless: headless !== void 0 ? headless : currentOptions.headless,
26001
+ startUrl: startUrl ?? currentOptions.startUrl ?? "about:home",
26002
+ prefs: mergedPrefs
26003
+ };
26004
+ setNextLaunchOptions(newOptions);
26005
+ try {
26006
+ await currentFirefox.close();
26007
+ } catch (error2) {
26008
+ }
26009
+ resetFirefox();
26010
+ const changes = [];
26011
+ if (firefoxPath && firefoxPath !== currentOptions.firefoxPath) {
26012
+ changes.push(`Binary: ${firefoxPath}`);
26013
+ }
26014
+ if (profilePath && profilePath !== currentOptions.profilePath) {
26015
+ changes.push(`Profile: ${profilePath}`);
26016
+ }
26017
+ if (newEnv !== void 0 && JSON.stringify(newEnv) !== JSON.stringify(currentOptions.env)) {
26018
+ changes.push(`Environment variables updated:`);
26019
+ for (const [key, value] of Object.entries(newEnv)) {
26020
+ changes.push(` ${key}=${value}`);
26021
+ }
26022
+ }
26023
+ if (headless !== void 0 && headless !== currentOptions.headless) {
26024
+ changes.push(`Headless: ${headless ? "enabled" : "disabled"}`);
26025
+ }
26026
+ if (startUrl && startUrl !== currentOptions.startUrl) {
26027
+ changes.push(`Start URL: ${startUrl}`);
26028
+ }
26029
+ if (changes.length === 0) {
26030
+ return successResponse(
26031
+ "\u2705 Firefox closed. Will restart with same configuration on next tool call."
26032
+ );
26033
+ }
26034
+ return successResponse(
26035
+ `\u2705 Firefox closed. Will restart with new configuration on next tool call:
26036
+ ${changes.join("\n")}`
26037
+ );
26038
+ } else {
26039
+ if (currentFirefox) {
26040
+ resetFirefox();
26041
+ }
26042
+ const resolvedFirefoxPath = firefoxPath ?? args.firefoxPath ?? void 0;
26043
+ if (!resolvedFirefoxPath) {
26044
+ return errorResponse(
26045
+ new Error(
26046
+ "Firefox is not running and no firefoxPath provided. Please specify firefoxPath to start Firefox."
26047
+ )
26048
+ );
26049
+ }
26050
+ const newOptions = {
26051
+ firefoxPath: resolvedFirefoxPath,
26052
+ profilePath: profilePath ?? args.profilePath ?? void 0,
26053
+ env: newEnv,
26054
+ headless: headless ?? false,
26055
+ startUrl: startUrl ?? "about:home"
26056
+ };
26057
+ setNextLaunchOptions(newOptions);
26058
+ const config2 = [`Binary: ${resolvedFirefoxPath}`];
26059
+ const resolvedProfilePath = profilePath ?? args.profilePath;
26060
+ if (resolvedProfilePath) {
26061
+ config2.push(`Profile: ${resolvedProfilePath}`);
26062
+ }
26063
+ if (newEnv) {
26064
+ config2.push("Environment variables:");
26065
+ for (const [key, value] of Object.entries(newEnv)) {
26066
+ config2.push(` ${key}=${value}`);
26067
+ }
26068
+ }
26069
+ if (headless) {
26070
+ config2.push("Headless: enabled");
26071
+ }
26072
+ if (startUrl) {
26073
+ config2.push(`Start URL: ${startUrl}`);
26074
+ }
26075
+ return successResponse(
26076
+ `\u2705 Firefox configured. Will start on next tool call:
26077
+ ${config2.join("\n")}`
26078
+ );
26079
+ }
26080
+ } catch (error2) {
26081
+ return errorResponse(error2);
26082
+ }
26083
+ }
26084
+ var getFirefoxLogsTool, getFirefoxInfoTool, restartFirefoxTool;
26085
+ var init_firefox_management = __esm({
26086
+ async "src/tools/firefox-management.ts"() {
26087
+ "use strict";
26088
+ await init_index();
26089
+ init_response_helpers();
26090
+ getFirefoxLogsTool = {
26091
+ name: "get_firefox_output",
26092
+ description: "Retrieve Firefox output (stdout/stderr including MOZ_LOG, warnings, crashes, stack traces). Returns recent output from the capture file. Use filters to focus on specific content.",
26093
+ inputSchema: {
26094
+ type: "object",
26095
+ properties: {
26096
+ lines: {
26097
+ type: "number",
26098
+ description: "Number of recent log lines to return (default: 100, max: 10000)"
26099
+ },
26100
+ grep: {
26101
+ type: "string",
26102
+ description: "Filter log lines containing this string (case-insensitive)"
26103
+ },
26104
+ since: {
26105
+ type: "number",
26106
+ description: "Only show logs written in the last N seconds"
26107
+ }
26108
+ }
26109
+ }
26110
+ };
26111
+ getFirefoxInfoTool = {
26112
+ name: "get_firefox_info",
26113
+ description: "Get information about the current Firefox instance configuration, including binary path, environment variables, and output file location.",
26114
+ inputSchema: {
26115
+ type: "object",
26116
+ properties: {}
26117
+ }
26118
+ };
26119
+ restartFirefoxTool = {
26120
+ name: "restart_firefox",
26121
+ description: "Restart Firefox with different configuration. Allows changing binary path, environment variables, and other options. All current tabs will be closed.",
26122
+ inputSchema: {
26123
+ type: "object",
26124
+ properties: {
26125
+ firefoxPath: {
26126
+ type: "string",
26127
+ description: "New Firefox binary path (optional, keeps current if not specified)"
26128
+ },
26129
+ profilePath: {
26130
+ type: "string",
26131
+ description: "Firefox profile path (optional, keeps current if not specified)"
26132
+ },
26133
+ env: {
26134
+ type: "array",
26135
+ items: {
26136
+ type: "string"
26137
+ },
26138
+ description: 'New environment variables in KEY=VALUE format (optional, e.g., ["MOZ_LOG=HTMLMediaElement:5", "MOZ_LOG_FILE=/tmp/ff.log"])'
26139
+ },
26140
+ headless: {
26141
+ type: "boolean",
26142
+ description: "Run in headless mode (optional, keeps current if not specified)"
26143
+ },
26144
+ startUrl: {
26145
+ type: "string",
26146
+ description: "URL to navigate to after restart (optional, uses about:home if not specified)"
26147
+ },
26148
+ prefs: {
26149
+ type: "object",
26150
+ description: "Firefox preferences to set at startup. Values are auto-typed: true/false become booleans, integers become numbers, everything else is a string. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1.",
26151
+ additionalProperties: {
26152
+ oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }]
26153
+ }
26154
+ }
26155
+ }
26156
+ }
26157
+ };
26158
+ }
26159
+ });
26160
+
26161
+ // src/tools/privileged-context.ts
26162
+ function formatContextList(contexts) {
26163
+ if (contexts.length === 0) {
26164
+ return "\u{1F527} No privileged contexts found";
26165
+ }
26166
+ const lines = [`\u{1F527} ${contexts.length} privileged contexts`];
26167
+ for (const ctx of contexts) {
26168
+ const id = ctx.context;
26169
+ const url2 = ctx.url || "(no url)";
26170
+ const children = ctx.children ? ` [${ctx.children.length} children]` : "";
26171
+ lines.push(` ${id}: ${url2}${children}`);
26172
+ }
26173
+ return lines.join("\n");
26174
+ }
26175
+ async function handleListPrivilegedContexts(_args) {
26176
+ try {
26177
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26178
+ const firefox3 = await getFirefox2();
26179
+ const result = await firefox3.sendBiDiCommand("browsingContext.getTree", {
26180
+ "moz:scope": "chrome"
26181
+ });
26182
+ const contexts = result.contexts || [];
26183
+ return successResponse(formatContextList(contexts));
26184
+ } catch (error2) {
26185
+ if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
26186
+ return errorResponse(
26187
+ new Error(
26188
+ "Privileged context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox."
26189
+ )
26190
+ );
26191
+ }
26192
+ return errorResponse(error2);
26193
+ }
26194
+ }
26195
+ async function handleSelectPrivilegedContext(args2) {
26196
+ try {
26197
+ const { contextId } = args2;
26198
+ if (!contextId || typeof contextId !== "string") {
26199
+ throw new Error("contextId parameter is required and must be a string");
26200
+ }
26201
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26202
+ const firefox3 = await getFirefox2();
26203
+ const driver = firefox3.getDriver();
26204
+ await driver.switchTo().window(contextId);
26205
+ try {
26206
+ await driver.setContext("chrome");
26207
+ } catch (contextError) {
26208
+ return errorResponse(
26209
+ new Error(
26210
+ `Switched to context ${contextId} but failed to set Marionette privileged context. Your Firefox build may not support privileged context or MOZ_REMOTE_ALLOW_SYSTEM_ACCESS is not set.`
26211
+ )
26212
+ );
26213
+ }
26214
+ return successResponse(
26215
+ `\u2705 Switched to privileged context: ${contextId} (Marionette context set to privileged)`
26216
+ );
26217
+ } catch (error2) {
26218
+ return errorResponse(error2);
26219
+ }
26220
+ }
26221
+ async function handleEvaluatePrivilegedScript(args2) {
26222
+ try {
26223
+ const { expression } = args2;
26224
+ if (!expression || typeof expression !== "string") {
26225
+ throw new Error("expression parameter is required and must be a string");
26226
+ }
26227
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26228
+ const firefox3 = await getFirefox2();
26229
+ const driver = firefox3.getDriver();
26230
+ try {
26231
+ const result = await driver.executeScript(`return (${expression});`);
26232
+ const resultText = typeof result === "string" ? result : result === null ? "null" : result === void 0 ? "undefined" : JSON.stringify(result, null, 2);
26233
+ return successResponse(`\u{1F527} Result:
26234
+ ${resultText}`);
26235
+ } catch (executeError) {
26236
+ return errorResponse(
26237
+ new Error(
26238
+ `Script execution failed: ${executeError instanceof Error ? executeError.message : String(executeError)}`
26239
+ )
26240
+ );
26241
+ }
26242
+ } catch (error2) {
26243
+ return errorResponse(error2);
26244
+ }
26245
+ }
26246
+ var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool;
26247
+ var init_privileged_context = __esm({
26248
+ "src/tools/privileged-context.ts"() {
26249
+ "use strict";
26250
+ init_response_helpers();
26251
+ listPrivilegedContextsTool = {
26252
+ name: "list_privileged_contexts",
26253
+ description: "List privileged (privileged) browsing contexts. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Use restart_firefox with env parameter to enable.",
26254
+ inputSchema: {
26255
+ type: "object",
26256
+ properties: {}
26257
+ }
26258
+ };
26259
+ selectPrivilegedContextTool = {
26260
+ name: "select_privileged_context",
26261
+ description: 'Select a privileged browsing context by ID and set WebDriver Classic context to "chrome" . Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var.',
26262
+ inputSchema: {
26263
+ type: "object",
26264
+ properties: {
26265
+ contextId: {
26266
+ type: "string",
26267
+ description: "Privileged browsing context ID from list_privileged_contexts"
26268
+ }
26269
+ },
26270
+ required: ["contextId"]
26271
+ }
26272
+ };
26273
+ evaluatePrivilegedScriptTool = {
26274
+ name: "evaluate_privileged_script",
26275
+ description: "Evaluate JavaScript in the current privileged context. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Returns the result of the expression.",
26276
+ inputSchema: {
26277
+ type: "object",
26278
+ properties: {
26279
+ expression: {
26280
+ type: "string",
26281
+ description: "JavaScript expression to evaluate in the privileged context"
26282
+ }
26283
+ },
26284
+ required: ["expression"]
26285
+ }
26286
+ };
26287
+ }
26288
+ });
26289
+
26290
+ // src/firefox/pref-utils.ts
26291
+ function generatePrefScript(name, value) {
26292
+ const escapedName = JSON.stringify(name);
26293
+ if (typeof value === "boolean") {
26294
+ return `Services.prefs.setBoolPref(${escapedName}, ${value})`;
26295
+ } else if (typeof value === "number") {
26296
+ return `Services.prefs.setIntPref(${escapedName}, ${value})`;
26297
+ } else {
26298
+ return `Services.prefs.setStringPref(${escapedName}, ${JSON.stringify(value)})`;
26299
+ }
26300
+ }
26301
+ var init_pref_utils = __esm({
26302
+ "src/firefox/pref-utils.ts"() {
26303
+ "use strict";
26304
+ }
26305
+ });
26306
+
26307
+ // src/tools/firefox-prefs.ts
26308
+ async function handleSetFirefoxPrefs(args2) {
26309
+ try {
26310
+ const { prefs } = args2;
26311
+ if (!prefs || typeof prefs !== "object") {
26312
+ throw new Error("prefs parameter is required and must be an object");
26313
+ }
26314
+ const prefEntries = Object.entries(prefs);
26315
+ if (prefEntries.length === 0) {
26316
+ return successResponse("No preferences to set");
26317
+ }
26318
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26319
+ const firefox3 = await getFirefox2();
26320
+ const result = await firefox3.sendBiDiCommand("browsingContext.getTree", {
26321
+ "moz:scope": "chrome"
26322
+ });
26323
+ const contexts = result.contexts || [];
26324
+ if (contexts.length === 0) {
26325
+ throw new Error(
26326
+ "No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set."
26327
+ );
26328
+ }
26329
+ const driver = firefox3.getDriver();
26330
+ const chromeContextId = contexts[0].context;
26331
+ const originalContextId = firefox3.getCurrentContextId();
26332
+ try {
26333
+ await driver.switchTo().window(chromeContextId);
26334
+ await driver.setContext("chrome");
26335
+ const results = [];
26336
+ const errors = [];
26337
+ for (const [name, value] of prefEntries) {
26338
+ try {
26339
+ const script = generatePrefScript(name, value);
26340
+ await driver.executeScript(script);
26341
+ results.push(` ${name} = ${JSON.stringify(value)}`);
26342
+ } catch (error2) {
26343
+ errors.push(` ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
26344
+ }
26345
+ }
26346
+ const output = [];
26347
+ if (results.length > 0) {
26348
+ output.push(`\u2705 Set ${results.length} preference(s):`);
26349
+ output.push(...results);
26350
+ }
26351
+ if (errors.length > 0) {
26352
+ output.push(`
26353
+ \u26A0\uFE0F Failed to set ${errors.length} preference(s):`);
26354
+ output.push(...errors);
26355
+ }
26356
+ return successResponse(output.join("\n"));
26357
+ } finally {
26358
+ try {
26359
+ await driver.setContext("content");
26360
+ if (originalContextId) {
26361
+ await driver.switchTo().window(originalContextId);
26362
+ }
26363
+ } catch {
26364
+ }
26365
+ }
26366
+ } catch (error2) {
26367
+ if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
26368
+ return errorResponse(
26369
+ new Error(
26370
+ "Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox."
26371
+ )
26372
+ );
26373
+ }
26374
+ return errorResponse(error2);
26375
+ }
26376
+ }
26377
+ async function handleGetFirefoxPrefs(args2) {
26378
+ try {
26379
+ const { names } = args2;
26380
+ if (!names || !Array.isArray(names) || names.length === 0) {
26381
+ throw new Error("names parameter is required and must be a non-empty array");
26382
+ }
26383
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26384
+ const firefox3 = await getFirefox2();
26385
+ const result = await firefox3.sendBiDiCommand("browsingContext.getTree", {
26386
+ "moz:scope": "chrome"
26387
+ });
26388
+ const contexts = result.contexts || [];
26389
+ if (contexts.length === 0) {
26390
+ throw new Error(
26391
+ "No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set."
26392
+ );
26393
+ }
26394
+ const driver = firefox3.getDriver();
26395
+ const chromeContextId = contexts[0].context;
26396
+ const originalContextId = firefox3.getCurrentContextId();
26397
+ try {
26398
+ await driver.switchTo().window(chromeContextId);
26399
+ await driver.setContext("chrome");
26400
+ const results = [];
26401
+ const errors = [];
26402
+ for (const name of names) {
26403
+ try {
26404
+ const script = `
26405
+ (function() {
26406
+ const type = Services.prefs.getPrefType(${JSON.stringify(name)});
26407
+ if (type === Services.prefs.PREF_INVALID) {
26408
+ return { exists: false };
26409
+ } else if (type === Services.prefs.PREF_BOOL) {
26410
+ return { exists: true, value: Services.prefs.getBoolPref(${JSON.stringify(name)}) };
26411
+ } else if (type === Services.prefs.PREF_INT) {
26412
+ return { exists: true, value: Services.prefs.getIntPref(${JSON.stringify(name)}) };
26413
+ } else {
26414
+ return { exists: true, value: Services.prefs.getStringPref(${JSON.stringify(name)}) };
26415
+ }
26416
+ })()
26417
+ `;
26418
+ const prefResult = await driver.executeScript(`return ${script}`);
26419
+ if (prefResult.exists) {
26420
+ results.push(` ${name} = ${JSON.stringify(prefResult.value)}`);
26421
+ } else {
26422
+ results.push(` ${name} = (not set)`);
26423
+ }
26424
+ } catch (error2) {
26425
+ errors.push(` ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
26426
+ }
26427
+ }
26428
+ const output = [];
26429
+ if (results.length > 0) {
26430
+ output.push(`\u{1F4CB} Firefox Preferences:`);
26431
+ output.push(...results);
26432
+ }
26433
+ if (errors.length > 0) {
26434
+ output.push(`
26435
+ \u26A0\uFE0F Failed to read ${errors.length} preference(s):`);
26436
+ output.push(...errors);
26437
+ }
26438
+ return successResponse(output.join("\n"));
26439
+ } finally {
26440
+ try {
26441
+ await driver.setContext("content");
26442
+ if (originalContextId) {
26443
+ await driver.switchTo().window(originalContextId);
26444
+ }
26445
+ } catch {
26446
+ }
26447
+ }
26448
+ } catch (error2) {
26449
+ if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
26450
+ return errorResponse(
26451
+ new Error(
26452
+ "Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox."
26453
+ )
26454
+ );
26455
+ }
26456
+ return errorResponse(error2);
26457
+ }
26458
+ }
26459
+ var setFirefoxPrefsTool, getFirefoxPrefsTool;
26460
+ var init_firefox_prefs = __esm({
26461
+ "src/tools/firefox-prefs.ts"() {
26462
+ "use strict";
26463
+ init_response_helpers();
26464
+ init_pref_utils();
26465
+ setFirefoxPrefsTool = {
26466
+ name: "set_firefox_prefs",
26467
+ description: "Set Firefox preferences at runtime a privileged API. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var.",
26468
+ inputSchema: {
26469
+ type: "object",
26470
+ properties: {
26471
+ prefs: {
26472
+ type: "object",
26473
+ description: "Object mapping preference names to values. Values are auto-typed: true/false become booleans, integers become numbers, everything else is a string.",
26474
+ additionalProperties: {
26475
+ oneOf: [{ type: "string" }, { type: "number" }, { type: "boolean" }]
26476
+ }
26477
+ }
26478
+ },
26479
+ required: ["prefs"]
26480
+ }
26481
+ };
26482
+ getFirefoxPrefsTool = {
26483
+ name: "get_firefox_prefs",
26484
+ description: "Get Firefox preference values via a privileged API. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var.",
26485
+ inputSchema: {
26486
+ type: "object",
26487
+ properties: {
26488
+ names: {
26489
+ type: "array",
26490
+ items: { type: "string" },
26491
+ description: "Array of preference names to read"
26492
+ }
26493
+ },
26494
+ required: ["names"]
26495
+ }
26496
+ };
26497
+ }
26498
+ });
26499
+
26500
+ // src/tools/webextension.ts
26501
+ async function handleInstallExtension(args2) {
26502
+ try {
26503
+ const { type, path, value, permanent } = args2;
26504
+ if (!type) {
26505
+ throw new Error("type parameter is required");
26506
+ }
26507
+ if ((type === "archivePath" || type === "path") && !path) {
26508
+ throw new Error(`path parameter is required for type "${type}"`);
26509
+ }
26510
+ if (type === "base64" && !value) {
26511
+ throw new Error('value parameter is required for type "base64"');
26512
+ }
26513
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26514
+ const firefox3 = await getFirefox2();
26515
+ const extensionData = { type };
26516
+ if (path) {
26517
+ extensionData.path = path;
26518
+ }
26519
+ if (value) {
26520
+ extensionData.value = value;
26521
+ }
26522
+ const params = { extensionData };
26523
+ if (permanent !== void 0) {
26524
+ params["moz:permanent"] = permanent;
26525
+ }
26526
+ const result = await firefox3.sendBiDiCommand("webExtension.install", params);
26527
+ const extensionId = result?.extension || "unknown";
26528
+ const installType = permanent ? "permanent" : "temporary";
26529
+ return successResponse(
26530
+ `\u2705 Extension installed (${installType}):
26531
+ ID: ${extensionId}
26532
+ Type: ${type}${path ? `
26533
+ Path: ${path}` : ""}`
26534
+ );
26535
+ } catch (error2) {
26536
+ return errorResponse(error2);
26537
+ }
26538
+ }
26539
+ async function handleUninstallExtension(args2) {
26540
+ try {
26541
+ const { id } = args2;
26542
+ if (!id || typeof id !== "string") {
26543
+ throw new Error("id parameter is required and must be a string");
26544
+ }
26545
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26546
+ const firefox3 = await getFirefox2();
26547
+ await firefox3.sendBiDiCommand("webExtension.uninstall", { extension: id });
26548
+ return successResponse(`\u2705 Extension uninstalled:
26549
+ ID: ${id}`);
26550
+ } catch (error2) {
26551
+ return errorResponse(error2);
26552
+ }
26553
+ }
26554
+ function formatExtensionList(extensions, filterId) {
26555
+ if (extensions.length === 0) {
26556
+ return filterId ? `\u{1F50D} Extension not found: ${filterId}` : "\u{1F4E6} No extensions installed";
26557
+ }
26558
+ const lines = [
26559
+ `\u{1F4E6} ${extensions.length} extension(s)${filterId ? ` (filtered by: ${filterId})` : ""}`
26560
+ ];
26561
+ for (const ext of extensions) {
26562
+ lines.push("");
26563
+ lines.push(` \u{1F4CC} ${ext.name} (v${ext.version})`);
26564
+ lines.push(` ID: ${ext.id}`);
26565
+ lines.push(` Type: ${ext.isSystem ? "\u{1F527} System/Built-in" : "\u{1F464} User-installed"}`);
26566
+ lines.push(` UUID: ${ext.uuid}`);
26567
+ lines.push(` Base URL: ${ext.baseURL}`);
26568
+ lines.push(` Manifest: v${ext.manifestVersion || "unknown"}`);
26569
+ lines.push(` Active: ${ext.isActive ? "\u2705" : "\u274C"}`);
26570
+ if (ext.backgroundScripts.length > 0) {
26571
+ lines.push(` Background scripts:`);
26572
+ for (const script of ext.backgroundScripts) {
26573
+ const scriptName = script.split("/").pop();
26574
+ lines.push(` \u2022 ${scriptName}`);
26575
+ }
26576
+ } else {
26577
+ lines.push(` Background scripts: (none)`);
26578
+ }
26579
+ }
26580
+ return lines.join("\n");
26581
+ }
26582
+ async function handleListExtensions(args2) {
26583
+ try {
26584
+ const { ids, name, isActive, isSystem } = args2 || {};
26585
+ const { getFirefox: getFirefox2 } = await init_index().then(() => index_exports);
26586
+ const firefox3 = await getFirefox2();
26587
+ const result = await firefox3.sendBiDiCommand("browsingContext.getTree", {
26588
+ "moz:scope": "chrome"
26589
+ });
26590
+ const contexts = result.contexts || [];
26591
+ if (contexts.length === 0) {
26592
+ throw new Error(
26593
+ "No privileged contexts available. Ensure MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 is set."
26594
+ );
26595
+ }
26596
+ const driver = firefox3.getDriver();
26597
+ const chromeContextId = contexts[0].context;
26598
+ const originalContextId = firefox3.getCurrentContextId();
26599
+ try {
26600
+ await driver.switchTo().window(chromeContextId);
26601
+ await driver.setContext("chrome");
26602
+ const filterParams = { ids, name, isActive, isSystem };
26603
+ const script = `
26604
+ const callback = arguments[arguments.length - 1];
26605
+ const filter = ${JSON.stringify(filterParams)};
26606
+ (async () => {
26607
+ try {
26608
+ const { AddonManager } = ChromeUtils.importESModule("resource://gre/modules/AddonManager.sys.mjs");
26609
+ let addons = await AddonManager.getAllAddons();
26610
+
26611
+ // Filter to only extensions (not themes, plugins, etc.)
26612
+ addons = addons.filter(addon => addon.type === "extension");
26613
+
26614
+ // Apply filters
26615
+ if (filter.ids && filter.ids.length > 0) {
26616
+ addons = addons.filter(addon => filter.ids.includes(addon.id));
26617
+ }
26618
+ if (filter.name) {
26619
+ const search = filter.name.toLowerCase();
26620
+ addons = addons.filter(addon => addon.name.toLowerCase().includes(search));
26621
+ }
26622
+ if (typeof filter.isActive === 'boolean') {
26623
+ addons = addons.filter(addon => addon.isActive === filter.isActive);
26624
+ }
26625
+ if (typeof filter.isSystem === 'boolean') {
26626
+ addons = addons.filter(addon => addon.isSystem === filter.isSystem);
26627
+ }
26628
+
26629
+ const extensions = [];
26630
+ for (const addon of addons) {
26631
+ const policy = WebExtensionPolicy.getByID(addon.id);
26632
+ if (!policy) continue; // Skip if no policy (addon not loaded)
26633
+
26634
+ extensions.push({
26635
+ id: addon.id,
26636
+ name: addon.name,
26637
+ version: addon.version,
26638
+ isActive: addon.isActive,
26639
+ isSystem: addon.isSystem,
26640
+ uuid: policy.mozExtensionHostname,
26641
+ baseURL: policy.baseURL,
26642
+ backgroundScripts: policy.extension?.backgroundScripts || [],
26643
+ manifestVersion: policy.extension?.manifest?.manifest_version || null
26644
+ });
26645
+ }
26646
+
26647
+ callback(extensions);
26648
+ } catch (error) {
26649
+ callback([]);
26650
+ }
26651
+ })();
26652
+ `;
26653
+ const extensions = await driver.executeAsyncScript(script);
26654
+ const filterDesc = [
26655
+ ids && ids.length > 0 ? `ids: [${ids.join(", ")}]` : null,
26656
+ name ? `name: "${name}"` : null,
26657
+ typeof isActive === "boolean" ? `active: ${isActive}` : null,
26658
+ typeof isSystem === "boolean" ? `system: ${isSystem}` : null
26659
+ ].filter(Boolean).join(", ");
26660
+ return successResponse(formatExtensionList(extensions, filterDesc || void 0));
26661
+ } finally {
26662
+ try {
26663
+ await driver.setContext("content");
26664
+ if (originalContextId) {
26665
+ await driver.switchTo().window(originalContextId);
26666
+ }
26667
+ } catch {
26668
+ }
26669
+ }
26670
+ } catch (error2) {
26671
+ if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
26672
+ return errorResponse(
26673
+ new Error(
26674
+ "Chrome context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox."
26675
+ )
26676
+ );
26677
+ }
26678
+ return errorResponse(error2);
26679
+ }
26680
+ }
26681
+ var installExtensionTool, uninstallExtensionTool, listExtensionsTool;
26682
+ var init_webextension = __esm({
26683
+ "src/tools/webextension.ts"() {
26684
+ "use strict";
26685
+ init_response_helpers();
26686
+ installExtensionTool = {
26687
+ name: "install_extension",
26688
+ description: "Install a Firefox extension using WebDriver BiDi webExtension.install command. Supports installing from archive (.xpi/.zip), base64-encoded data, or unpacked directory.",
26689
+ inputSchema: {
26690
+ type: "object",
26691
+ properties: {
26692
+ type: {
26693
+ type: "string",
26694
+ enum: ["archivePath", "base64", "path"],
26695
+ description: 'Extension data type: "archivePath" for .xpi/.zip, "base64" for encoded data, "path" for unpacked directory'
26696
+ },
26697
+ path: {
26698
+ type: "string",
26699
+ description: "File path (for archivePath or path types)"
26700
+ },
26701
+ value: {
26702
+ type: "string",
26703
+ description: "Base64-encoded extension data (for base64 type)"
26704
+ },
26705
+ permanent: {
26706
+ type: "boolean",
26707
+ description: "Firefox-specific: Install permanently (requires signed extension). Default: false (temporary install)"
26708
+ }
26709
+ },
26710
+ required: ["type"]
26711
+ }
26712
+ };
26713
+ uninstallExtensionTool = {
26714
+ name: "uninstall_extension",
26715
+ description: "Uninstall a Firefox extension using WebDriver BiDi webExtension.uninstall command. Requires the extension ID returned by install_extension or obtained from list_extensions.",
26716
+ inputSchema: {
26717
+ type: "object",
26718
+ properties: {
26719
+ id: {
26720
+ type: "string",
26721
+ description: 'Extension ID (e.g., "addon@example.com")'
26722
+ }
26723
+ },
26724
+ required: ["id"]
26725
+ }
26726
+ };
26727
+ listExtensionsTool = {
26728
+ name: "list_extensions",
26729
+ description: (
26730
+ // MOZ_REMOTE_ALLOW_SYSTEM_ACCESS is required because the tool relies on the
26731
+ // privileged AddonManager API as a workaround for the currently missing
26732
+ // webExtension.getExtensions WebDriver BiDi command.
26733
+ "List installed Firefox extensions with UUIDs and background scripts. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var."
26734
+ ),
26735
+ inputSchema: {
26736
+ type: "object",
26737
+ properties: {
26738
+ ids: {
26739
+ type: "array",
26740
+ items: { type: "string" },
26741
+ description: 'Optional: Filter by exact extension IDs (e.g., ["addon@example.com"])'
26742
+ },
26743
+ name: {
26744
+ type: "string",
26745
+ description: 'Optional: Filter by partial name match (case-insensitive, e.g., "shopify")'
26746
+ },
26747
+ isActive: {
26748
+ type: "boolean",
26749
+ description: "Optional: Filter by enabled (true) or disabled (false) status"
26750
+ },
26751
+ isSystem: {
26752
+ type: "boolean",
26753
+ description: "Optional: Filter by system/built-in (true) or user-installed (false) extensions"
26754
+ }
26755
+ }
26756
+ }
26757
+ };
26758
+ }
26759
+ });
26760
+
25536
26761
  // src/tools/index.ts
25537
26762
  var init_tools = __esm({
25538
- "src/tools/index.ts"() {
26763
+ async "src/tools/index.ts"() {
25539
26764
  "use strict";
25540
26765
  init_pages2();
26766
+ init_script();
25541
26767
  init_console2();
25542
26768
  init_network2();
25543
26769
  init_snapshot2();
25544
26770
  init_input();
25545
26771
  init_screenshot();
25546
26772
  init_utilities();
26773
+ await init_firefox_management();
26774
+ init_privileged_context();
26775
+ init_firefox_prefs();
26776
+ init_webextension();
25547
26777
  }
25548
26778
  });
25549
26779
 
@@ -25565,7 +26795,7 @@ var init_errors4 = __esm({
25565
26795
  FirefoxDisconnectedError = class extends Error {
25566
26796
  constructor(reason) {
25567
26797
  const baseMessage = "Firefox browser is not connected";
25568
- const instruction = "The Firefox browser window was closed by the user. To continue browser automation, ask the user to restart the firefox-devtools-mcp server (they need to restart Claude Code or the MCP connection). This will launch a new Firefox instance.";
26798
+ const instruction = 'The Firefox browser window was closed. Use the restart_firefox tool with firefoxPath parameter to start a new Firefox instance. Example: restart_firefox with firefoxPath="/usr/bin/firefox"';
25569
26799
  const fullMessage = reason ? `${baseMessage}: ${reason}. ${instruction}` : `${baseMessage}. ${instruction}`;
25570
26800
  super(fullMessage);
25571
26801
  this.name = "FirefoxDisconnectedError";
@@ -25979,8 +27209,11 @@ __export(index_exports, {
25979
27209
  FirefoxDisconnectedError: () => FirefoxDisconnectedError,
25980
27210
  args: () => args,
25981
27211
  getFirefox: () => getFirefox,
27212
+ getFirefoxIfRunning: () => getFirefoxIfRunning,
25982
27213
  isDisconnectionError: () => isDisconnectionError,
25983
- resetFirefox: () => resetFirefox
27214
+ isFirefoxRunning: () => isFirefoxRunning,
27215
+ resetFirefox: () => resetFirefox,
27216
+ setNextLaunchOptions: () => setNextLaunchOptions
25984
27217
  });
25985
27218
  import { version as version2 } from "process";
25986
27219
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -25993,6 +27226,16 @@ function resetFirefox() {
25993
27226
  }
25994
27227
  log("Firefox instance reset - will reconnect on next tool call");
25995
27228
  }
27229
+ function setNextLaunchOptions(options) {
27230
+ nextLaunchOptions = options;
27231
+ log("Next launch options updated");
27232
+ }
27233
+ function isFirefoxRunning() {
27234
+ return firefox2 !== null;
27235
+ }
27236
+ function getFirefoxIfRunning() {
27237
+ return firefox2;
27238
+ }
25996
27239
  async function getFirefox() {
25997
27240
  if (firefox2) {
25998
27241
  const isConnected = await firefox2.isConnected();
@@ -26004,21 +27247,48 @@ async function getFirefox() {
26004
27247
  return firefox2;
26005
27248
  }
26006
27249
  log("Initializing Firefox DevTools connection...");
26007
- const options = {
26008
- firefoxPath: args.firefoxPath ?? void 0,
26009
- headless: args.headless,
26010
- profilePath: args.profilePath ?? void 0,
26011
- viewport: args.viewport ?? void 0,
26012
- args: args.firefoxArg ?? void 0,
26013
- startUrl: args.startUrl ?? void 0,
26014
- acceptInsecureCerts: args.acceptInsecureCerts,
26015
- connectExisting: args.connectExisting,
26016
- marionettePort: args.marionettePort
26017
- };
27250
+ let options;
27251
+ if (nextLaunchOptions) {
27252
+ options = nextLaunchOptions;
27253
+ nextLaunchOptions = null;
27254
+ log("Using custom launch options from restart_firefox");
27255
+ } else {
27256
+ let envVars;
27257
+ if (args.env && Array.isArray(args.env) && args.env.length > 0) {
27258
+ envVars = {};
27259
+ for (const envStr of args.env) {
27260
+ const [key, ...valueParts] = envStr.split("=");
27261
+ if (key && valueParts.length > 0) {
27262
+ envVars[key] = valueParts.join("=");
27263
+ }
27264
+ }
27265
+ }
27266
+ const prefValues = parsePrefs(args.pref);
27267
+ const prefs = Object.keys(prefValues).length > 0 ? prefValues : void 0;
27268
+ options = {
27269
+ firefoxPath: args.firefoxPath ?? void 0,
27270
+ headless: args.headless,
27271
+ profilePath: args.profilePath ?? void 0,
27272
+ viewport: args.viewport ?? void 0,
27273
+ args: args.firefoxArg ?? void 0,
27274
+ startUrl: args.startUrl ?? void 0,
27275
+ acceptInsecureCerts: args.acceptInsecureCerts,
27276
+ connectExisting: args.connectExisting,
27277
+ marionettePort: args.marionettePort,
27278
+ env: envVars,
27279
+ logFile: args.outputFile ?? void 0,
27280
+ prefs
27281
+ };
27282
+ }
26018
27283
  firefox2 = new FirefoxClient(options);
26019
- await firefox2.connect();
26020
- log("Firefox DevTools connection established");
26021
- return firefox2;
27284
+ try {
27285
+ await firefox2.connect();
27286
+ log("Firefox DevTools connection established");
27287
+ return firefox2;
27288
+ } catch (error2) {
27289
+ firefox2 = null;
27290
+ throw error2;
27291
+ }
26022
27292
  }
26023
27293
  async function main() {
26024
27294
  log(`Starting ${SERVER_NAME} v${SERVER_VERSION}`);
@@ -26074,7 +27344,7 @@ async function main() {
26074
27344
  log("Firefox DevTools MCP server running on stdio");
26075
27345
  log("Ready to accept tool requests");
26076
27346
  }
26077
- var major, args, firefox2, toolHandlers, allTools, modulePath, scriptPath, isMainModule;
27347
+ var major, args, firefox2, nextLaunchOptions, toolHandlers, allTools, modulePath, scriptPath, isMainModule;
26078
27348
  var init_index = __esm({
26079
27349
  async "src/index.ts"() {
26080
27350
  init_server2();
@@ -26084,7 +27354,7 @@ var init_index = __esm({
26084
27354
  init_logger();
26085
27355
  init_cli();
26086
27356
  init_firefox();
26087
- init_tools();
27357
+ await init_tools();
26088
27358
  init_errors4();
26089
27359
  init_firefox();
26090
27360
  init_errors4();
@@ -26105,15 +27375,14 @@ var init_index = __esm({
26105
27375
  }
26106
27376
  args = parseArguments(SERVER_VERSION);
26107
27377
  firefox2 = null;
26108
- toolHandlers = /* @__PURE__ */ new Map([
27378
+ nextLaunchOptions = null;
27379
+ toolHandlers = new Map([
26109
27380
  // Pages
26110
27381
  ["list_pages", handleListPages],
26111
27382
  ["new_page", handleNewPage],
26112
27383
  ["navigate_page", handleNavigatePage],
26113
27384
  ["select_page", handleSelectPage],
26114
27385
  ["close_page", handleClosePage],
26115
- // Script evaluation - DISABLED (see docs/future-features.md)
26116
- // ['evaluate_script', tools.handleEvaluateScript],
26117
27386
  // Console
26118
27387
  ["list_console_messages", handleListConsoleMessages],
26119
27388
  ["clear_console_messages", handleClearConsoleMessages],
@@ -26138,7 +27407,25 @@ var init_index = __esm({
26138
27407
  ["accept_dialog", handleAcceptDialog],
26139
27408
  ["dismiss_dialog", handleDismissDialog],
26140
27409
  ["navigate_history", handleNavigateHistory],
26141
- ["set_viewport_size", handleSetViewportSize]
27410
+ ["set_viewport_size", handleSetViewportSize],
27411
+ // Firefox Management
27412
+ ["get_firefox_output", handleGetFirefoxLogs],
27413
+ ["get_firefox_info", handleGetFirefoxInfo],
27414
+ ["restart_firefox", handleRestartFirefox],
27415
+ // WebExtensions (install/uninstall use standard BiDi, no privileged context required)
27416
+ ["install_extension", handleInstallExtension],
27417
+ ["uninstall_extension", handleUninstallExtension],
27418
+ // Script evaluation — requires --enable-script
27419
+ ...args.enableScript ? [["evaluate_script", handleEvaluateScript]] : [],
27420
+ // Privileged context tools — requires --enable-privileged-context
27421
+ ...args.enablePrivilegedContext ? [
27422
+ ["list_privileged_contexts", handleListPrivilegedContexts],
27423
+ ["select_privileged_context", handleSelectPrivilegedContext],
27424
+ ["evaluate_privileged_script", handleEvaluatePrivilegedScript],
27425
+ ["set_firefox_prefs", handleSetFirefoxPrefs],
27426
+ ["get_firefox_prefs", handleGetFirefoxPrefs],
27427
+ ["list_extensions", handleListExtensions]
27428
+ ] : []
26142
27429
  ]);
26143
27430
  allTools = [
26144
27431
  listPagesTool,
@@ -26164,7 +27451,23 @@ var init_index = __esm({
26164
27451
  acceptDialogTool,
26165
27452
  dismissDialogTool,
26166
27453
  navigateHistoryTool,
26167
- setViewportSizeTool
27454
+ setViewportSizeTool,
27455
+ getFirefoxLogsTool,
27456
+ getFirefoxInfoTool,
27457
+ restartFirefoxTool,
27458
+ installExtensionTool,
27459
+ uninstallExtensionTool,
27460
+ // Script evaluation — requires --enable-script
27461
+ ...args.enableScript ? [evaluateScriptTool] : [],
27462
+ // Privileged context tools — requires --enable-privileged-context
27463
+ ...args.enablePrivilegedContext ? [
27464
+ listPrivilegedContextsTool,
27465
+ selectPrivilegedContextTool,
27466
+ evaluatePrivilegedScriptTool,
27467
+ setFirefoxPrefsTool,
27468
+ getFirefoxPrefsTool,
27469
+ listExtensionsTool
27470
+ ] : []
26168
27471
  ];
26169
27472
  modulePath = fileURLToPath2(import.meta.url);
26170
27473
  scriptPath = process.argv[1] ? resolve3(process.argv[1]) : "";
@@ -26190,6 +27493,9 @@ export {
26190
27493
  FirefoxDisconnectedError,
26191
27494
  args,
26192
27495
  getFirefox,
27496
+ getFirefoxIfRunning,
26193
27497
  isDisconnectionError,
26194
- resetFirefox
27498
+ isFirefoxRunning,
27499
+ resetFirefox,
27500
+ setNextLaunchOptions
26195
27501
  };