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