Loading...
Loading...
Automate and control Firefox browser through MCP using WebDriver BiDi for AI-assisted web testing, scraping, and interaction
npx skill4agent add aradotso/devtools-skills firefox-devtools-mcp-automationSkill by ara.so — Devtools Skills collection.
--enable-script--enable-privileged-context# Add to Claude Code
claude mcp add firefox-devtools npx firefox-devtools-mcp@latest
# With options
claude mcp add firefox-devtools npx firefox-devtools-mcp@latest -- --headless --viewport 1280x720~/Library/Application Support/Claude/Code/mcp_settings.json{
"mcpServers": {
"firefox-devtools": {
"command": "npx",
"args": ["-y", "firefox-devtools-mcp@latest", "--headless"],
"env": {
"START_URL": "about:home",
"FIREFOX_HEADLESS": "true"
}
}
}
}npx @modelcontextprotocol/inspector npx firefox-devtools-mcp@latest --start-url https://example.com --headless# Basic options
--firefox-path /path/to/firefox # Custom Firefox binary path
--headless # Run without UI
--viewport 1280x720 # Initial window size
--profile-path /path/to/profile # Use specific Firefox profile
--start-url https://example.com # Open URL on start
# Security options
--accept-insecure-certs # Ignore TLS errors
--enable-script # Enable evaluate_script tool
--enable-privileged-context # Enable privileged tools
# Connection options
--connect-existing # Attach to running Firefox
--marionette-port 2828 # Marionette port (default: 2828)
# Advanced options
--firefox-arg --arg-name # Extra Firefox arguments (repeatable)
--pref name=value # Set Firefox preference (repeatable)FIREFOX_HEADLESS=true
START_URL=https://example.com
ACCEPT_INSECURE_CERTS=true
CONNECT_EXISTING=true
MARIONETTE_PORT=2828
ENABLE_SCRIPT=true
ENABLE_PRIVILEGED_CONTEXT=true// List all open pages/tabs
list_pages()
// Returns: { pages: [{ id, url, title, isActive }] }
// Create new page
new_page({ url: "https://example.com" })
// Returns: { pageId, url }
// Navigate existing page
navigate_page({
pageId: "page-id-123",
url: "https://example.com/login"
})
// Switch active page
select_page({ pageId: "page-id-123" })
// Close page
close_page({ pageId: "page-id-123" })// Take snapshot of current page
take_snapshot()
// Returns: {
// snapshot: "Visual representation with [uid] markers",
// elements: [{ uid, role, name, tagName, coordinates }]
// }
// Click element by UID
click_by_uid({ uid: "abc123" })
// Fill input field
fill_by_uid({
uid: "def456",
value: "user@example.com"
})
// Hover over element
hover_by_uid({ uid: "ghi789" })
// Drag and drop
drag_by_uid({
sourceUid: "drag-me",
targetUid: "drop-here"
})
// Upload file
upload_by_uid({
uid: "file-input",
filePath: "/path/to/file.pdf"
})
// Fill entire form at once
fill_form_by_uids({
fields: [
{ uid: "email-field", value: "user@example.com" },
{ uid: "password-field", value: "secure-password" },
{ uid: "submit-button", action: "click" }
]
})
// Clear UIDs after navigation
clear_uids()// Screenshot entire page
screenshot_page()
// Returns: { screenshot: "base64-encoded-png" }
// Save to disk (recommended for Claude Code to save context)
screenshot_page({
saveTo: "/tmp/page.png"
})
// Returns: { filePath: "/tmp/page.png" }
// Screenshot specific element
screenshot_by_uid({
uid: "abc123",
saveTo: "/tmp/element.png"
})// List all captured network requests
list_network_requests()
// Returns: {
// requests: [{
// id, method, url, status,
// contentType, responseSize, duration
// }]
// }
// Filter requests
list_network_requests({
filter: {
method: "POST",
status: 200,
urlPattern: "api.example.com"
}
})
// Get detailed request/response
get_network_request({ requestId: "req-123" })
// Returns: {
// request: { method, url, headers, body },
// response: { status, headers, body }
// }// List console logs
list_console_messages()
// Returns: {
// messages: [{
// level, text, timestamp,
// url, lineNumber
// }]
// }
// Clear console
clear_console_messages()--enable-script// Execute JavaScript in page context
evaluate_script({
script: "document.title"
})
// Returns: { result: "Page Title" }
// Access DOM
evaluate_script({
script: `
const button = document.querySelector('.submit-btn');
return button ? button.textContent : null;
`
})
// Modify page
evaluate_script({
script: `
document.body.style.backgroundColor = 'lightblue';
return 'Background changed';
`
})// Navigate history
history_back()
history_forward()
// Set viewport size
set_viewport({ width: 1920, height: 1080 })
// Handle dialogs
accept_dialog({ promptText: "optional text for prompts" })
dismiss_dialog()
// Get Firefox info
get_firefox_info()
// Returns: { version, buildId, profilePath, binaryPath }
// View Firefox console output
get_firefox_output()
// Returns: { stdout, stderr }
// Restart Firefox
restart_firefox()// 1. Navigate to login page
navigate_page({
pageId: "page-1",
url: "https://app.example.com/login"
})
// 2. Take snapshot to get UIDs
const snap = take_snapshot()
// Inspect snap.snapshot to find email/password/submit UIDs
// 3. Fill and submit form
fill_form_by_uids({
fields: [
{ uid: "email-uid", value: process.env.USER_EMAIL },
{ uid: "password-uid", value: process.env.USER_PASSWORD },
{ uid: "submit-uid", action: "click" }
]
})
// 4. Wait and verify success
// (add delay or check for redirect)// 1. Navigate to page
navigate_page({
pageId: "page-1",
url: "https://api-site.example.com/dashboard"
})
// 2. Interact to trigger API calls
const snap = take_snapshot()
click_by_uid({ uid: "load-more-button-uid" })
// 3. Capture API responses
const requests = list_network_requests({
filter: {
urlPattern: "api.example.com/v1/data",
method: "GET"
}
})
// 4. Extract data from response
const apiData = get_network_request({
requestId: requests.requests[0].id
})
// Parse apiData.response.body// 1. Navigate to page
navigate_page({
pageId: "page-1",
url: "https://example.com/component"
})
// 2. Take baseline screenshot
screenshot_page({ saveTo: "/tmp/baseline.png" })
// 3. Make changes via script or interaction
evaluate_script({
script: `document.querySelector('.hero').style.fontSize = '24px'`
})
// 4. Take comparison screenshot
screenshot_page({ saveTo: "/tmp/comparison.png" })
// 5. Use image diff tool externally// Multi-step form with snapshots at each step
const pages = list_pages()
const pageId = pages.pages[0].id
// Step 1
let snap = take_snapshot()
fill_form_by_uids({
fields: [
{ uid: "first-name-uid", value: "John" },
{ uid: "last-name-uid", value: "Doe" },
{ uid: "next-btn-uid", action: "click" }
]
})
// Step 2 - take fresh snapshot after navigation
snap = take_snapshot()
fill_form_by_uids({
fields: [
{ uid: "address-uid", value: "123 Main St" },
{ uid: "city-uid", value: "Anytown" },
{ uid: "submit-uid", action: "click" }
]
})--enable-privileged-contextMOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1// List privileged contexts (chrome contexts)
list_privileged_contexts()
// Returns: { contexts: [{ id, type }] }
// Select privileged context
select_privileged_context({ contextId: "chrome-123" })
// Execute privileged JavaScript
evaluate_privileged_script({
script: `
Services.prefs.setIntPref("browser.cache.disk.capacity", 1024000);
return "Pref set";
`
})
// Get Firefox preferences
get_firefox_prefs({ prefNames: ["browser.cache.disk.capacity"] })
// Returns: { prefs: { "browser.cache.disk.capacity": 1024000 } }
// Set Firefox preferences
set_firefox_prefs({
prefs: {
"browser.cache.disk.capacity": 2048000,
"privacy.trackingprotection.enabled": true
}
})// Install extension
install_extension({
xpiPath: "/path/to/extension.xpi"
})
// Returns: { extensionId: "extension-uuid" }
// Uninstall extension
uninstall_extension({ extensionId: "extension-uuid" })
// List installed extensions (requires privileged context)
list_extensions()
// Returns: { extensions: [{ id, name, version }] }# Terminal 1: Start Firefox with Marionette
firefox --marionette
# Terminal 2: Connect MCP server
npx firefox-devtools-mcp@latest --connect-existing --marionette-port 2828# macOS
npx firefox-devtools-mcp@latest --firefox-path "/Applications/Firefox.app/Contents/MacOS/firefox"
# Linux
npx firefox-devtools-mcp@latest --firefox-path "/usr/bin/firefox"
# Windows
npx firefox-devtools-mcp@latest --firefox-path "C:\Program Files\Mozilla Firefox\firefox.exe"navigate_page({ pageId: "page-1", url: "https://new-page.com" })
clear_uids() // Clear old UIDs
const snap = take_snapshot() // Get fresh UIDscmd /c{
"mcpServers": {
"firefox-devtools": {
"command": "cmd",
"args": ["/c", "npx", "-y", "firefox-devtools-mcp@latest"]
}
}
}{
"mcpServers": {
"firefox-devtools": {
"command": "C:\\Program Files\\nodejs\\npx.cmd",
"args": ["-y", "firefox-devtools-mcp@latest"]
}
}
}# Run with inspector to see detailed logs
npx @modelcontextprotocol/inspector npx firefox-devtools-mcp@latest --start-url about:home
# Check Firefox is running
ps aux | grep firefox
# Verify Marionette port
lsof -i :2828// Create new page
const { pageId } = new_page({ url: "https://example.com" })
// Navigate to search
navigate_page({ pageId, url: "https://example.com/search" })
// Take snapshot and find search box
const snap = take_snapshot()
// Review snap.snapshot to identify UIDs
// Perform search
fill_form_by_uids({
fields: [
{ uid: "search-input-uid", value: "test query" },
{ uid: "search-button-uid", action: "click" }
]
})
// Capture results
const screenshot = screenshot_page({ saveTo: "/tmp/results.png" })
// Verify API calls
const requests = list_network_requests({
filter: { urlPattern: "example.com/api/search" }
})
const searchData = get_network_request({
requestId: requests.requests[0].id
})
// Check console for errors
const logs = list_console_messages()
const errors = logs.messages.filter(m => m.level === "error")
// Cleanup
close_page({ pageId })