UNPKG

@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
/** * 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