@dorothywebb/any-browser-mcp
Version:
Any Browser MCP - Launch Chrome with your actual data in debug mode for comprehensive browser automation
387 lines • 16 kB
JavaScript
/**
* Chrome Launcher - Manages separate Chrome instances for MCP
* Launches Chrome with debugging in a separate profile to avoid interfering with user's main Chrome
*/
import { spawn } from 'child_process';
import { existsSync, mkdirSync, copyFileSync, statSync, cpSync } from 'fs';
import { join, resolve, dirname } from 'path';
import { platform, homedir } from 'os';
import http from 'http';
import { getConfigManager } from '../core/ConfigManager.js';
import { BrowserConnectionError } from '../types/index.js';
export class ChromeLauncher {
config = getConfigManager();
runningInstances = new Map();
verbose;
constructor(verbose = false) {
this.verbose = verbose;
}
/**
* Check if Chrome is available for debugging on the specified port
*/
async isDebugPortAvailable(port) {
return new Promise((resolve) => {
const req = http.get(`http://localhost:${port}/json/version`, { timeout: 2000 }, (res) => {
resolve(res.statusCode === 200);
});
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
});
}
/**
* Get the default Chrome user data directory for the current platform
*/
getDefaultChromeUserDataDir() {
const platformName = platform();
const home = homedir();
const defaultPaths = {
win32: join(home, 'AppData', 'Local', 'Google', 'Chrome', 'User Data'),
darwin: join(home, 'Library', 'Application Support', 'Google', 'Chrome'),
linux: join(home, '.config', 'google-chrome')
};
return defaultPaths[platformName] || defaultPaths.linux;
}
/**
* Find Chrome executable path for the current platform
*/
getChromePath() {
const platformName = platform();
const chromePaths = {
win32: [
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
process.env.CHROME_PATH
],
darwin: [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
process.env.CHROME_PATH
],
linux: [
'google-chrome',
'google-chrome-stable',
'chromium-browser',
'chromium',
process.env.CHROME_PATH
]
};
const paths = chromePaths[platformName] || chromePaths.linux;
for (const path of paths) {
if (path && existsSync(path)) {
return path;
}
}
// Fallback to system Chrome
return platformName === 'win32' ? 'chrome.exe' : 'google-chrome';
}
/**
* Copy user's Chrome data to MCP profile directory
*/
async copyUserChromeData(sourceDataDir, targetProfilePath) {
if (!existsSync(sourceDataDir)) {
if (this.verbose) {
console.log(`⚠️ Source Chrome data directory not found: ${sourceDataDir}`);
console.log(` Creating clean MCP profile instead`);
}
return;
}
if (this.verbose) {
console.log(`📋 Copying Chrome user data to MCP profile...`);
console.log(` Source: ${sourceDataDir}`);
console.log(` Target: ${targetProfilePath}`);
}
try {
// Important files/folders to copy for user experience
const importantItems = [
'Default/Bookmarks',
'Default/Preferences',
'Default/Login Data',
'Default/Web Data',
'Default/History',
'Default/Cookies',
'Default/Extensions',
'Default/Local Extension Settings',
'Default/Sync Extension Settings',
'Default/Extension State',
'Local State'
];
// Create target directory structure
mkdirSync(join(targetProfilePath, 'Default'), { recursive: true });
// Copy important user data files
for (const item of importantItems) {
const sourcePath = join(sourceDataDir, item);
const targetPath = join(targetProfilePath, item);
if (existsSync(sourcePath)) {
try {
const sourceStats = statSync(sourcePath);
if (sourceStats.isDirectory()) {
// Copy directory recursively
cpSync(sourcePath, targetPath, { recursive: true, force: true });
if (this.verbose) {
console.log(` ✅ Copied directory: ${item}`);
}
}
else {
// Copy file
mkdirSync(dirname(targetPath), { recursive: true });
copyFileSync(sourcePath, targetPath);
if (this.verbose) {
console.log(` ✅ Copied file: ${item}`);
}
}
}
catch (error) {
// Some files might be locked by running Chrome, that's okay
if (this.verbose) {
console.log(` ⚠️ Could not copy ${item} (might be in use): ${error.message}`);
}
}
}
}
if (this.verbose) {
console.log(`✅ Chrome user data copied successfully`);
console.log(` Your bookmarks, passwords, and extensions should be available in MCP Chrome`);
}
}
catch (error) {
if (this.verbose) {
console.warn(`⚠️ Error copying user data: ${error.message}`);
console.log(` MCP Chrome will start with a clean profile`);
}
}
}
/**
* Prepare Chrome profile directory with optional user data copying
*/
async prepareProfileDirectory(profilePath, copyUserData = true, sourceProfilePath) {
const fullPath = resolve(profilePath);
if (!existsSync(fullPath)) {
mkdirSync(fullPath, { recursive: true });
if (this.verbose) {
console.log(`📁 Created MCP Chrome profile directory: ${fullPath}`);
}
}
// Copy user data if requested
if (copyUserData) {
const sourceDataDir = sourceProfilePath || this.getDefaultChromeUserDataDir();
await this.copyUserChromeData(sourceDataDir, fullPath);
}
return fullPath;
}
/**
* Launch a separate Chrome instance for MCP use
*/
async launchSeparateChrome(options = {}) {
const config = this.config.getBrowserConfig();
const debugPort = options.debugPort || config.debugPort;
const profilePath = await this.prepareProfileDirectory(options.profilePath || config.mcpProfilePath, options.copyUserData !== false, // Default to true
options.sourceProfilePath);
// Check if port is already in use
if (await this.isDebugPortAvailable(debugPort)) {
if (this.verbose) {
console.log(`⚠️ Port ${debugPort} already in use, checking if it's our instance...`);
}
// Check if it's one of our instances
const existingInstance = Array.from(this.runningInstances.values())
.find(instance => instance.debugPort === debugPort);
if (existingInstance) {
if (this.verbose) {
console.log(`✅ Reusing existing MCP Chrome instance on port ${debugPort}`);
}
return existingInstance;
}
throw new BrowserConnectionError(`Port ${debugPort} is already in use by another Chrome instance. ` +
`Please close other Chrome debugging sessions or use a different port.`);
}
const chromePath = this.getChromePath();
// Build Chrome arguments for MCP instance with user data
const chromeArgs = [
`--remote-debugging-port=${debugPort}`,
`--user-data-dir=${profilePath}`,
'--no-first-run',
'--no-default-browser-check',
// Preserve user experience while enabling debugging
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-ipc-flooding-protection',
'--disable-hang-monitor',
'--disable-prompt-on-repost',
// Keep extensions and user features enabled
// '--disable-default-apps', // Removed to keep user apps
// '--disable-component-extensions-with-background-pages', // Removed to keep extensions
'--disable-web-security', // For automation purposes
'--disable-features=VizDisplayCompositor',
// Custom window title to identify MCP instance
'--window-name=Any-Browser-MCP',
// Allow user's extensions and features to work
'--enable-extensions',
'--enable-sync',
...(options.args || [])
];
if (options.headless) {
chromeArgs.push('--headless=new');
}
else {
// Set a distinctive window size for MCP Chrome
const windowSize = options.windowSize || { width: 1200, height: 800 };
chromeArgs.push(`--window-size=${windowSize.width},${windowSize.height}`);
chromeArgs.push('--window-position=100,100');
}
if (options.userAgent) {
chromeArgs.push(`--user-agent=${options.userAgent}`);
}
if (this.verbose) {
console.log(`🚀 Launching separate Chrome instance for MCP...`);
console.log(` Executable: ${chromePath}`);
console.log(` Debug Port: ${debugPort}`);
console.log(` Profile: ${profilePath}`);
console.log(` Args: ${chromeArgs.join(' ')}`);
}
// Launch Chrome process
const chromeProcess = spawn(chromePath, chromeArgs, {
stdio: this.verbose ? 'inherit' : 'ignore',
detached: false
});
if (!chromeProcess.pid) {
throw new BrowserConnectionError('Failed to launch Chrome process');
}
const instance = {
process: chromeProcess,
debugPort,
profilePath,
pid: chromeProcess.pid,
startTime: new Date()
};
// Store instance
this.runningInstances.set(chromeProcess.pid, instance);
// Handle process events
chromeProcess.on('error', (error) => {
if (this.verbose) {
console.error(`❌ Chrome process error:`, error);
}
this.runningInstances.delete(chromeProcess.pid);
});
chromeProcess.on('exit', (code, signal) => {
if (this.verbose) {
console.log(`🔌 Chrome process exited: code=${code}, signal=${signal}`);
}
this.runningInstances.delete(chromeProcess.pid);
});
// Wait for Chrome to be ready
await this.waitForChromeReady(debugPort, config.launchTimeout);
if (this.verbose) {
console.log(`✅ MCP Chrome instance launched successfully`);
console.log(` PID: ${chromeProcess.pid}`);
console.log(` Debug URL: http://localhost:${debugPort}`);
console.log(` Profile: ${profilePath}`);
}
return instance;
}
/**
* Wait for Chrome to be ready for debugging
*/
async waitForChromeReady(port, timeout) {
const startTime = Date.now();
const checkInterval = 500;
while (Date.now() - startTime < timeout) {
if (await this.isDebugPortAvailable(port)) {
return;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
throw new BrowserConnectionError(`Chrome failed to start within ${timeout}ms. Debug port ${port} not accessible.`);
}
/**
* Get or launch Chrome instance for MCP use
*/
async getOrLaunchChrome(options = {}) {
const debugPort = options.debugPort || this.config.getBrowserConfig().debugPort;
// Check if we already have a running instance on this port
const existingInstance = Array.from(this.runningInstances.values())
.find(instance => instance.debugPort === debugPort);
if (existingInstance) {
// Verify the instance is still running and accessible
if (await this.isDebugPortAvailable(debugPort)) {
if (this.verbose) {
console.log(`♻️ Reusing existing MCP Chrome instance on port ${debugPort}`);
}
return existingInstance;
}
else {
// Instance died, remove from tracking
this.runningInstances.delete(existingInstance.pid);
}
}
// Check if there's already a Chrome instance running on this port (not ours)
if (await this.isDebugPortAvailable(debugPort)) {
throw new BrowserConnectionError(`Another Chrome instance is already using port ${debugPort}. ` +
`This could be your main Chrome browser. MCP needs its own separate instance. ` +
`Please close the other Chrome debugging session or use a different port.`);
}
// Launch new instance
return await this.launchSeparateChrome(options);
}
/**
* Close a specific Chrome instance
*/
async closeInstance(instance) {
if (this.verbose) {
console.log(`🔌 Closing MCP Chrome instance (PID: ${instance.pid})`);
}
try {
// Try graceful shutdown first
instance.process.kill('SIGTERM');
// Wait a bit for graceful shutdown
await new Promise(resolve => setTimeout(resolve, 2000));
// Force kill if still running
if (!instance.process.killed) {
instance.process.kill('SIGKILL');
}
this.runningInstances.delete(instance.pid);
if (this.verbose) {
console.log(`✅ MCP Chrome instance closed successfully`);
}
}
catch (error) {
if (this.verbose) {
console.warn(`⚠️ Error closing Chrome instance:`, error);
}
}
}
/**
* Close all MCP Chrome instances
*/
async closeAllInstances() {
if (this.verbose && this.runningInstances.size > 0) {
console.log(`🔌 Closing ${this.runningInstances.size} MCP Chrome instance(s)`);
}
const instances = Array.from(this.runningInstances.values());
await Promise.all(instances.map(instance => this.closeInstance(instance)));
}
/**
* Get information about running instances
*/
getRunningInstances() {
return Array.from(this.runningInstances.values());
}
/**
* Check if Chrome executable exists
*/
isChromeAvailable() {
try {
const chromePath = this.getChromePath();
return existsSync(chromePath);
}
catch {
return false;
}
}
}
export function createChromeLauncher(verbose = false) {
return new ChromeLauncher(verbose);
}
//# sourceMappingURL=ChromeLauncher.js.map