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