@mozilla/firefox-devtools-mcp 0.9.3 → 0.9.4

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.
Files changed (3) hide show
  1. package/README.md +22 -0
  2. package/dist/index.js +126 -54
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -108,9 +108,31 @@ You can pass flags or environment variables (names on the right):
108
108
  - `--pref name=value` — set Firefox preference at startup via `moz:firefoxOptions` (repeatable)
109
109
  - `--enable-script` — enable the `evaluate_script` tool, which executes arbitrary JavaScript in the page context (`ENABLE_SCRIPT=true`)
110
110
  - `--enable-privileged-context` — 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` (`ENABLE_PRIVILEGED_CONTEXT=true`)
111
+ - `--android-device` — enable Firefox for Android mode; value is the ADB device serial (e.g. `emulator-5554`). Run `adb devices` to list connected devices. Omit the value or use `auto` to select the single connected device automatically.
112
+ - `--android-package` — Android app package name, default `org.mozilla.firefox`. Other packages: `org.mozilla.firefox_beta` for Firefox Beta, `org.mozilla.fenix` for Firefox Nightly, `org.mozilla.fenix.debug` for Firefox Nightly Debug, `org.mozilla.geckoview_example` for geckoview (`ANDROID_PACKAGE`)
111
113
 
112
114
  > **Note on `--pref`:** When Firefox runs in automation, it applies [RecommendedPreferences](https://searchfox.org/firefox-main/source/remote/shared/RecommendedPreferences.sys.mjs) that modify browser behavior for testing. The `--pref` option allows overriding these defaults when needed.
113
115
 
116
+ ### Firefox for Android
117
+
118
+ Use `--android-device` to automate Firefox running on an Android device. Requires `adb` on your PATH and geckodriver, which is managed automatically.
119
+
120
+ ```bash
121
+ # List connected devices
122
+ adb devices
123
+
124
+ # Launch Firefox for Android on the single connected device
125
+ npx firefox-devtools-mcp --android-device auto
126
+
127
+ # Target a specific device
128
+ npx firefox-devtools-mcp --android-device <serial>
129
+
130
+ # Use Firefox Nightly instead
131
+ npx firefox-devtools-mcp --android-device <serial> --android-package org.mozilla.fenix
132
+ ```
133
+
134
+ Port forwarding between the host and device is handled automatically by geckodriver.
135
+
114
136
  ### Connect to existing Firefox
115
137
 
116
138
  Use `--connect-existing` to automate your real browsing session — with cookies, logins, and open tabs intact:
package/dist/index.js CHANGED
@@ -155,6 +155,15 @@ var init_cli = __esm({
155
155
  description: "Set Firefox preference at startup via moz:firefoxOptions (format: name=value). Can be specified multiple times.",
156
156
  alias: "p"
157
157
  },
158
+ androidDevice: {
159
+ type: "string",
160
+ description: "Android device serial for launching Firefox for Android via ADB. Omit to auto-select the single connected device. Requires adb on PATH."
161
+ },
162
+ androidPackage: {
163
+ type: "string",
164
+ description: "Android app package name (default: org.mozilla.firefox). Use org.mozilla.fenix for Nightly.",
165
+ default: process.env.ANDROID_PACKAGE ?? "org.mozilla.firefox"
166
+ },
158
167
  enableScript: {
159
168
  type: "boolean",
160
169
  description: "Enable the evaluate_script tool, which allows executing arbitrary JavaScript in the page context.",
@@ -5172,8 +5181,8 @@ var init_regexes = __esm({
5172
5181
  _emoji = `^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$`;
5173
5182
  ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;
5174
5183
  ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
5175
- mac = (delimiter) => {
5176
- const escapedDelim = escapeRegex(delimiter ?? ":");
5184
+ mac = (delimiter2) => {
5185
+ const escapedDelim = escapeRegex(delimiter2 ?? ":");
5177
5186
  return new RegExp(`^(?:[0-9A-F]{2}${escapedDelim}){5}[0-9A-F]{2}$|^(?:[0-9a-f]{2}${escapedDelim}){5}[0-9a-f]{2}$`);
5178
5187
  };
5179
5188
  cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\/([0-9]|[1-2][0-9]|3[0-2])$/;
@@ -28049,50 +28058,61 @@ var init_profile = __esm({
28049
28058
  // src/firefox/core.ts
28050
28059
  import { Builder, Browser, Capabilities } from "selenium-webdriver";
28051
28060
  import firefox from "selenium-webdriver/firefox.js";
28052
- import { mkdirSync as mkdirSync2, openSync, closeSync } from "fs";
28061
+ import { mkdirSync as mkdirSync2, openSync, closeSync, existsSync as existsSync2, readdirSync, statSync } from "fs";
28053
28062
  import { homedir } from "os";
28054
- import { join as join2 } from "path";
28055
- async function findGeckodriver() {
28056
- const path = await import("path");
28057
- const { execFileSync } = await import("child_process");
28063
+ import { join as join2, delimiter } from "path";
28064
+ function findGeckodriverInPath(binaryName) {
28065
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
28066
+ if (!dir) {
28067
+ continue;
28068
+ }
28069
+ const candidate = join2(dir, binaryName);
28070
+ if (existsSync2(candidate)) {
28071
+ return candidate;
28072
+ }
28073
+ }
28074
+ return null;
28075
+ }
28076
+ function findGeckodriverInSeleniumCache(binaryName) {
28077
+ const cacheBase = join2(homedir(), ".cache/selenium/geckodriver");
28058
28078
  try {
28059
- const { createRequire } = await import("module");
28060
- const require2 = createRequire(import.meta.url);
28061
- const swPkg = require2.resolve("selenium-webdriver/package.json");
28062
- const swDir = path.dirname(swPkg);
28063
- const platform = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux";
28064
- const ext = process.platform === "win32" ? ".exe" : "";
28065
- const smBin = path.join(swDir, "bin", platform, `selenium-manager${ext}`);
28066
- const result = JSON.parse(
28067
- execFileSync(smBin, ["--driver", "geckodriver", "--output", "json"], { encoding: "utf-8" })
28068
- );
28069
- return result.result.driver_path;
28070
- } catch {
28071
- const os = await import("os");
28072
- const fs = await import("fs");
28073
- const cacheBase = path.join(os.homedir(), ".cache/selenium/geckodriver");
28074
- const ext = process.platform === "win32" ? ".exe" : "";
28075
- const binaryName = `geckodriver${ext}`;
28076
- try {
28077
- if (fs.existsSync(cacheBase)) {
28078
- for (const platformDir of fs.readdirSync(cacheBase)) {
28079
- const platformPath = path.join(cacheBase, platformDir);
28080
- if (!fs.statSync(platformPath).isDirectory()) {
28081
- continue;
28082
- }
28083
- for (const versionDir of fs.readdirSync(platformPath).sort().reverse()) {
28084
- const candidate = path.join(platformPath, versionDir, binaryName);
28085
- if (fs.existsSync(candidate)) {
28086
- return candidate;
28087
- }
28088
- }
28079
+ if (!existsSync2(cacheBase)) {
28080
+ return null;
28081
+ }
28082
+ for (const platformDir of readdirSync(cacheBase)) {
28083
+ const platformPath = join2(cacheBase, platformDir);
28084
+ if (!statSync(platformPath).isDirectory()) {
28085
+ continue;
28086
+ }
28087
+ for (const versionDir of readdirSync(platformPath).sort().reverse()) {
28088
+ const candidate = join2(platformPath, versionDir, binaryName);
28089
+ if (existsSync2(candidate)) {
28090
+ return candidate;
28089
28091
  }
28090
28092
  }
28091
- } catch {
28092
28093
  }
28093
- throw new Error("Cannot find geckodriver binary. Ensure selenium-webdriver is installed.");
28094
+ } catch {
28095
+ }
28096
+ return null;
28097
+ }
28098
+ async function findGeckodriverInNpmPackage() {
28099
+ try {
28100
+ const { download } = await import("geckodriver");
28101
+ log("geckodriver not found in PATH or selenium cache, downloading via npm package...");
28102
+ return await download();
28103
+ } catch {
28104
+ return null;
28094
28105
  }
28095
28106
  }
28107
+ async function findGeckodriver() {
28108
+ const ext = process.platform === "win32" ? ".exe" : "";
28109
+ const binaryName = `geckodriver${ext}`;
28110
+ const found = findGeckodriverInPath(binaryName) ?? findGeckodriverInSeleniumCache(binaryName) ?? await findGeckodriverInNpmPackage();
28111
+ if (!found) {
28112
+ throw new Error("Cannot find geckodriver binary. Ensure geckodriver is in PATH.");
28113
+ }
28114
+ return found;
28115
+ }
28096
28116
  var FirefoxCore;
28097
28117
  var init_core3 = __esm({
28098
28118
  "src/firefox/core.ts"() {
@@ -28113,12 +28133,35 @@ var init_core3 = __esm({
28113
28133
  * Launch Firefox (or connect to an existing instance) and establish BiDi connection
28114
28134
  */
28115
28135
  async connect() {
28116
- if (this.options.connectExisting) {
28136
+ const isAndroid = this.options.androidDevice !== void 0;
28137
+ if (isAndroid) {
28138
+ log("Launching Firefox for Android via ADB...");
28139
+ } else if (this.options.connectExisting) {
28117
28140
  log("Connecting to existing Firefox via Marionette...");
28118
28141
  } else {
28119
28142
  log("Launching Firefox via Selenium WebDriver BiDi...");
28120
28143
  }
28121
- if (this.options.connectExisting) {
28144
+ if (isAndroid) {
28145
+ const geckodriverPath = await findGeckodriver();
28146
+ logDebug(`Using geckodriver: ${geckodriverPath}`);
28147
+ const pkg = this.options.androidPackage ?? "org.mozilla.firefox";
28148
+ const mozOptions = { androidPackage: pkg };
28149
+ const deviceSerial = this.options.androidDevice;
28150
+ if (deviceSerial && deviceSerial !== "auto") {
28151
+ mozOptions.androidDeviceSerial = deviceSerial;
28152
+ }
28153
+ if (this.options.prefs) {
28154
+ mozOptions.prefs = this.options.prefs;
28155
+ }
28156
+ const caps = new Capabilities();
28157
+ caps.set("webSocketUrl", true);
28158
+ caps.set("moz:firefoxOptions", mozOptions);
28159
+ if (this.options.acceptInsecureCerts) {
28160
+ caps.set("acceptInsecureCerts", true);
28161
+ }
28162
+ const serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
28163
+ this.driver = firefox.Driver.createSession(caps, serviceBuilder.build());
28164
+ } else if (this.options.connectExisting) {
28122
28165
  const port = this.options.marionettePort ?? 2828;
28123
28166
  const geckodriverPath = await findGeckodriver();
28124
28167
  logDebug(`Using geckodriver: ${geckodriverPath}`);
@@ -28177,7 +28220,14 @@ var init_core3 = __esm({
28177
28220
  firefoxOptions.setPreference(name, value);
28178
28221
  }
28179
28222
  }
28180
- const serviceBuilder = new firefox.ServiceBuilder();
28223
+ let serviceBuilder;
28224
+ if (process.platform === "win32") {
28225
+ const geckodriverPath = await findGeckodriver();
28226
+ logDebug(`Using geckodriver: ${geckodriverPath}`);
28227
+ serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
28228
+ } else {
28229
+ serviceBuilder = new firefox.ServiceBuilder();
28230
+ }
28181
28231
  if (this.logFilePath) {
28182
28232
  this.logFileFd = openSync(this.logFilePath, "a");
28183
28233
  serviceBuilder.setStdio(["ignore", this.logFileFd, this.logFileFd]);
@@ -29051,22 +29101,41 @@ var init_dom = __esm({
29051
29101
  });
29052
29102
 
29053
29103
  // src/firefox/pages.ts
29054
- var PageManagement;
29104
+ function isCommonScheme(url2) {
29105
+ try {
29106
+ return COMMON_URL_SCHEMES.includes(new URL(url2).protocol);
29107
+ } catch {
29108
+ return false;
29109
+ }
29110
+ }
29111
+ var COMMON_URL_SCHEMES, PageManagement;
29055
29112
  var init_pages = __esm({
29056
29113
  "src/firefox/pages.ts"() {
29057
29114
  "use strict";
29058
29115
  init_logger();
29116
+ COMMON_URL_SCHEMES = ["http:", "https:", "data:", "blob:", "file:"];
29059
29117
  PageManagement = class {
29060
- constructor(driver, getCurrentContextId, setCurrentContextId) {
29118
+ constructor(driver, getCurrentContextId, setCurrentContextId, sendBiDiCommand) {
29061
29119
  this.driver = driver;
29062
29120
  this.getCurrentContextId = getCurrentContextId;
29063
29121
  this.setCurrentContextId = setCurrentContextId;
29122
+ this.sendBiDiCommand = sendBiDiCommand;
29064
29123
  }
29065
29124
  /**
29066
- * Navigate to URL
29125
+ * Navigate to URL using BiDi
29067
29126
  */
29068
29127
  async navigate(url2) {
29069
- await this.driver.get(url2);
29128
+ const contextId = this.getCurrentContextId();
29129
+ if (!contextId) {
29130
+ throw new Error(`Cannot navigate: no browsing context ID`);
29131
+ }
29132
+ const wait = isCommonScheme(url2) ? "interactive" : "none";
29133
+ await this.sendBiDiCommand("browsingContext.navigate", {
29134
+ context: contextId,
29135
+ url: url2,
29136
+ wait
29137
+ });
29138
+ logDebug(`BiDi navigate (wait:${wait}) to: ${url2}`);
29070
29139
  log(`Navigated to: ${url2}`);
29071
29140
  }
29072
29141
  /**
@@ -29188,7 +29257,7 @@ var init_pages = __esm({
29188
29257
  const newIdx = handles.length - 1;
29189
29258
  this.setCurrentContextId(handles[newIdx]);
29190
29259
  this.cachedSelectedIdx = newIdx;
29191
- await this.driver.get(url2);
29260
+ await this.navigate(url2);
29192
29261
  return newIdx;
29193
29262
  }
29194
29263
  /**
@@ -29673,7 +29742,8 @@ var init_firefox = __esm({
29673
29742
  this.pages = new PageManagement(
29674
29743
  driver,
29675
29744
  () => this.core.getCurrentContextId(),
29676
- (id) => this.core.setCurrentContextId(id)
29745
+ (id) => this.core.setCurrentContextId(id),
29746
+ (method, params) => this.core.sendBiDiCommand(method, params)
29677
29747
  );
29678
29748
  if (this.consoleEvents) {
29679
29749
  try {
@@ -31586,7 +31656,7 @@ var init_utilities = __esm({
31586
31656
  });
31587
31657
 
31588
31658
  // src/tools/firefox-management.ts
31589
- import { readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "fs";
31659
+ import { readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
31590
31660
  async function handleGetFirefoxLogs(input) {
31591
31661
  try {
31592
31662
  const {
@@ -31601,11 +31671,11 @@ async function handleGetFirefoxLogs(input) {
31601
31671
  "No output capture configured. Use --env to set environment variables or --output-file to enable output capture."
31602
31672
  );
31603
31673
  }
31604
- if (!existsSync2(logFilePath)) {
31674
+ if (!existsSync3(logFilePath)) {
31605
31675
  return successResponse(`Output file not found: ${logFilePath}`);
31606
31676
  }
31607
31677
  if (since !== void 0) {
31608
- const stats = statSync(logFilePath);
31678
+ const stats = statSync2(logFilePath);
31609
31679
  const ageSeconds = (Date.now() - stats.mtimeMs) / 1e3;
31610
31680
  if (ageSeconds > since) {
31611
31681
  return successResponse(
@@ -31674,8 +31744,8 @@ async function handleGetFirefoxInfo(_input) {
31674
31744
  if (logFilePath) {
31675
31745
  info.push("");
31676
31746
  info.push(`Output File: ${logFilePath}`);
31677
- if (existsSync2(logFilePath)) {
31678
- const stats = statSync(logFilePath);
31747
+ if (existsSync3(logFilePath)) {
31748
+ const stats = statSync2(logFilePath);
31679
31749
  const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
31680
31750
  info.push(` Size: ${sizeMB} MB`);
31681
31751
  info.push(` Last Modified: ${stats.mtime.toISOString()}`);
@@ -32577,7 +32647,9 @@ async function getFirefox() {
32577
32647
  marionettePort: args.marionettePort,
32578
32648
  env: envVars,
32579
32649
  logFile: args.outputFile ?? void 0,
32580
- prefs
32650
+ prefs,
32651
+ androidDevice: args.androidDevice ?? void 0,
32652
+ androidPackage: args.androidPackage ?? void 0
32581
32653
  };
32582
32654
  }
32583
32655
  firefox2 = new FirefoxClient(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozilla/firefox-devtools-mcp",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Model Context Protocol (MCP) server for Firefox DevTools automation",
5
5
  "author": "Mozilla",
6
6
  "license": "MIT OR Apache-2.0",