UNPKG

@dorothywebb/any-browser-mcp

Version:

Any Browser MCP - Launch Chrome with your actual data in debug mode for comprehensive browser automation

376 lines 15.1 kB
/** * Browser Connection Utility - STRICT EXISTING BROWSER ONLY * This utility NEVER launches browsers, only connects to existing ones */ import WebSocket from 'ws'; import http from 'http'; import { BrowserConnectionError, BrowserNotFoundError, ConnectionStatus, ErrorFactory } from '../types/index.js'; import { handleError } from './ErrorHandler.js'; import { getConfigManager } from '../core/ConfigManager.js'; import { createChromeLauncher } from './ChromeLauncher.js'; export class BrowserConnection { ws = null; state; config = getConfigManager(); messageId = 0; pendingMessages = new Map(); chromeLauncher; mcpChromeInstance = null; discoveredPort = null; constructor() { this.state = { status: ConnectionStatus.DISCONNECTED }; this.chromeLauncher = createChromeLauncher(this.config.isVerbose()); } /** * Discover active Chrome debug ports by scanning common port ranges */ async discoverChromeDebugPort() { // Common Chrome debug ports to scan const commonPorts = [9222, 9223, 9224, 9225, 9226, 9227, 9228, 9229, 9230, 9277]; const timeout = 1000; // Quick timeout for port scanning if (this.config.isVerbose()) { console.log('🔍 Scanning for active Chrome debug ports...'); } for (const port of commonPorts) { try { const pages = await this.getAvailablePagesOnPort(port, timeout); if (pages.length > 0) { // Prioritize ports with more pages (likely main browser) // and filter out service workers to focus on actual browser pages const browserPages = pages.filter(page => page.type === 'page'); if (browserPages.length > 0) { if (this.config.isVerbose()) { console.log(`✅ Found Chrome on port ${port} with ${browserPages.length} pages`); } return { port, pages: browserPages }; } } } catch (error) { // Port not available, continue scanning continue; } } if (this.config.isVerbose()) { console.log('❌ No active Chrome debug ports found'); } return null; } /** * Get available pages from a specific port with custom timeout */ async getAvailablePagesOnPort(port, timeout) { return new Promise((resolve, reject) => { const req = http.get(`http://localhost:${port}/json`, { timeout }, (res) => { if (res.statusCode !== 200) { reject(new BrowserNotFoundError(`Browser API returned status ${res.statusCode}`)); return; } let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const pages = JSON.parse(data); resolve(pages); } catch (error) { reject(new BrowserConnectionError(`Failed to parse browser response: ${error.message}`)); } }); }); req.on('error', (error) => { reject(new BrowserNotFoundError(`Cannot connect to browser: ${error.message}`)); }); req.on('timeout', () => { req.destroy(); reject(new BrowserNotFoundError(`Connection timeout to browser on port ${port}`)); }); }); } /** * Check if a browser is running with debugging enabled * This method ONLY checks - it NEVER launches anything * Now includes dynamic port discovery */ async checkBrowserAvailability() { // First try the configured port const configuredPort = this.config.getDebugPort(); const endpoint = `http://localhost:${configuredPort}`; try { // Test configured port first const pages = await this.getAvailablePages(); if (pages.length > 0) { if (this.config.isVerbose()) { console.log(`✅ Chrome found on configured port ${configuredPort}`); } return { available: true }; } } catch (error) { // Configured port failed, try dynamic discovery if (this.config.isVerbose()) { console.log(`⚠️ Configured port ${configuredPort} not available, trying dynamic discovery...`); } } // Try dynamic port discovery const discovered = await this.discoverChromeDebugPort(); if (discovered) { if (this.config.isVerbose()) { console.log(`🎯 Discovered Chrome on port ${discovered.port}`); } return { available: true, discoveredPort: discovered.port }; } // No Chrome found anywhere const mcpError = ErrorFactory.createConnectionError(`No Chrome browser found with debugging enabled`, { operation: 'checkBrowserAvailability', details: { configuredPort, endpoint, scannedPorts: true } }); handleError(mcpError); return { available: false, reason: `No Chrome browser found with debugging enabled. Please start Chrome with: chrome --remote-debugging-port=9222` }; } /** * Get available pages from existing browser * NEVER launches - only queries existing browser * Uses discovered port if available */ async getAvailablePages() { let port = this.discoveredPort || this.config.getDebugPort(); const timeout = this.config.getBrowserConfig().connectionTimeout; // If no discovered port and configured port fails, try discovery if (!this.discoveredPort) { try { const pages = await this.getAvailablePagesOnPort(port, timeout); return pages.filter(page => page.type === 'page'); } catch (error) { // Try dynamic discovery const discovered = await this.discoverChromeDebugPort(); if (discovered) { this.discoveredPort = discovered.port; port = discovered.port; if (this.config.isVerbose()) { console.log(`🔄 Switched to discovered port ${port}`); } return discovered.pages; } throw error; } } // Use discovered or configured port const pages = await this.getAvailablePagesOnPort(port, timeout); return pages.filter(page => page.type === 'page'); } /** * Connect to browser page - either existing or launch separate MCP instance */ async connectToPage(pageId) { const browserConfig = this.config.getBrowserConfig(); // First, try to connect to existing browser if configured if (browserConfig.useExistingOnly || !browserConfig.allowLaunch) { const availability = await this.checkBrowserAvailability(); if (!availability.available) { throw new BrowserNotFoundError(`Cannot connect: ${availability.reason}\n\n` + `This MCP server is configured to only use existing browsers.\n` + `Please start your browser manually with debugging enabled.`); } // Store discovered port for subsequent operations if (availability.discoveredPort) { this.discoveredPort = availability.discoveredPort; if (this.config.isVerbose()) { console.log(`🎯 Using discovered Chrome port ${this.discoveredPort}`); } } } else if (browserConfig.useSeparateInstance && browserConfig.allowLaunch) { // Launch or connect to separate MCP Chrome instance try { this.mcpChromeInstance = await this.chromeLauncher.getOrLaunchChrome({ debugPort: browserConfig.debugPort, profilePath: browserConfig.mcpProfilePath, copyUserData: browserConfig.copyUserData, sourceProfilePath: browserConfig.sourceProfilePath }); if (this.config.isVerbose()) { console.log(`🎯 Using separate MCP Chrome instance on port ${browserConfig.debugPort}`); } } catch (error) { throw new BrowserConnectionError(`Failed to launch separate Chrome instance for MCP: ${error.message}`); } } this.state.status = ConnectionStatus.CONNECTING; try { const pages = await this.getAvailablePages(); if (pages.length === 0) { throw new BrowserNotFoundError('No browser pages available'); } // Use specified page or first available const targetPage = pageId ? pages.find(p => p.id === pageId) : pages[0]; if (!targetPage || !targetPage.webSocketDebuggerUrl) { throw new BrowserConnectionError('No valid page found to connect to'); } // Connect via WebSocket await this.establishWebSocketConnection(targetPage.webSocketDebuggerUrl); this.state = { status: ConnectionStatus.CONNECTED, browserType: 'chrome', // Assume Chrome for now endpoint: targetPage.webSocketDebuggerUrl, pageCount: pages.length, activePageId: targetPage.id, lastConnected: new Date() }; if (this.config.isVerbose()) { console.log(`✅ Connected to existing browser page: ${targetPage.title}`); console.log(` URL: ${targetPage.url}`); console.log(` Pages available: ${pages.length}`); } } catch (error) { this.state.status = ConnectionStatus.FAILED; this.state.lastError = error.message; throw error; } } /** * Establish WebSocket connection to browser */ async establishWebSocketConnection(wsUrl) { return new Promise((resolve, reject) => { this.ws = new WebSocket(wsUrl); this.ws.on('open', () => { resolve(); }); this.ws.on('error', (error) => { reject(new BrowserConnectionError(`WebSocket connection failed: ${error.message}`)); }); this.ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); if (message.id && this.pendingMessages.has(message.id)) { const pending = this.pendingMessages.get(message.id); this.pendingMessages.delete(message.id); if (message.error) { pending?.reject(new Error(message.error.message)); } else { pending?.resolve(message.result); } } } catch (error) { console.warn('Failed to parse message from browser:', error); } }); this.ws.on('close', () => { this.state.status = ConnectionStatus.DISCONNECTED; this.ws = null; }); }); } /** * Send command to browser via CDP */ async sendCommand(method, params = {}) { if (!this.isConnected()) { throw new BrowserConnectionError('Not connected to browser'); } return new Promise((resolve, reject) => { const id = ++this.messageId; const message = { id, method, params }; this.pendingMessages.set(id, { resolve, reject }); this.ws.send(JSON.stringify(message), (error) => { if (error) { this.pendingMessages.delete(id); reject(new BrowserConnectionError(`Failed to send command: ${error.message}`)); } }); // Timeout after 30 seconds setTimeout(() => { if (this.pendingMessages.has(id)) { this.pendingMessages.delete(id); reject(new BrowserConnectionError(`Command timeout: ${method}`)); } }, 30000); }); } /** * Check if connected to browser */ isConnected() { return this.state.status === ConnectionStatus.CONNECTED && this.ws?.readyState === WebSocket.OPEN; } /** * Get current connection state */ getState() { return { ...this.state }; } /** * Get the currently active debug port (discovered or configured) */ getActiveDebugPort() { return this.discoveredPort || this.config.getDebugPort(); } /** * Get information about port discovery */ getPortInfo() { const configuredPort = this.config.getDebugPort(); return { configuredPort, discoveredPort: this.discoveredPort, activePort: this.getActiveDebugPort() }; } /** * Disconnect from browser */ async disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } this.state.status = ConnectionStatus.DISCONNECTED; this.pendingMessages.clear(); } /** * Disconnect and optionally close MCP Chrome instance */ async disconnectAndClose(closeMCPInstance = false) { await this.disconnect(); if (closeMCPInstance && this.mcpChromeInstance) { if (this.config.isVerbose()) { console.log('🔌 Closing MCP Chrome instance...'); } await this.chromeLauncher.closeInstance(this.mcpChromeInstance); this.mcpChromeInstance = null; } } /** * Get MCP Chrome instance information */ getMCPInstance() { return this.mcpChromeInstance; } /** * SAFETY CHECK: Prevent any accidental browser launching * This method always throws an error if called */ launchBrowser() { throw new Error('SAFETY VIOLATION: Attempted to launch browser!\n' + 'This MCP server is configured to NEVER launch browsers.\n' + 'Please start your browser manually with debugging enabled.'); } } export function createBrowserConnection() { return new BrowserConnection(); } //# sourceMappingURL=BrowserConnection.js.map