@mozilla/firefox-devtools-mcp 0.9.4 → 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.
Files changed (3) hide show
  1. package/README.md +14 -14
  2. package/dist/index.js +133 -46
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Firefox DevTools MCP
2
2
 
3
- [![npm version](https://badge.fury.io/js/firefox-devtools-mcp.svg)](https://www.npmjs.com/package/firefox-devtools-mcp)
3
+ [![npm version](https://badge.fury.io/js/@mozilla%2Ffirefox-devtools-mcp.svg)](https://www.npmjs.com/package/mozilla/firefox-devtools-mcp)
4
4
  [![CI](https://github.com/mozilla/firefox-devtools-mcp/workflows/CI/badge.svg)](https://github.com/mozilla/firefox-devtools-mcp/actions/workflows/ci.yml)
5
5
  [![codecov](https://codecov.io/gh/mozilla/firefox-devtools-mcp/branch/main/graph/badge.svg)](https://codecov.io/gh/mozilla/firefox-devtools-mcp)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE-MIT) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](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:home"
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:
@@ -122,13 +122,13 @@ Use `--android-device` to automate Firefox running on an Android device. Require
122
122
  adb devices
123
123
 
124
124
  # Launch Firefox for Android on the single connected device
125
- npx firefox-devtools-mcp --android-device auto
125
+ npx @mozilla/firefox-devtools-mcp --android-device auto
126
126
 
127
127
  # Target a specific device
128
- npx firefox-devtools-mcp --android-device <serial>
128
+ npx @mozilla/firefox-devtools-mcp --android-device <serial>
129
129
 
130
130
  # Use Firefox Nightly instead
131
- npx firefox-devtools-mcp --android-device <serial> --android-package org.mozilla.fenix
131
+ npx @mozilla/firefox-devtools-mcp --android-device <serial> --android-package org.mozilla.fenix
132
132
  ```
133
133
 
134
134
  Port forwarding between the host and device is handled automatically by geckodriver.
@@ -142,7 +142,7 @@ Use `--connect-existing` to automate your real browsing session — with cookies
142
142
  firefox --marionette
143
143
 
144
144
  # Run the MCP server
145
- npx firefox-devtools-mcp --connect-existing --marionette-port 2828
145
+ npx @mozilla/firefox-devtools-mcp --connect-existing --marionette-port 2828
146
146
  ```
147
147
 
148
148
  Or set `marionette.enabled` to `true` in `about:config` (or `user.js`) to enable Marionette on every launch.
@@ -208,7 +208,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on local development, te
208
208
  "mcpServers": {
209
209
  "firefox-devtools": {
210
210
  "command": "cmd",
211
- "args": ["/c", "npx", "-y", "firefox-devtools-mcp@latest"]
211
+ "args": ["/c", "npx", "-y", "@mozilla/firefox-devtools-mcp@latest"]
212
212
  }
213
213
  }
214
214
  ```
@@ -219,7 +219,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on local development, te
219
219
  "mcpServers": {
220
220
  "firefox-devtools": {
221
221
  "command": "C:\\nvm4w\\nodejs\\npx.ps1",
222
- "args": ["-y", "firefox-devtools-mcp@latest"]
222
+ "args": ["-y", "@mozilla/firefox-devtools-mcp@latest"]
223
223
  }
224
224
  }
225
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:home)",
132
- default: process.env.START_URL ?? "about:home"
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",
@@ -17762,6 +17762,9 @@ var require_utils = __commonJS({
17762
17762
  "use strict";
17763
17763
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
17764
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);
17765
17768
  function stringArrayToHexStripped(input) {
17766
17769
  let acc = "";
17767
17770
  let code = 0;
@@ -17954,27 +17957,77 @@ var require_utils = __commonJS({
17954
17957
  }
17955
17958
  return output.join("");
17956
17959
  }
17957
- function normalizeComponentEncoding(component, esc2) {
17958
- const func = esc2 !== true ? escape : unescape;
17959
- if (component.scheme !== void 0) {
17960
- component.scheme = func(component.scheme);
17961
- }
17962
- if (component.userinfo !== void 0) {
17963
- component.userinfo = func(component.userinfo);
17964
- }
17965
- if (component.host !== void 0) {
17966
- component.host = func(component.host);
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;
17967
17971
  }
17968
- if (component.path !== void 0) {
17969
- component.path = func(component.path);
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];
17970
17989
  }
17971
- if (component.query !== void 0) {
17972
- component.query = func(component.query);
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
+ }
17973
18014
  }
17974
- if (component.fragment !== void 0) {
17975
- component.fragment = func(component.fragment);
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]);
17976
18029
  }
17977
- return component;
18030
+ return output;
17978
18031
  }
17979
18032
  function recomposeAuthority(component) {
17980
18033
  const uriTokens = [];
@@ -17989,7 +18042,7 @@ var require_utils = __commonJS({
17989
18042
  if (ipV6res.isIPV6 === true) {
17990
18043
  host = `[${ipV6res.escapedHost}]`;
17991
18044
  } else {
17992
- host = component.host;
18045
+ host = reescapeHostDelimiters(host, false);
17993
18046
  }
17994
18047
  }
17995
18048
  uriTokens.push(host);
@@ -18003,7 +18056,10 @@ var require_utils = __commonJS({
18003
18056
  module.exports = {
18004
18057
  nonSimpleDomain,
18005
18058
  recomposeAuthority,
18006
- normalizeComponentEncoding,
18059
+ reescapeHostDelimiters,
18060
+ normalizePercentEncoding,
18061
+ normalizePathEncoding,
18062
+ escapePreservingEscapes,
18007
18063
  removeDotSegments,
18008
18064
  isIPv4,
18009
18065
  isUUID,
@@ -18227,12 +18283,12 @@ var require_schemes = __commonJS({
18227
18283
  var require_fast_uri = __commonJS({
18228
18284
  "node_modules/fast-uri/index.js"(exports, module) {
18229
18285
  "use strict";
18230
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
18286
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
18231
18287
  var { SCHEMES, getSchemeHandler } = require_schemes();
18232
18288
  function normalize(uri, options) {
18233
18289
  if (typeof uri === "string") {
18234
18290
  uri = /** @type {T} */
18235
- serialize(parse3(uri, options), options);
18291
+ normalizeString(uri, options);
18236
18292
  } else if (typeof uri === "object") {
18237
18293
  uri = /** @type {T} */
18238
18294
  parse3(serialize(uri, options), options);
@@ -18299,19 +18355,9 @@ var require_fast_uri = __commonJS({
18299
18355
  return target;
18300
18356
  }
18301
18357
  function equal(uriA, uriB, options) {
18302
- if (typeof uriA === "string") {
18303
- uriA = unescape(uriA);
18304
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
18305
- } else if (typeof uriA === "object") {
18306
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
18307
- }
18308
- if (typeof uriB === "string") {
18309
- uriB = unescape(uriB);
18310
- uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
18311
- } else if (typeof uriB === "object") {
18312
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
18313
- }
18314
- 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();
18315
18361
  }
18316
18362
  function serialize(cmpts, opts) {
18317
18363
  const component = {
@@ -18336,12 +18382,12 @@ var require_fast_uri = __commonJS({
18336
18382
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
18337
18383
  if (component.path !== void 0) {
18338
18384
  if (!options.skipEscape) {
18339
- component.path = escape(component.path);
18385
+ component.path = escapePreservingEscapes(component.path);
18340
18386
  if (component.scheme !== void 0) {
18341
18387
  component.path = component.path.split("%3A").join(":");
18342
18388
  }
18343
18389
  } else {
18344
- component.path = unescape(component.path);
18390
+ component.path = normalizePercentEncoding(component.path);
18345
18391
  }
18346
18392
  }
18347
18393
  if (options.reference !== "suffix" && component.scheme) {
@@ -18376,7 +18422,16 @@ var require_fast_uri = __commonJS({
18376
18422
  return uriTokens.join("");
18377
18423
  }
18378
18424
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
18379
- function parse3(uri, opts) {
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) {
18380
18435
  const options = Object.assign({}, opts);
18381
18436
  const parsed = {
18382
18437
  scheme: void 0,
@@ -18387,6 +18442,7 @@ var require_fast_uri = __commonJS({
18387
18442
  query: void 0,
18388
18443
  fragment: void 0
18389
18444
  };
18445
+ let malformedAuthorityOrPort = false;
18390
18446
  let isIP = false;
18391
18447
  if (options.reference === "suffix") {
18392
18448
  if (options.scheme) {
@@ -18407,6 +18463,11 @@ var require_fast_uri = __commonJS({
18407
18463
  if (isNaN(parsed.port)) {
18408
18464
  parsed.port = matches[5];
18409
18465
  }
18466
+ const parseError = getParseError(parsed, matches);
18467
+ if (parseError !== void 0) {
18468
+ parsed.error = parsed.error || parseError;
18469
+ malformedAuthorityOrPort = true;
18470
+ }
18410
18471
  if (parsed.host) {
18411
18472
  const ipv4result = isIPv4(parsed.host);
18412
18473
  if (ipv4result === false) {
@@ -18445,14 +18506,18 @@ var require_fast_uri = __commonJS({
18445
18506
  parsed.scheme = unescape(parsed.scheme);
18446
18507
  }
18447
18508
  if (parsed.host !== void 0) {
18448
- parsed.host = unescape(parsed.host);
18509
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
18449
18510
  }
18450
18511
  }
18451
18512
  if (parsed.path) {
18452
- parsed.path = escape(unescape(parsed.path));
18513
+ parsed.path = normalizePathEncoding(parsed.path);
18453
18514
  }
18454
18515
  if (parsed.fragment) {
18455
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
18516
+ try {
18517
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
18518
+ } catch {
18519
+ parsed.error = parsed.error || "URI malformed";
18520
+ }
18456
18521
  }
18457
18522
  }
18458
18523
  if (schemeHandler && schemeHandler.parse) {
@@ -18461,7 +18526,29 @@ var require_fast_uri = __commonJS({
18461
18526
  } else {
18462
18527
  parsed.error = parsed.error || "URI can not be parsed.";
18463
18528
  }
18464
- 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
+ }
18465
18552
  }
18466
18553
  var fastUri = {
18467
18554
  SCHEMES,
@@ -31782,7 +31869,7 @@ async function handleRestartFirefox(input) {
31782
31869
  profilePath: profilePath ?? currentOptions.profilePath,
31783
31870
  env: newEnv !== void 0 ? newEnv : currentOptions.env,
31784
31871
  headless: headless !== void 0 ? headless : currentOptions.headless,
31785
- startUrl: startUrl ?? currentOptions.startUrl ?? "about:home",
31872
+ startUrl: startUrl ?? currentOptions.startUrl ?? "about:blank",
31786
31873
  prefs: mergedPrefs
31787
31874
  };
31788
31875
  setNextLaunchOptions(newOptions);
@@ -31836,7 +31923,7 @@ ${changes.join("\n")}`
31836
31923
  profilePath: profilePath ?? args.profilePath ?? void 0,
31837
31924
  env: newEnv,
31838
31925
  headless: headless ?? false,
31839
- startUrl: startUrl ?? "about:home"
31926
+ startUrl: startUrl ?? "about:blank"
31840
31927
  };
31841
31928
  setNextLaunchOptions(newOptions);
31842
31929
  const config2 = [`Binary: ${resolvedFirefoxPath}`];
@@ -31927,7 +32014,7 @@ var init_firefox_management = __esm({
31927
32014
  },
31928
32015
  startUrl: {
31929
32016
  type: "string",
31930
- description: "URL to navigate to after restart (optional, uses about:home if not specified)"
32017
+ description: "URL to navigate to after restart (optional, uses about:blank if not specified)"
31931
32018
  },
31932
32019
  prefs: {
31933
32020
  type: "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozilla/firefox-devtools-mcp",
3
- "version": "0.9.4",
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",