import { Option, program } from 'commander';
// @ts-expect-error - playwright-core internal module without proper types
import { startTraceViewerServer } from 'playwright-core/lib/server';
import { contextFactory } from './browser-context-factory.js';
import {
BrowserServerBackend,
type FactoryList,
} from './browser-server-backend.js';
import {
commaSeparatedList,
resolveCLIConfig,
semicolonSeparatedList,
} from './config.js';
import { Context } from './context.js';
import {
createExtensionContextFactory,
runWithExtension,
} from './extension/main.js';
import { runLoopTools } from './loopTools/main.js';
import type { ServerBackendFactory } from './mcp/server.js';
import { start } from './mcp/transport.js';
import { programDebug } from './utils/log.js';
import { packageJSON } from './utils/package.js';
import { logServerStart } from './utils/request-logger.js';
program
.version(`Version ${packageJSON.version}`)
.name(packageJSON.name)
.option(
'--allowed-origins <origins>',
'semicolon-separated list of origins to allow the browser to request. Default is to allow all.',
semicolonSeparatedList
)
.option(
'--blocked-origins <origins>',
'semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.',
semicolonSeparatedList
)
.option('--block-service-workers', 'block service workers')
.option(
'--browser <browser>',
'browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.'
)
.option(
'--caps <caps>',
'comma-separated list of additional capabilities to enable, possible values: vision, pdf.',
commaSeparatedList
)
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
.option('--config <path>', 'path to the configuration file.')
.option('--device <device>', 'device to emulate, for example: "iPhone 15"')
.option('--executable-path <path>', 'path to the browser executable.')
.option('--headless', 'run browser in headless mode, headed by default')
.option(
'--host <host>',
'host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.'
)
.option('--ignore-https-errors', 'ignore https errors')
.option(
'--isolated',
'keep the browser profile in memory, do not save it to disk.'
)
.option(
'--image-responses <mode>',
'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".'
)
.option(
'--no-sandbox',
'disable the sandbox for all process types that are normally sandboxed.'
)
.option('--output-dir <path>', 'path to the directory for output files.')
.option('--port <port>', 'port to listen on for SSE transport.')
.option(
'--proxy-bypass <bypass>',
'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"'
)
.option(
'--proxy-server <proxy>',
'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"'
)
.option(
'--save-session',
'Whether to save the Playwright MCP session into the output directory.'
)
.option(
'--save-trace',
'Whether to save the Playwright Trace of the session into the output directory.'
)
.option(
'--storage-state <path>',
'path to the storage state file for isolated sessions.'
)
.option('--user-agent <ua string>', 'specify user agent string')
.option(
'--user-data-dir <path>',
'path to the user data directory. If not specified, a temporary directory will be created.'
)
.option(
'--viewport-size <size>',
'specify browser viewport size in pixels, for example "1280, 720"'
)
.addOption(
new Option(
'--extension',
'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.'
).hideHelp()
)
.addOption(
new Option(
'--connect-tool',
'Allow to switch between different browser connection methods.'
).hideHelp()
)
.addOption(new Option('--loop-tools', 'Run loop tools').hideHelp())
.addOption(
new Option(
'--vision',
'Legacy option, use --caps=vision instead'
).hideHelp()
)
.action(async (options) => {
setupExitWatchdog();
if (options.vision) {
options.caps = 'vision';
}
try {
const config = await resolveCLIConfig(options);
if (options.extension) {
await runWithExtension(config);
return;
}
if (options.loopTools) {
await runLoopTools(config);
return;
}
const browserContextFactory = contextFactory(config);
let serverBackendFactory: ServerBackendFactory;
if (options.connectTool) {
// Use our FactoryList approach for multi-factory support
const factories: FactoryList = [
browserContextFactory,
createExtensionContextFactory(config),
];
serverBackendFactory = () =>
new BrowserServerBackend(config, factories);
} else {
// Single factory for regular usage
const factories: FactoryList = [browserContextFactory];
serverBackendFactory = () =>
new BrowserServerBackend(config, factories);
}
logServerStart();
await start(serverBackendFactory, config.server);
if (config.saveTrace) {
const server = await startTraceViewerServer();
const urlPrefix = server.urlPrefix('human-readable');
const url =
urlPrefix +
'/trace/index.html?trace=' +
config.browser.launchOptions.tracesDir +
'/trace.json';
programDebug(`Trace viewer available at: ${url}`);
}
} catch (error) {
// CLI action failed - output error to stderr and exit with error code
process.stderr.write(
`${error instanceof Error ? error.message : String(error)}\n`
);
process.exit(1);
}
});
function setupExitWatchdog() {
let isExiting = false;
const handleExit = async () => {
if (isExiting) {
return;
}
isExiting = true;
setTimeout(() => process.exit(0), 15_000);
await Context.disposeAll();
process.exit(0);
};
process.stdin.on('close', handleExit);
process.on('SIGINT', handleExit);
process.on('SIGTERM', handleExit);
}
program.parseAsync(process.argv).catch(() => {
// Program execution failed - exit with error code
process.exit(1);
});