@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.
- package/README.md +22 -0
- package/dist/index.js +126 -54
- 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 = (
|
|
5176
|
-
const escapedDelim = escapeRegex(
|
|
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
|
-
|
|
28056
|
-
const
|
|
28057
|
-
|
|
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
|
-
|
|
28060
|
-
|
|
28061
|
-
|
|
28062
|
-
const
|
|
28063
|
-
|
|
28064
|
-
|
|
28065
|
-
|
|
28066
|
-
|
|
28067
|
-
|
|
28068
|
-
|
|
28069
|
-
|
|
28070
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 (!
|
|
31674
|
+
if (!existsSync3(logFilePath)) {
|
|
31605
31675
|
return successResponse(`Output file not found: ${logFilePath}`);
|
|
31606
31676
|
}
|
|
31607
31677
|
if (since !== void 0) {
|
|
31608
|
-
const stats =
|
|
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 (
|
|
31678
|
-
const stats =
|
|
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);
|