UNPKG

@dorothywebb/any-browser-mcp

Version:

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

351 lines 12.9 kB
/** * Lazy Browser Manager - Only connects when MCP tools are actually used * Prevents any browser connections during VS Code startup */ import { createBrowserConnection } from '../utils/BrowserConnection.js'; import { getConfigManager } from './ConfigManager.js'; import { ConnectionPool } from '../utils/ConnectionPool.js'; import { ResourceManager } from '../utils/ResourceManager.js'; import { BrowserConnectionError, LazyInitializationError, ErrorFactory } from '../types/index.js'; import { handleError } from '../utils/ErrorHandler.js'; export var InitializationState; (function (InitializationState) { InitializationState["NOT_INITIALIZED"] = "not_initialized"; InitializationState["INITIALIZING"] = "initializing"; InitializationState["READY"] = "ready"; InitializationState["FAILED"] = "failed"; })(InitializationState || (InitializationState = {})); export class LazyBrowserManager { connection = null; initState = InitializationState.NOT_INITIALIZED; config = getConfigManager(); initPromise = null; lastError = null; connectionPool = null; resourceManager; constructor() { this.resourceManager = ResourceManager.getInstance(); if (this.config.isVerbose()) { console.log('🌐 LazyBrowserManager created - will connect only when needed'); } } /** * Check if manager is ready (initialized and connected) */ isReady() { return this.initState === InitializationState.READY && this.connection?.isConnected() === true; } /** * Get current status without triggering initialization */ getStatus() { return { state: this.initState, connected: this.connection?.isConnected() || false, connectionState: this.connection?.getState(), lastError: this.lastError?.message }; } /** * Initialize connection pool for performance optimization */ initializeConnectionPool() { if (!this.connectionPool) { this.connectionPool = new ConnectionPool({ maxConnections: this.config.getServerConfig().maxConcurrentConnections, verbose: this.config.isVerbose() }); // Register pool for cleanup this.resourceManager.registerResource('connection-pool', async () => { if (this.connectionPool) { await this.connectionPool.shutdown(); } }, 'connection-pool'); if (this.config.isVerbose()) { console.log('🏊 Connection pool initialized for performance optimization'); } } } /** * Initialize connection to browser (lazy - only when first needed) * This is the ONLY method that actually connects to browser */ async ensureConnection() { // Initialize connection pool on first use this.initializeConnectionPool(); // If already ready, nothing to do if (this.isReady()) { return; } // If already initializing, wait for existing promise if (this.initPromise) { return this.initPromise; } // If failed before, try again if (this.initState === InitializationState.FAILED) { this.initState = InitializationState.NOT_INITIALIZED; this.lastError = null; } // Start initialization this.initState = InitializationState.INITIALIZING; this.initPromise = this.performInitialization(); try { await this.initPromise; this.initState = InitializationState.READY; if (this.config.isVerbose()) { console.log('✅ Browser connection established lazily'); } } catch (error) { this.initState = InitializationState.FAILED; this.lastError = error; throw error; } finally { this.initPromise = null; } } /** * Perform the actual browser connection */ async performInitialization() { if (this.config.isVerbose()) { console.log('🔄 Initializing browser connection (lazy)...'); } // Create connection instance this.connection = createBrowserConnection(); // Check if browser is available before attempting connection const availability = await this.connection.checkBrowserAvailability(); if (!availability.available) { throw new LazyInitializationError(`Browser not available for lazy connection: ${availability.reason}\n\n` + `Please start your browser with debugging enabled:\n` + `Chrome: chrome --remote-debugging-port=9222\n` + `Edge: msedge --remote-debugging-port=9222`); } // Connect to existing browser await this.connection.connectToPage(); if (this.config.isVerbose()) { console.log('✅ Lazy browser connection successful'); } } /** * Execute a browser action, ensuring connection is established first * This is the main entry point for all browser operations */ async executeAction(actionName, action) { try { // Ensure we're connected before executing any action await this.ensureConnection(); if (!this.isReady()) { const error = ErrorFactory.createLazyInitError('Failed to establish browser connection', { operation: actionName }); throw error; } if (this.config.isVerbose()) { console.log(`🎯 Executing browser action: ${actionName}`); } // Execute the action return await action(); } catch (error) { let mcpError; if (error instanceof LazyInitializationError || error instanceof BrowserConnectionError) { // Already an MCP error, just enhance context mcpError = error; mcpError.context = { ...mcpError.context, operation: actionName, component: 'LazyBrowserManager' }; } else { // Create new tool execution error mcpError = ErrorFactory.createToolError(actionName, error.message, { component: 'LazyBrowserManager' }, error); } // Handle the error and update state handleError(mcpError, { actionName, initState: this.initState }); // If connection failed, reset state for retry if (error instanceof LazyInitializationError || error instanceof BrowserConnectionError) { this.initState = InitializationState.FAILED; this.lastError = error; } throw mcpError; } } /** * Send command to browser (with lazy initialization) */ async sendCommand(method, params = {}) { return this.executeAction(`${method}`, async () => { if (!this.connection) { throw new LazyInitializationError('Browser connection not available'); } return this.connection.sendCommand(method, params); }); } /** * Get available pages (with lazy initialization) */ async getPages() { return this.executeAction('getPages', async () => { if (!this.connection) { throw new LazyInitializationError('Browser connection not available'); } return this.connection.getAvailablePages(); }); } /** * Navigate to URL (with lazy initialization) */ async navigate(url) { return this.executeAction('navigate', async () => { await this.sendCommand('Page.navigate', { url }); await this.sendCommand('Page.loadEventFired'); return { success: true, message: `Navigated to ${url}`, timestamp: new Date() }; }); } /** * Take screenshot (with lazy initialization) */ async screenshot() { return this.executeAction('screenshot', async () => { const result = await this.sendCommand('Page.captureScreenshot', { format: 'png', quality: 80 }); return { success: true, message: 'Screenshot captured', data: result.data, timestamp: new Date() }; }); } /** * Click element (with lazy initialization) */ async click(selector, options = {}) { return this.executeAction('click', async () => { // Enable DOM domain await this.sendCommand('DOM.enable'); await this.sendCommand('Runtime.enable'); // Get document const doc = await this.sendCommand('DOM.getDocument'); // Query selector const node = await this.sendCommand('DOM.querySelector', { nodeId: doc.root.nodeId, selector }); if (!node.nodeId) { throw new Error(`Element not found: ${selector}`); } // Get box model const box = await this.sendCommand('DOM.getBoxModel', { nodeId: node.nodeId }); // Calculate click coordinates const quad = box.model.content; const x = (quad[0] + quad[2]) / 2; const y = (quad[1] + quad[5]) / 2; // Perform click await this.sendCommand('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }); await this.sendCommand('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }); return { success: true, message: `Clicked element: ${selector}`, timestamp: new Date() }; }); } /** * Type text (with lazy initialization) */ async type(text, options = {}) { return this.executeAction('type', async () => { const delay = options.delay || 0; for (const char of text) { await this.sendCommand('Input.dispatchKeyEvent', { type: 'char', text: char }); if (delay > 0) { await new Promise(resolve => setTimeout(resolve, delay)); } } return { success: true, message: `Typed text: ${text}`, timestamp: new Date() }; }); } /** * Get page content (with lazy initialization) */ async getContent(selector) { return this.executeAction('getContent', async () => { // Enable DOM await this.sendCommand('DOM.enable'); // Get document const doc = await this.sendCommand('DOM.getDocument'); let nodeId = doc.root.nodeId; let description = 'Page content retrieved'; // If selector provided, find specific element if (selector) { const node = await this.sendCommand('DOM.querySelector', { nodeId: doc.root.nodeId, selector }); if (!node.nodeId) { throw new Error(`Element not found: ${selector}`); } nodeId = node.nodeId; description = `Content retrieved for: ${selector}`; } // Get outer HTML const content = await this.sendCommand('DOM.getOuterHTML', { nodeId }); return { success: true, message: description, data: content.outerHTML, timestamp: new Date() }; }); } /** * Disconnect and cleanup */ async disconnect() { if (this.connection) { await this.connection.disconnect(); this.connection = null; } this.initState = InitializationState.NOT_INITIALIZED; this.initPromise = null; this.lastError = null; if (this.config.isVerbose()) { console.log('🔌 Browser connection closed'); } } } export function createLazyBrowserManager() { return new LazyBrowserManager(); } //# sourceMappingURL=LazyBrowserManager.js.map