/** * CLI argument parsing for Firefox DevTools MCP server */ import type { Options as YargsOptions } from 'yargs'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; /** * Parsed preference value (boolean, integer, or string) */ export type PrefValue = string | number | boolean; /** * Parse preference strings into typed values * Format: "name=value" where value is auto-typed as boolean/integer/string */ export function parsePrefs(prefs: string[] | undefined): Record { const result: Record = {}; if (!prefs || prefs.length === 0) { return result; } for (const pref of prefs) { const eqIndex = pref.indexOf('='); if (eqIndex === -1) { // Skip malformed entries (no equals sign) continue; } const name = pref.slice(0, eqIndex); const rawValue = pref.slice(eqIndex + 1); // Type inference let value: PrefValue; if (rawValue === 'true') { value = true; } else if (rawValue === 'false') { value = false; } else if (/^-?\d+$/.test(rawValue)) { value = parseInt(rawValue, 10); } else { value = rawValue; } result[name] = value; } return result; } export const cliOptions = { firefoxPath: { type: 'string', description: 'Path to Firefox executable (optional, uses system Firefox if not specified)', alias: 'f', }, headless: { type: 'boolean', description: 'Whether to run Firefox in headless (no UI) mode', default: (process.env.FIREFOX_HEADLESS ?? 'false') === 'true', }, viewport: { type: 'string', description: 'Initial viewport size for Firefox instances. For example, `1280x720`. In headless mode, max size is 3840x2160px.', coerce: (arg: string | undefined) => { if (arg === undefined) { return; } const [width, height] = arg.split('x').map(Number); if (!width || !height || Number.isNaN(width) || Number.isNaN(height)) { throw new Error('Invalid viewport. Expected format is `1280x720`.'); } return { width, height, }; }, }, acceptInsecureCerts: { type: 'boolean', description: 'If enabled, ignores errors relative to self-signed and expired certificates. Use with caution.', default: (process.env.ACCEPT_INSECURE_CERTS ?? 'false') === 'true', }, profilePath: { type: 'string', description: 'Path to Firefox profile directory (optional, for persistent profile)', }, firefoxArg: { type: 'array', description: 'Additional arguments for Firefox. Only applies when Firefox is launched by firefox-devtools-mcp.', }, startUrl: { type: 'string', description: 'URL to open when Firefox starts (default: about:home)', default: process.env.START_URL ?? 'about:home', }, connectExisting: { type: 'boolean', description: 'Connect to an already-running Firefox instance via Marionette instead of launching a new one. Requires Firefox to be running with marionette.enabled=true (set in user.js or launched with --marionette).', default: (process.env.CONNECT_EXISTING ?? 'false') === 'true', }, marionettePort: { type: 'number', description: 'Marionette port to connect to when using --connect-existing (default: 2828)', default: Number(process.env.MARIONETTE_PORT ?? '2828'), }, env: { type: 'array', description: 'Environment variables for Firefox in KEY=VALUE format. Can be specified multiple times. Example: --env MOZ_LOG=HTMLMediaElement:4', }, outputFile: { type: 'string', description: 'Path to file where Firefox output (stdout/stderr) will be written. If not specified, output is written to ~/.firefox-devtools-mcp/output/', }, pref: { type: 'array', string: true, description: 'Set Firefox preference at startup via moz:firefoxOptions (format: name=value). Can be specified multiple times.', alias: 'p', }, enableScript: { type: 'boolean', description: 'Enable the evaluate_script tool, which allows executing arbitrary JavaScript in the page context.', default: (process.env.ENABLE_SCRIPT ?? 'false') === 'true', }, enablePrivilegedContext: { type: 'boolean', description: '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.', default: (process.env.ENABLE_PRIVILEGED_CONTEXT ?? 'false') === 'true', }, enableBridge: { type: 'boolean', description: "Enable host-network bridge tools (connect/disconnect_host_network) that tunnel the browser to a remote host's localhost via the firefox-bridge P2P transport. Requires --enable-privileged-context.", default: (process.env.ENABLE_BRIDGE ?? 'false') === 'true', }, screenshotWaitMs: { type: 'number', description: 'Max milliseconds the auto-screenshot middleware waits for visual readiness (document loaded, body visible, viewport images decoded) before capturing. 0 disables the wait. On timeout the screenshot is taken anyway with a note about what was still pending.', default: Number(process.env.FIREFOX_SCREENSHOT_WAIT_MS ?? '8000'), }, screenshotMaxEdge: { type: 'number', description: 'Long-edge pixel budget for a screenshot an agent asked for by name. Image tokens are charged by area (about width x height / 750), so a 1920px capture costs ~2.5k tokens where 1024px costs ~720 and still reads every glyph on a dense page. 0 disables downscaling.', default: Number(process.env.FIREFOX_SCREENSHOT_MAX_EDGE ?? '1024'), }, screenshotAutoMaxEdge: { type: 'number', description: 'Long-edge pixel budget for pictures nobody asked for: the one bundled with every mutation and the one attached to a refusal. These fire far more often than explicit captures, so they carry a smaller budget by default. 0 disables downscaling.', default: Number(process.env.FIREFOX_SCREENSHOT_AUTO_MAX_EDGE ?? '800'), }, navTimeoutMs: { type: 'number', description: 'Max milliseconds a document-unloading command (navigate_page, new_page, close_page, navigate_history) may run before failing with a diagnostic. Guards against a modal the driver cannot reach hanging the whole server. 0 disables the watchdog.', default: Number(process.env.FIREFOX_NAV_TIMEOUT_MS ?? '20000'), }, http: { type: 'boolean', description: 'Serve MCP over streamable HTTP instead of stdio. Intended for containerised deployments where the server is co-located with Firefox and clients connect over the network.', default: (process.env.MCP_HTTP ?? 'false') === 'true', }, port: { type: 'number', description: 'TCP port for the HTTP transport (default: 8931). Ignored unless --http.', default: Number(process.env.MCP_HTTP_PORT ?? '8931'), }, host: { type: 'string', description: 'Bind address for the HTTP transport (default: 0.0.0.0 so the port can be published out of a container). Ignored unless --http.', default: process.env.MCP_HTTP_HOST ?? '0.0.0.0', }, token: { type: 'string', description: 'Shared bearer token required in the Authorization header of every HTTP request. Empty string disables the check. Ignored unless --http.', default: process.env.MCP_HTTP_TOKEN ?? '', }, } satisfies Record; export function parseArguments(version: string, argv = process.argv) { const yargsInstance = yargs(hideBin(argv)) .scriptName('npx firefox-devtools-mcp@latest') .options(cliOptions) .example([ [ '$0 --firefox-path /Applications/Firefox.app/Contents/MacOS/firefox', 'Use specific Firefox', ], ['$0 --headless', 'Run Firefox in headless mode'], ['$0 --viewport 1280x720', 'Launch Firefox with viewport size of 1280x720px'], ['$0 --help', 'Print CLI options'], ]); return yargsInstance .wrap(Math.min(120, yargsInstance.terminalWidth())) .help() .version(version) .parseSync(); }