@mozilla/firefox-devtools-mcp 0.9.3 → 0.9.5
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 +33 -11
- package/dist/index.js +259 -100
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Firefox DevTools MCP
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/mozilla/firefox-devtools-mcp)
|
|
4
4
|
[](https://github.com/mozilla/firefox-devtools-mcp/actions/workflows/ci.yml)
|
|
5
5
|
[](https://codecov.io/gh/mozilla/firefox-devtools-mcp)
|
|
6
6
|
[](LICENSE-MIT) [](LICENSE-APACHE)
|
|
@@ -11,7 +11,7 @@ Model Context Protocol server for automating Firefox via WebDriver BiDi (through
|
|
|
11
11
|
|
|
12
12
|
Repository: https://github.com/mozilla/firefox-devtools-mcp
|
|
13
13
|
|
|
14
|
-
> **Note**: This MCP server requires a local Firefox browser installation and cannot run on cloud hosting services like glama.ai. Use `npx firefox-devtools-mcp@latest` to run locally, or use Docker with the provided Dockerfile.
|
|
14
|
+
> **Note**: This MCP server requires a local Firefox browser installation and cannot run on cloud hosting services like glama.ai. Use `npx @mozilla/firefox-devtools-mcp@latest` to run locally, or use Docker with the provided Dockerfile.
|
|
15
15
|
|
|
16
16
|
## Security
|
|
17
17
|
|
|
@@ -35,17 +35,17 @@ Recommended: use npx so you always run the latest published version from npm.
|
|
|
35
35
|
Option A — Claude Code CLI
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
claude mcp add firefox-devtools npx firefox-devtools-mcp@latest
|
|
38
|
+
claude mcp add firefox-devtools npx @mozilla/firefox-devtools-mcp@latest
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Pass options either as args or env vars. Examples:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
# Headless + viewport via args
|
|
45
|
-
claude mcp add firefox-devtools npx firefox-devtools-mcp@latest -- --headless --viewport 1280x720
|
|
45
|
+
claude mcp add firefox-devtools npx @mozilla/firefox-devtools-mcp@latest -- --headless --viewport 1280x720
|
|
46
46
|
|
|
47
47
|
# Or via environment variables
|
|
48
|
-
claude mcp add firefox-devtools npx firefox-devtools-mcp@latest \
|
|
48
|
+
claude mcp add firefox-devtools npx @mozilla/firefox-devtools-mcp@latest \
|
|
49
49
|
--env START_URL=https://example.com \
|
|
50
50
|
--env FIREFOX_HEADLESS=true
|
|
51
51
|
```
|
|
@@ -63,9 +63,9 @@ Add to your Claude Code config file:
|
|
|
63
63
|
"mcpServers": {
|
|
64
64
|
"firefox-devtools": {
|
|
65
65
|
"command": "npx",
|
|
66
|
-
"args": ["-y", "firefox-devtools-mcp@latest", "--headless", "--viewport", "1280x720"],
|
|
66
|
+
"args": ["-y", "@mozilla/firefox-devtools-mcp@latest", "--headless", "--viewport", "1280x720"],
|
|
67
67
|
"env": {
|
|
68
|
-
"START_URL": "about:
|
|
68
|
+
"START_URL": "about:blank"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -82,7 +82,7 @@ npm run setup
|
|
|
82
82
|
## Try it with MCP Inspector
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
|
-
npx @modelcontextprotocol/inspector npx firefox-devtools-mcp@latest --start-url https://example.com --headless
|
|
85
|
+
npx @modelcontextprotocol/inspector npx @mozilla/firefox-devtools-mcp@latest --start-url https://example.com --headless
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
Then call tools like:
|
|
@@ -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 @mozilla/firefox-devtools-mcp --android-device auto
|
|
126
|
+
|
|
127
|
+
# Target a specific device
|
|
128
|
+
npx @mozilla/firefox-devtools-mcp --android-device <serial>
|
|
129
|
+
|
|
130
|
+
# Use Firefox Nightly instead
|
|
131
|
+
npx @mozilla/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:
|
|
@@ -120,7 +142,7 @@ Use `--connect-existing` to automate your real browsing session — with cookies
|
|
|
120
142
|
firefox --marionette
|
|
121
143
|
|
|
122
144
|
# Run the MCP server
|
|
123
|
-
npx firefox-devtools-mcp --connect-existing --marionette-port 2828
|
|
145
|
+
npx @mozilla/firefox-devtools-mcp --connect-existing --marionette-port 2828
|
|
124
146
|
```
|
|
125
147
|
|
|
126
148
|
Or set `marionette.enabled` to `true` in `about:config` (or `user.js`) to enable Marionette on every launch.
|
|
@@ -186,7 +208,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on local development, te
|
|
|
186
208
|
"mcpServers": {
|
|
187
209
|
"firefox-devtools": {
|
|
188
210
|
"command": "cmd",
|
|
189
|
-
"args": ["/c", "npx", "-y", "firefox-devtools-mcp@latest"]
|
|
211
|
+
"args": ["/c", "npx", "-y", "@mozilla/firefox-devtools-mcp@latest"]
|
|
190
212
|
}
|
|
191
213
|
}
|
|
192
214
|
```
|
|
@@ -197,7 +219,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on local development, te
|
|
|
197
219
|
"mcpServers": {
|
|
198
220
|
"firefox-devtools": {
|
|
199
221
|
"command": "C:\\nvm4w\\nodejs\\npx.ps1",
|
|
200
|
-
"args": ["-y", "firefox-devtools-mcp@latest"]
|
|
222
|
+
"args": ["-y", "@mozilla/firefox-devtools-mcp@latest"]
|
|
201
223
|
}
|
|
202
224
|
}
|
|
203
225
|
```
|
package/dist/index.js
CHANGED
|
@@ -128,8 +128,8 @@ var init_cli = __esm({
|
|
|
128
128
|
},
|
|
129
129
|
startUrl: {
|
|
130
130
|
type: "string",
|
|
131
|
-
description: "URL to open when Firefox starts (default: about:
|
|
132
|
-
default: process.env.START_URL ?? "about:
|
|
131
|
+
description: "URL to open when Firefox starts (default: about:blank)",
|
|
132
|
+
default: process.env.START_URL ?? "about:blank"
|
|
133
133
|
},
|
|
134
134
|
connectExisting: {
|
|
135
135
|
type: "boolean",
|
|
@@ -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])$/;
|
|
@@ -17753,6 +17762,9 @@ var require_utils = __commonJS({
|
|
|
17753
17762
|
"use strict";
|
|
17754
17763
|
var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
|
|
17755
17764
|
var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
|
|
17765
|
+
var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
|
|
17766
|
+
var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
|
|
17767
|
+
var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
|
|
17756
17768
|
function stringArrayToHexStripped(input) {
|
|
17757
17769
|
let acc = "";
|
|
17758
17770
|
let code = 0;
|
|
@@ -17945,27 +17957,77 @@ var require_utils = __commonJS({
|
|
|
17945
17957
|
}
|
|
17946
17958
|
return output.join("");
|
|
17947
17959
|
}
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17960
|
+
var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
|
|
17961
|
+
var HOST_DELIM_RE = /[@/?#:]/g;
|
|
17962
|
+
var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
|
|
17963
|
+
function reescapeHostDelimiters(host, isIP) {
|
|
17964
|
+
const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
|
|
17965
|
+
re.lastIndex = 0;
|
|
17966
|
+
return host.replace(re, (ch) => HOST_DELIMS[ch]);
|
|
17967
|
+
}
|
|
17968
|
+
function normalizePercentEncoding(input, decodeUnreserved = false) {
|
|
17969
|
+
if (input.indexOf("%") === -1) {
|
|
17970
|
+
return input;
|
|
17958
17971
|
}
|
|
17959
|
-
|
|
17960
|
-
|
|
17972
|
+
let output = "";
|
|
17973
|
+
for (let i = 0; i < input.length; i++) {
|
|
17974
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
17975
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
17976
|
+
if (isHexPair(hex3)) {
|
|
17977
|
+
const normalizedHex = hex3.toUpperCase();
|
|
17978
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
17979
|
+
if (decodeUnreserved && isUnreserved(decoded)) {
|
|
17980
|
+
output += decoded;
|
|
17981
|
+
} else {
|
|
17982
|
+
output += "%" + normalizedHex;
|
|
17983
|
+
}
|
|
17984
|
+
i += 2;
|
|
17985
|
+
continue;
|
|
17986
|
+
}
|
|
17987
|
+
}
|
|
17988
|
+
output += input[i];
|
|
17961
17989
|
}
|
|
17962
|
-
|
|
17963
|
-
|
|
17990
|
+
return output;
|
|
17991
|
+
}
|
|
17992
|
+
function normalizePathEncoding(input) {
|
|
17993
|
+
let output = "";
|
|
17994
|
+
for (let i = 0; i < input.length; i++) {
|
|
17995
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
17996
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
17997
|
+
if (isHexPair(hex3)) {
|
|
17998
|
+
const normalizedHex = hex3.toUpperCase();
|
|
17999
|
+
const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
|
|
18000
|
+
if (decoded !== "." && isUnreserved(decoded)) {
|
|
18001
|
+
output += decoded;
|
|
18002
|
+
} else {
|
|
18003
|
+
output += "%" + normalizedHex;
|
|
18004
|
+
}
|
|
18005
|
+
i += 2;
|
|
18006
|
+
continue;
|
|
18007
|
+
}
|
|
18008
|
+
}
|
|
18009
|
+
if (isPathCharacter(input[i])) {
|
|
18010
|
+
output += input[i];
|
|
18011
|
+
} else {
|
|
18012
|
+
output += escape(input[i]);
|
|
18013
|
+
}
|
|
17964
18014
|
}
|
|
17965
|
-
|
|
17966
|
-
|
|
18015
|
+
return output;
|
|
18016
|
+
}
|
|
18017
|
+
function escapePreservingEscapes(input) {
|
|
18018
|
+
let output = "";
|
|
18019
|
+
for (let i = 0; i < input.length; i++) {
|
|
18020
|
+
if (input[i] === "%" && i + 2 < input.length) {
|
|
18021
|
+
const hex3 = input.slice(i + 1, i + 3);
|
|
18022
|
+
if (isHexPair(hex3)) {
|
|
18023
|
+
output += "%" + hex3.toUpperCase();
|
|
18024
|
+
i += 2;
|
|
18025
|
+
continue;
|
|
18026
|
+
}
|
|
18027
|
+
}
|
|
18028
|
+
output += escape(input[i]);
|
|
17967
18029
|
}
|
|
17968
|
-
return
|
|
18030
|
+
return output;
|
|
17969
18031
|
}
|
|
17970
18032
|
function recomposeAuthority(component) {
|
|
17971
18033
|
const uriTokens = [];
|
|
@@ -17980,7 +18042,7 @@ var require_utils = __commonJS({
|
|
|
17980
18042
|
if (ipV6res.isIPV6 === true) {
|
|
17981
18043
|
host = `[${ipV6res.escapedHost}]`;
|
|
17982
18044
|
} else {
|
|
17983
|
-
host =
|
|
18045
|
+
host = reescapeHostDelimiters(host, false);
|
|
17984
18046
|
}
|
|
17985
18047
|
}
|
|
17986
18048
|
uriTokens.push(host);
|
|
@@ -17994,7 +18056,10 @@ var require_utils = __commonJS({
|
|
|
17994
18056
|
module.exports = {
|
|
17995
18057
|
nonSimpleDomain,
|
|
17996
18058
|
recomposeAuthority,
|
|
17997
|
-
|
|
18059
|
+
reescapeHostDelimiters,
|
|
18060
|
+
normalizePercentEncoding,
|
|
18061
|
+
normalizePathEncoding,
|
|
18062
|
+
escapePreservingEscapes,
|
|
17998
18063
|
removeDotSegments,
|
|
17999
18064
|
isIPv4,
|
|
18000
18065
|
isUUID,
|
|
@@ -18218,12 +18283,12 @@ var require_schemes = __commonJS({
|
|
|
18218
18283
|
var require_fast_uri = __commonJS({
|
|
18219
18284
|
"node_modules/fast-uri/index.js"(exports, module) {
|
|
18220
18285
|
"use strict";
|
|
18221
|
-
var { normalizeIPv6, removeDotSegments, recomposeAuthority,
|
|
18286
|
+
var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
|
|
18222
18287
|
var { SCHEMES, getSchemeHandler } = require_schemes();
|
|
18223
18288
|
function normalize(uri, options) {
|
|
18224
18289
|
if (typeof uri === "string") {
|
|
18225
18290
|
uri = /** @type {T} */
|
|
18226
|
-
|
|
18291
|
+
normalizeString(uri, options);
|
|
18227
18292
|
} else if (typeof uri === "object") {
|
|
18228
18293
|
uri = /** @type {T} */
|
|
18229
18294
|
parse3(serialize(uri, options), options);
|
|
@@ -18290,19 +18355,9 @@ var require_fast_uri = __commonJS({
|
|
|
18290
18355
|
return target;
|
|
18291
18356
|
}
|
|
18292
18357
|
function equal(uriA, uriB, options) {
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
} else if (typeof uriA === "object") {
|
|
18297
|
-
uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
|
|
18298
|
-
}
|
|
18299
|
-
if (typeof uriB === "string") {
|
|
18300
|
-
uriB = unescape(uriB);
|
|
18301
|
-
uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
|
|
18302
|
-
} else if (typeof uriB === "object") {
|
|
18303
|
-
uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
|
|
18304
|
-
}
|
|
18305
|
-
return uriA.toLowerCase() === uriB.toLowerCase();
|
|
18358
|
+
const normalizedA = normalizeComparableURI(uriA, options);
|
|
18359
|
+
const normalizedB = normalizeComparableURI(uriB, options);
|
|
18360
|
+
return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
|
|
18306
18361
|
}
|
|
18307
18362
|
function serialize(cmpts, opts) {
|
|
18308
18363
|
const component = {
|
|
@@ -18327,12 +18382,12 @@ var require_fast_uri = __commonJS({
|
|
|
18327
18382
|
if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
|
|
18328
18383
|
if (component.path !== void 0) {
|
|
18329
18384
|
if (!options.skipEscape) {
|
|
18330
|
-
component.path =
|
|
18385
|
+
component.path = escapePreservingEscapes(component.path);
|
|
18331
18386
|
if (component.scheme !== void 0) {
|
|
18332
18387
|
component.path = component.path.split("%3A").join(":");
|
|
18333
18388
|
}
|
|
18334
18389
|
} else {
|
|
18335
|
-
component.path =
|
|
18390
|
+
component.path = normalizePercentEncoding(component.path);
|
|
18336
18391
|
}
|
|
18337
18392
|
}
|
|
18338
18393
|
if (options.reference !== "suffix" && component.scheme) {
|
|
@@ -18367,7 +18422,16 @@ var require_fast_uri = __commonJS({
|
|
|
18367
18422
|
return uriTokens.join("");
|
|
18368
18423
|
}
|
|
18369
18424
|
var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
|
|
18370
|
-
function
|
|
18425
|
+
function getParseError(parsed, matches) {
|
|
18426
|
+
if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
|
|
18427
|
+
return 'URI path must start with "/" when authority is present.';
|
|
18428
|
+
}
|
|
18429
|
+
if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
|
|
18430
|
+
return "URI port is malformed.";
|
|
18431
|
+
}
|
|
18432
|
+
return void 0;
|
|
18433
|
+
}
|
|
18434
|
+
function parseWithStatus(uri, opts) {
|
|
18371
18435
|
const options = Object.assign({}, opts);
|
|
18372
18436
|
const parsed = {
|
|
18373
18437
|
scheme: void 0,
|
|
@@ -18378,6 +18442,7 @@ var require_fast_uri = __commonJS({
|
|
|
18378
18442
|
query: void 0,
|
|
18379
18443
|
fragment: void 0
|
|
18380
18444
|
};
|
|
18445
|
+
let malformedAuthorityOrPort = false;
|
|
18381
18446
|
let isIP = false;
|
|
18382
18447
|
if (options.reference === "suffix") {
|
|
18383
18448
|
if (options.scheme) {
|
|
@@ -18398,6 +18463,11 @@ var require_fast_uri = __commonJS({
|
|
|
18398
18463
|
if (isNaN(parsed.port)) {
|
|
18399
18464
|
parsed.port = matches[5];
|
|
18400
18465
|
}
|
|
18466
|
+
const parseError = getParseError(parsed, matches);
|
|
18467
|
+
if (parseError !== void 0) {
|
|
18468
|
+
parsed.error = parsed.error || parseError;
|
|
18469
|
+
malformedAuthorityOrPort = true;
|
|
18470
|
+
}
|
|
18401
18471
|
if (parsed.host) {
|
|
18402
18472
|
const ipv4result = isIPv4(parsed.host);
|
|
18403
18473
|
if (ipv4result === false) {
|
|
@@ -18436,14 +18506,18 @@ var require_fast_uri = __commonJS({
|
|
|
18436
18506
|
parsed.scheme = unescape(parsed.scheme);
|
|
18437
18507
|
}
|
|
18438
18508
|
if (parsed.host !== void 0) {
|
|
18439
|
-
parsed.host = unescape(parsed.host);
|
|
18509
|
+
parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
|
|
18440
18510
|
}
|
|
18441
18511
|
}
|
|
18442
18512
|
if (parsed.path) {
|
|
18443
|
-
parsed.path =
|
|
18513
|
+
parsed.path = normalizePathEncoding(parsed.path);
|
|
18444
18514
|
}
|
|
18445
18515
|
if (parsed.fragment) {
|
|
18446
|
-
|
|
18516
|
+
try {
|
|
18517
|
+
parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
|
|
18518
|
+
} catch {
|
|
18519
|
+
parsed.error = parsed.error || "URI malformed";
|
|
18520
|
+
}
|
|
18447
18521
|
}
|
|
18448
18522
|
}
|
|
18449
18523
|
if (schemeHandler && schemeHandler.parse) {
|
|
@@ -18452,7 +18526,29 @@ var require_fast_uri = __commonJS({
|
|
|
18452
18526
|
} else {
|
|
18453
18527
|
parsed.error = parsed.error || "URI can not be parsed.";
|
|
18454
18528
|
}
|
|
18455
|
-
return parsed;
|
|
18529
|
+
return { parsed, malformedAuthorityOrPort };
|
|
18530
|
+
}
|
|
18531
|
+
function parse3(uri, opts) {
|
|
18532
|
+
return parseWithStatus(uri, opts).parsed;
|
|
18533
|
+
}
|
|
18534
|
+
function normalizeString(uri, opts) {
|
|
18535
|
+
return normalizeStringWithStatus(uri, opts).normalized;
|
|
18536
|
+
}
|
|
18537
|
+
function normalizeStringWithStatus(uri, opts) {
|
|
18538
|
+
const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
|
|
18539
|
+
return {
|
|
18540
|
+
normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
|
|
18541
|
+
malformedAuthorityOrPort
|
|
18542
|
+
};
|
|
18543
|
+
}
|
|
18544
|
+
function normalizeComparableURI(uri, opts) {
|
|
18545
|
+
if (typeof uri === "string") {
|
|
18546
|
+
const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
|
|
18547
|
+
return malformedAuthorityOrPort ? void 0 : normalized;
|
|
18548
|
+
}
|
|
18549
|
+
if (typeof uri === "object") {
|
|
18550
|
+
return serialize(uri, opts);
|
|
18551
|
+
}
|
|
18456
18552
|
}
|
|
18457
18553
|
var fastUri = {
|
|
18458
18554
|
SCHEMES,
|
|
@@ -28049,49 +28145,60 @@ var init_profile = __esm({
|
|
|
28049
28145
|
// src/firefox/core.ts
|
|
28050
28146
|
import { Builder, Browser, Capabilities } from "selenium-webdriver";
|
|
28051
28147
|
import firefox from "selenium-webdriver/firefox.js";
|
|
28052
|
-
import { mkdirSync as mkdirSync2, openSync, closeSync } from "fs";
|
|
28148
|
+
import { mkdirSync as mkdirSync2, openSync, closeSync, existsSync as existsSync2, readdirSync, statSync } from "fs";
|
|
28053
28149
|
import { homedir } from "os";
|
|
28054
|
-
import { join as join2 } from "path";
|
|
28055
|
-
|
|
28056
|
-
const
|
|
28057
|
-
|
|
28150
|
+
import { join as join2, delimiter } from "path";
|
|
28151
|
+
function findGeckodriverInPath(binaryName) {
|
|
28152
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter)) {
|
|
28153
|
+
if (!dir) {
|
|
28154
|
+
continue;
|
|
28155
|
+
}
|
|
28156
|
+
const candidate = join2(dir, binaryName);
|
|
28157
|
+
if (existsSync2(candidate)) {
|
|
28158
|
+
return candidate;
|
|
28159
|
+
}
|
|
28160
|
+
}
|
|
28161
|
+
return null;
|
|
28162
|
+
}
|
|
28163
|
+
function findGeckodriverInSeleniumCache(binaryName) {
|
|
28164
|
+
const cacheBase = join2(homedir(), ".cache/selenium/geckodriver");
|
|
28058
28165
|
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
|
-
}
|
|
28166
|
+
if (!existsSync2(cacheBase)) {
|
|
28167
|
+
return null;
|
|
28168
|
+
}
|
|
28169
|
+
for (const platformDir of readdirSync(cacheBase)) {
|
|
28170
|
+
const platformPath = join2(cacheBase, platformDir);
|
|
28171
|
+
if (!statSync(platformPath).isDirectory()) {
|
|
28172
|
+
continue;
|
|
28173
|
+
}
|
|
28174
|
+
for (const versionDir of readdirSync(platformPath).sort().reverse()) {
|
|
28175
|
+
const candidate = join2(platformPath, versionDir, binaryName);
|
|
28176
|
+
if (existsSync2(candidate)) {
|
|
28177
|
+
return candidate;
|
|
28089
28178
|
}
|
|
28090
28179
|
}
|
|
28091
|
-
} catch {
|
|
28092
28180
|
}
|
|
28093
|
-
|
|
28181
|
+
} catch {
|
|
28094
28182
|
}
|
|
28183
|
+
return null;
|
|
28184
|
+
}
|
|
28185
|
+
async function findGeckodriverInNpmPackage() {
|
|
28186
|
+
try {
|
|
28187
|
+
const { download } = await import("geckodriver");
|
|
28188
|
+
log("geckodriver not found in PATH or selenium cache, downloading via npm package...");
|
|
28189
|
+
return await download();
|
|
28190
|
+
} catch {
|
|
28191
|
+
return null;
|
|
28192
|
+
}
|
|
28193
|
+
}
|
|
28194
|
+
async function findGeckodriver() {
|
|
28195
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
28196
|
+
const binaryName = `geckodriver${ext}`;
|
|
28197
|
+
const found = findGeckodriverInPath(binaryName) ?? findGeckodriverInSeleniumCache(binaryName) ?? await findGeckodriverInNpmPackage();
|
|
28198
|
+
if (!found) {
|
|
28199
|
+
throw new Error("Cannot find geckodriver binary. Ensure geckodriver is in PATH.");
|
|
28200
|
+
}
|
|
28201
|
+
return found;
|
|
28095
28202
|
}
|
|
28096
28203
|
var FirefoxCore;
|
|
28097
28204
|
var init_core3 = __esm({
|
|
@@ -28113,12 +28220,35 @@ var init_core3 = __esm({
|
|
|
28113
28220
|
* Launch Firefox (or connect to an existing instance) and establish BiDi connection
|
|
28114
28221
|
*/
|
|
28115
28222
|
async connect() {
|
|
28116
|
-
|
|
28223
|
+
const isAndroid = this.options.androidDevice !== void 0;
|
|
28224
|
+
if (isAndroid) {
|
|
28225
|
+
log("Launching Firefox for Android via ADB...");
|
|
28226
|
+
} else if (this.options.connectExisting) {
|
|
28117
28227
|
log("Connecting to existing Firefox via Marionette...");
|
|
28118
28228
|
} else {
|
|
28119
28229
|
log("Launching Firefox via Selenium WebDriver BiDi...");
|
|
28120
28230
|
}
|
|
28121
|
-
if (
|
|
28231
|
+
if (isAndroid) {
|
|
28232
|
+
const geckodriverPath = await findGeckodriver();
|
|
28233
|
+
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
28234
|
+
const pkg = this.options.androidPackage ?? "org.mozilla.firefox";
|
|
28235
|
+
const mozOptions = { androidPackage: pkg };
|
|
28236
|
+
const deviceSerial = this.options.androidDevice;
|
|
28237
|
+
if (deviceSerial && deviceSerial !== "auto") {
|
|
28238
|
+
mozOptions.androidDeviceSerial = deviceSerial;
|
|
28239
|
+
}
|
|
28240
|
+
if (this.options.prefs) {
|
|
28241
|
+
mozOptions.prefs = this.options.prefs;
|
|
28242
|
+
}
|
|
28243
|
+
const caps = new Capabilities();
|
|
28244
|
+
caps.set("webSocketUrl", true);
|
|
28245
|
+
caps.set("moz:firefoxOptions", mozOptions);
|
|
28246
|
+
if (this.options.acceptInsecureCerts) {
|
|
28247
|
+
caps.set("acceptInsecureCerts", true);
|
|
28248
|
+
}
|
|
28249
|
+
const serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
|
|
28250
|
+
this.driver = firefox.Driver.createSession(caps, serviceBuilder.build());
|
|
28251
|
+
} else if (this.options.connectExisting) {
|
|
28122
28252
|
const port = this.options.marionettePort ?? 2828;
|
|
28123
28253
|
const geckodriverPath = await findGeckodriver();
|
|
28124
28254
|
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
@@ -28177,7 +28307,14 @@ var init_core3 = __esm({
|
|
|
28177
28307
|
firefoxOptions.setPreference(name, value);
|
|
28178
28308
|
}
|
|
28179
28309
|
}
|
|
28180
|
-
|
|
28310
|
+
let serviceBuilder;
|
|
28311
|
+
if (process.platform === "win32") {
|
|
28312
|
+
const geckodriverPath = await findGeckodriver();
|
|
28313
|
+
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
28314
|
+
serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
|
|
28315
|
+
} else {
|
|
28316
|
+
serviceBuilder = new firefox.ServiceBuilder();
|
|
28317
|
+
}
|
|
28181
28318
|
if (this.logFilePath) {
|
|
28182
28319
|
this.logFileFd = openSync(this.logFilePath, "a");
|
|
28183
28320
|
serviceBuilder.setStdio(["ignore", this.logFileFd, this.logFileFd]);
|
|
@@ -29051,22 +29188,41 @@ var init_dom = __esm({
|
|
|
29051
29188
|
});
|
|
29052
29189
|
|
|
29053
29190
|
// src/firefox/pages.ts
|
|
29054
|
-
|
|
29191
|
+
function isCommonScheme(url2) {
|
|
29192
|
+
try {
|
|
29193
|
+
return COMMON_URL_SCHEMES.includes(new URL(url2).protocol);
|
|
29194
|
+
} catch {
|
|
29195
|
+
return false;
|
|
29196
|
+
}
|
|
29197
|
+
}
|
|
29198
|
+
var COMMON_URL_SCHEMES, PageManagement;
|
|
29055
29199
|
var init_pages = __esm({
|
|
29056
29200
|
"src/firefox/pages.ts"() {
|
|
29057
29201
|
"use strict";
|
|
29058
29202
|
init_logger();
|
|
29203
|
+
COMMON_URL_SCHEMES = ["http:", "https:", "data:", "blob:", "file:"];
|
|
29059
29204
|
PageManagement = class {
|
|
29060
|
-
constructor(driver, getCurrentContextId, setCurrentContextId) {
|
|
29205
|
+
constructor(driver, getCurrentContextId, setCurrentContextId, sendBiDiCommand) {
|
|
29061
29206
|
this.driver = driver;
|
|
29062
29207
|
this.getCurrentContextId = getCurrentContextId;
|
|
29063
29208
|
this.setCurrentContextId = setCurrentContextId;
|
|
29209
|
+
this.sendBiDiCommand = sendBiDiCommand;
|
|
29064
29210
|
}
|
|
29065
29211
|
/**
|
|
29066
|
-
* Navigate to URL
|
|
29212
|
+
* Navigate to URL using BiDi
|
|
29067
29213
|
*/
|
|
29068
29214
|
async navigate(url2) {
|
|
29069
|
-
|
|
29215
|
+
const contextId = this.getCurrentContextId();
|
|
29216
|
+
if (!contextId) {
|
|
29217
|
+
throw new Error(`Cannot navigate: no browsing context ID`);
|
|
29218
|
+
}
|
|
29219
|
+
const wait = isCommonScheme(url2) ? "interactive" : "none";
|
|
29220
|
+
await this.sendBiDiCommand("browsingContext.navigate", {
|
|
29221
|
+
context: contextId,
|
|
29222
|
+
url: url2,
|
|
29223
|
+
wait
|
|
29224
|
+
});
|
|
29225
|
+
logDebug(`BiDi navigate (wait:${wait}) to: ${url2}`);
|
|
29070
29226
|
log(`Navigated to: ${url2}`);
|
|
29071
29227
|
}
|
|
29072
29228
|
/**
|
|
@@ -29188,7 +29344,7 @@ var init_pages = __esm({
|
|
|
29188
29344
|
const newIdx = handles.length - 1;
|
|
29189
29345
|
this.setCurrentContextId(handles[newIdx]);
|
|
29190
29346
|
this.cachedSelectedIdx = newIdx;
|
|
29191
|
-
await this.
|
|
29347
|
+
await this.navigate(url2);
|
|
29192
29348
|
return newIdx;
|
|
29193
29349
|
}
|
|
29194
29350
|
/**
|
|
@@ -29673,7 +29829,8 @@ var init_firefox = __esm({
|
|
|
29673
29829
|
this.pages = new PageManagement(
|
|
29674
29830
|
driver,
|
|
29675
29831
|
() => this.core.getCurrentContextId(),
|
|
29676
|
-
(id) => this.core.setCurrentContextId(id)
|
|
29832
|
+
(id) => this.core.setCurrentContextId(id),
|
|
29833
|
+
(method, params) => this.core.sendBiDiCommand(method, params)
|
|
29677
29834
|
);
|
|
29678
29835
|
if (this.consoleEvents) {
|
|
29679
29836
|
try {
|
|
@@ -31586,7 +31743,7 @@ var init_utilities = __esm({
|
|
|
31586
31743
|
});
|
|
31587
31744
|
|
|
31588
31745
|
// src/tools/firefox-management.ts
|
|
31589
|
-
import { readFileSync as readFileSync2, existsSync as
|
|
31746
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
31590
31747
|
async function handleGetFirefoxLogs(input) {
|
|
31591
31748
|
try {
|
|
31592
31749
|
const {
|
|
@@ -31601,11 +31758,11 @@ async function handleGetFirefoxLogs(input) {
|
|
|
31601
31758
|
"No output capture configured. Use --env to set environment variables or --output-file to enable output capture."
|
|
31602
31759
|
);
|
|
31603
31760
|
}
|
|
31604
|
-
if (!
|
|
31761
|
+
if (!existsSync3(logFilePath)) {
|
|
31605
31762
|
return successResponse(`Output file not found: ${logFilePath}`);
|
|
31606
31763
|
}
|
|
31607
31764
|
if (since !== void 0) {
|
|
31608
|
-
const stats =
|
|
31765
|
+
const stats = statSync2(logFilePath);
|
|
31609
31766
|
const ageSeconds = (Date.now() - stats.mtimeMs) / 1e3;
|
|
31610
31767
|
if (ageSeconds > since) {
|
|
31611
31768
|
return successResponse(
|
|
@@ -31674,8 +31831,8 @@ async function handleGetFirefoxInfo(_input) {
|
|
|
31674
31831
|
if (logFilePath) {
|
|
31675
31832
|
info.push("");
|
|
31676
31833
|
info.push(`Output File: ${logFilePath}`);
|
|
31677
|
-
if (
|
|
31678
|
-
const stats =
|
|
31834
|
+
if (existsSync3(logFilePath)) {
|
|
31835
|
+
const stats = statSync2(logFilePath);
|
|
31679
31836
|
const sizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
|
31680
31837
|
info.push(` Size: ${sizeMB} MB`);
|
|
31681
31838
|
info.push(` Last Modified: ${stats.mtime.toISOString()}`);
|
|
@@ -31712,7 +31869,7 @@ async function handleRestartFirefox(input) {
|
|
|
31712
31869
|
profilePath: profilePath ?? currentOptions.profilePath,
|
|
31713
31870
|
env: newEnv !== void 0 ? newEnv : currentOptions.env,
|
|
31714
31871
|
headless: headless !== void 0 ? headless : currentOptions.headless,
|
|
31715
|
-
startUrl: startUrl ?? currentOptions.startUrl ?? "about:
|
|
31872
|
+
startUrl: startUrl ?? currentOptions.startUrl ?? "about:blank",
|
|
31716
31873
|
prefs: mergedPrefs
|
|
31717
31874
|
};
|
|
31718
31875
|
setNextLaunchOptions(newOptions);
|
|
@@ -31766,7 +31923,7 @@ ${changes.join("\n")}`
|
|
|
31766
31923
|
profilePath: profilePath ?? args.profilePath ?? void 0,
|
|
31767
31924
|
env: newEnv,
|
|
31768
31925
|
headless: headless ?? false,
|
|
31769
|
-
startUrl: startUrl ?? "about:
|
|
31926
|
+
startUrl: startUrl ?? "about:blank"
|
|
31770
31927
|
};
|
|
31771
31928
|
setNextLaunchOptions(newOptions);
|
|
31772
31929
|
const config2 = [`Binary: ${resolvedFirefoxPath}`];
|
|
@@ -31857,7 +32014,7 @@ var init_firefox_management = __esm({
|
|
|
31857
32014
|
},
|
|
31858
32015
|
startUrl: {
|
|
31859
32016
|
type: "string",
|
|
31860
|
-
description: "URL to navigate to after restart (optional, uses about:
|
|
32017
|
+
description: "URL to navigate to after restart (optional, uses about:blank if not specified)"
|
|
31861
32018
|
},
|
|
31862
32019
|
prefs: {
|
|
31863
32020
|
type: "object",
|
|
@@ -32577,7 +32734,9 @@ async function getFirefox() {
|
|
|
32577
32734
|
marionettePort: args.marionettePort,
|
|
32578
32735
|
env: envVars,
|
|
32579
32736
|
logFile: args.outputFile ?? void 0,
|
|
32580
|
-
prefs
|
|
32737
|
+
prefs,
|
|
32738
|
+
androidDevice: args.androidDevice ?? void 0,
|
|
32739
|
+
androidPackage: args.androidPackage ?? void 0
|
|
32581
32740
|
};
|
|
32582
32741
|
}
|
|
32583
32742
|
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
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Firefox DevTools automation",
|
|
5
5
|
"author": "Mozilla",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"build": "tsup",
|
|
16
16
|
"build:moz": "node scripts/generate-moz-package.mjs && tsup --config tsup.config.moz.ts",
|
|
17
17
|
"build:all": "npm run build && npm run build:moz",
|
|
18
|
+
"publish:moz": "node scripts/publish-moz-package.mjs",
|
|
18
19
|
"start": "node dist/index.js",
|
|
19
20
|
"setup": "node scripts/setup-mcp-config.js",
|
|
20
21
|
"clean": "rm -rf dist",
|