Bug 2018245 - Give firefox-devtools-mcp better defaults r=ai4dev-reviewers,padenot

Differential Revision: https://phabricator.services.mozilla.com/D294087
This commit is contained in:
Dan Mosedale
2026-06-02 07:19:43 +00:00
committed by jdescottes@mozilla.com
parent d0b8a022bc
commit 73620ff460
6 changed files with 206 additions and 6 deletions
+1 -1
View File
@@ -3,4 +3,4 @@ url = "https://mcp-dev.moz.tools/mcp"
[mcp_servers.firefox-devtools]
command = "./mach"
args = ["npx", "@mozilla/firefox-devtools-mcp-moz", "--pref", "browser.ml.enable=true", "--enable-scripting", "--enable-privileged-context"]
args = ["npx", "@mozilla/firefox-devtools-mcp-moz", "--pref", "browser.ml.enable=true", "--enable-script", "--enable-privileged-context"]
+6 -5
View File
@@ -9,12 +9,13 @@
"command": "./mach",
"args": [
"--quiet",
"npx",
"@mozilla/firefox-devtools-mcp-moz",
"devtools-mcp",
"--pref",
"browser.ml.enable=true",
"--enable-scripting",
"--enable-privileged-context"
"remote.prefs.recommended=false",
"--pref",
"browser.smartwindow.enabled=true",
"--enable-privileged-context",
"--enable-script"
]
}
}
+1
View File
@@ -95,6 +95,7 @@ MACH_COMMANDS = {
"data-review": MachCommandReference(
"toolkit/components/glean/build_scripts/mach_commands.py"
),
"devtools-mcp": MachCommandReference("tools/mach_commands.py"),
"devtools-node-test": MachCommandReference("devtools/mach_commands.py"),
"doc": MachCommandReference("tools/moztreedocs/mach_commands.py"),
"doctor": MachCommandReference("python/mozbuild/mozbuild/mach_commands.py"),
@@ -91,6 +91,8 @@ subsuite = "mozbuild"
["test_containers.py"]
["test_devtools_mcp.py"]
["test_dotproperties.py"]
["test_expression.py"]
@@ -0,0 +1,161 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import sys
import unittest
from unittest.mock import MagicMock, patch
import buildconfig
from mach.registrar import Registrar
from mozunit import main
sys.path.insert(0, os.path.join(buildconfig.topsrcdir, "tools"))
class TestDevtoolsMcp(unittest.TestCase):
def setUp(self):
self.remove_cats = []
for cat in ("build", "post-build", "misc", "testing", "devenv"):
if cat in Registrar.categories:
continue
Registrar.register_category(cat, cat, cat)
self.remove_cats.append(cat)
def tearDown(self):
for cat in self.remove_cats:
del Registrar.categories[cat]
del Registrar.commands_by_category[cat]
def _mock_context(self, topobjdir="/obj", binary_path="/obj/dist/bin/firefox"):
ctx = MagicMock()
ctx.topobjdir = topobjdir
ctx.get_binary_path.return_value = binary_path
return ctx
@patch("os.path.exists", return_value=True)
@patch("mach_commands.npx")
def test_delegates_to_npx_with_build_args(self, mock_npx, mock_exists):
from mach_commands import devtools_mcp
mock_context = self._mock_context()
mock_npx.return_value = 0
devtools_mcp(mock_context, args=["--pref", "x=1"])
mock_context.get_binary_path.assert_called_once_with(validate_exists=True)
mock_npx.assert_called_once_with(
mock_context,
[
"@mozilla/firefox-devtools-mcp-moz",
"--firefox-path",
"/obj/dist/bin/firefox",
"--profile-path",
"/obj/tmp/profile-default",
"--pref",
"x=1",
],
)
@patch("mach_commands.npx")
def test_starts_without_binary_when_no_build(self, mock_npx):
from mach_commands import devtools_mcp
from mozbuild.base import BinaryNotFoundException
mock_context = self._mock_context()
mock_context.get_binary_path.side_effect = BinaryNotFoundException(
"/obj/dist/bin/firefox"
)
mock_npx.return_value = 0
result = devtools_mcp(mock_context, args=[])
self.assertEqual(result, 0)
mock_npx.assert_called_once_with(
mock_context,
["@mozilla/firefox-devtools-mcp-moz"],
)
@patch("mach_commands.npx")
def test_starts_without_binary_when_no_build_environment(self, mock_npx):
from mach_commands import devtools_mcp
from mozbuild.base import BuildEnvironmentNotFoundException
mock_context = self._mock_context()
mock_context.get_binary_path.side_effect = BuildEnvironmentNotFoundException(
"no build environment"
)
mock_npx.return_value = 0
result = devtools_mcp(mock_context, args=[])
self.assertEqual(result, 0)
mock_npx.assert_called_once_with(
mock_context,
["@mozilla/firefox-devtools-mcp-moz"],
)
@patch("os.path.exists", return_value=True)
@patch("mach_commands.npx")
def test_works_with_no_extra_args(self, mock_npx, mock_exists):
from mach_commands import devtools_mcp
mock_context = self._mock_context()
mock_npx.return_value = 0
devtools_mcp(mock_context, args=[])
mock_npx.assert_called_once_with(
mock_context,
[
"@mozilla/firefox-devtools-mcp-moz",
"--firefox-path",
"/obj/dist/bin/firefox",
"--profile-path",
"/obj/tmp/profile-default",
],
)
@patch("os.path.exists", return_value=True)
@patch("mach_commands.npx")
def test_profile_path_derives_from_topobjdir(self, mock_npx, mock_exists):
from mach_commands import devtools_mcp
mock_context = self._mock_context(
topobjdir="/builds/objdir",
binary_path="/somewhere/else/dist/bin/firefox",
)
mock_npx.return_value = 0
devtools_mcp(mock_context, args=[])
call_args = mock_npx.call_args[0][1]
profile_idx = call_args.index("--profile-path")
self.assertEqual(
call_args[profile_idx + 1], "/builds/objdir/tmp/profile-default"
)
@patch("os.path.exists", return_value=True)
@patch("mach_commands.npx")
def test_skips_binary_detection_when_firefox_path_provided(
self, mock_npx, mock_exists
):
from mach_commands import devtools_mcp
mock_context = self._mock_context()
mock_npx.return_value = 0
devtools_mcp(mock_context, args=["--firefox-path", "/custom/firefox"])
mock_context.get_binary_path.assert_not_called()
mock_npx.assert_called_once_with(
mock_context,
["@mozilla/firefox-devtools-mcp-moz", "--firefox-path", "/custom/firefox"],
)
if __name__ == "__main__":
main()
+35
View File
@@ -310,6 +310,41 @@ def npx(command_context, args):
)
@Command(
"devtools-mcp",
category="devenv",
description="Run the firefox-devtools-mcp server with the local build.",
)
@CommandArgument("args", nargs=argparse.REMAINDER)
def devtools_mcp(command_context, args):
import os
from pathlib import Path
from mozbuild.base import BinaryNotFoundException, BuildEnvironmentNotFoundException
extra_args = []
if "--firefox-path" not in args and "--firefoxPath" not in args:
binary_path = None
try:
binary_path = command_context.get_binary_path(validate_exists=True)
except (BuildEnvironmentNotFoundException, BinaryNotFoundException) as e:
print(f"Warning: Local build not found: {e}")
if binary_path and os.path.exists(binary_path):
extra_args += ["--firefox-path", binary_path]
if "--profile-path" not in args and "--profilePath" not in args:
profile_path = (
Path(command_context.topobjdir) / "tmp" / "profile-default"
).as_posix()
extra_args += ["--profile-path", profile_path]
return npx(
command_context,
["@mozilla/firefox-devtools-mcp-moz"] + extra_args + args,
)
def logspam_create_parser(subcommand):
# Create the logspam command line parser.
# if logspam is not installed, or not up to date, it will