Skip to main content
Glama
browser-manager.ts10.6 kB
import { chromium, firefox, webkit, Browser } from 'playwright'; import { v4 as uuidv4 } from 'uuid'; import { BrowserInstance, BrowserConfig, ServerConfig, ToolResult } from './types.js'; import { execSync } from 'child_process'; export class BrowserManager { private instances: Map<string, BrowserInstance> = new Map(); private config: ServerConfig; private cleanupTimer?: NodeJS.Timeout; private detectedProxy?: string; constructor(config: ServerConfig) { this.config = config; this.startCleanupTimer(); // Initialize proxy detection during construction this.initializeProxy(); } /** * Initialize proxy configuration */ private async initializeProxy(): Promise<void> { const globalProxy = this.config.proxy; if (globalProxy?.server) { this.detectedProxy = globalProxy.server; console.log(`Using configured proxy: ${this.detectedProxy}`); } else if (globalProxy?.autoDetect !== false) { // Enable auto-detection by default this.detectedProxy = await this.detectLocalProxy(); if (this.detectedProxy) { console.log(`Auto-detected proxy: ${this.detectedProxy}`); } } } /** * Auto-detect local proxy */ private async detectLocalProxy(): Promise<string | undefined> { // 1. Check environment variables const envProxy = this.getProxyFromEnv(); if (envProxy) { console.log(`Proxy detected from environment variables: ${envProxy}`); return envProxy; } // 2. Check common proxy ports const commonPorts = [7890, 1087, 8080, 3128, 8888, 10809, 20171]; for (const port of commonPorts) { const proxyUrl = `http://127.0.0.1:${port}`; if (await this.testProxyConnection(proxyUrl)) { console.log(`Local proxy port detected: ${port}`); return proxyUrl; } } // 3. Try to detect system proxy settings (macOS) if (process.platform === 'darwin') { const systemProxy = this.getMacOSSystemProxy(); if (systemProxy) { console.log(`System proxy detected: ${systemProxy}`); return systemProxy; } } return undefined; } /** * Get proxy from environment variables */ private getProxyFromEnv(): string | undefined { const httpProxy = process.env['HTTP_PROXY'] || process.env['http_proxy']; const httpsProxy = process.env['HTTPS_PROXY'] || process.env['https_proxy']; const allProxy = process.env['ALL_PROXY'] || process.env['all_proxy']; return httpProxy || httpsProxy || allProxy; } /** * Get macOS system proxy settings */ private getMacOSSystemProxy(): string | undefined { try { const result = execSync('networksetup -getwebproxy "Wi-Fi" 2>/dev/null || networksetup -getwebproxy "Ethernet" 2>/dev/null', { encoding: 'utf8', timeout: 5000 }); const lines = result.split('\n'); const enabled = lines.find(line => line.includes('Enabled: Yes')); if (!enabled) return undefined; const server = lines.find(line => line.includes('Server:'))?.split(': ')[1]; const port = lines.find(line => line.includes('Port:'))?.split(': ')[1]; if (server && port) { return `http://${server}:${port}`; } } catch (error) { // Ignore errors and continue with other methods } return undefined; } /** * Test proxy connection */ private async testProxyConnection(proxyUrl: string): Promise<boolean> { try { // Simple port detection to avoid network request complexity const url = new URL(proxyUrl); const net = require('net'); return new Promise((resolve) => { const socket = new net.Socket(); socket.setTimeout(3000); socket.on('connect', () => { socket.destroy(); resolve(true); }); socket.on('timeout', () => { socket.destroy(); resolve(false); }); socket.on('error', () => { resolve(false); }); socket.connect(parseInt(url.port), url.hostname); }); } catch (error) { return false; } } /** * Get effective proxy configuration */ private getEffectiveProxy(browserConfig?: Partial<BrowserConfig>): string | undefined { // Priority: instance config > global config > auto-detected if (browserConfig?.proxy?.server) { return browserConfig.proxy.server; } if (browserConfig?.proxy?.autoDetect === false) { return undefined; // Explicitly disable proxy } return this.detectedProxy; } /** * Create a new browser instance */ async createInstance( browserConfig?: Partial<BrowserConfig>, metadata?: BrowserInstance['metadata'] ): Promise<ToolResult> { try { if (this.instances.size >= this.config.maxInstances) { return { success: false, error: `Maximum number of instances (${this.config.maxInstances}) reached` }; } const config = { ...this.config.defaultBrowserConfig, ...browserConfig }; const browser = await this.launchBrowser(config); const contextOptions: any = { viewport: config.viewport, ...config.contextOptions }; if (config.userAgent) { contextOptions.userAgent = config.userAgent; } // Add proxy configuration to context const effectiveProxy = this.getEffectiveProxy(browserConfig); if (effectiveProxy) { contextOptions.proxy = { server: effectiveProxy }; } const context = await browser.newContext(contextOptions); const page = await context.newPage(); const instanceId = uuidv4(); const instance: BrowserInstance = { id: instanceId, browser, context, page, createdAt: new Date(), lastUsed: new Date(), isActive: true, ...(metadata && { metadata }) }; this.instances.set(instanceId, instance); return { success: true, data: { instanceId, browserType: config.browserType, headless: config.headless, viewport: config.viewport, proxy: effectiveProxy, metadata }, instanceId }; } catch (error) { return { success: false, error: `Failed to create browser instance: ${error instanceof Error ? error.message : error}` }; } } /** * Get browser instance */ getInstance(instanceId: string): BrowserInstance | undefined { const instance = this.instances.get(instanceId); if (instance) { instance.lastUsed = new Date(); } return instance; } /** * List all instances */ listInstances(): ToolResult { const instanceList = Array.from(this.instances.values()).map(instance => ({ id: instance.id, isActive: instance.isActive, createdAt: instance.createdAt.toISOString(), lastUsed: instance.lastUsed.toISOString(), metadata: instance.metadata, currentUrl: instance.page.url() })); return { success: true, data: { instances: instanceList, totalCount: this.instances.size, maxInstances: this.config.maxInstances } }; } /** * Close browser instance */ async closeInstance(instanceId: string): Promise<ToolResult> { try { const instance = this.instances.get(instanceId); if (!instance) { return { success: false, error: `Instance ${instanceId} not found` }; } await instance.browser.close(); this.instances.delete(instanceId); return { success: true, data: { instanceId, closed: true }, instanceId }; } catch (error) { return { success: false, error: `Failed to close instance: ${error instanceof Error ? error.message : error}` }; } } /** * Close all instances */ async closeAllInstances(): Promise<ToolResult> { try { const closePromises = Array.from(this.instances.values()).map( instance => instance.browser.close() ); await Promise.all(closePromises); const closedCount = this.instances.size; this.instances.clear(); return { success: true, data: { closedCount } }; } catch (error) { return { success: false, error: `Failed to close all instances: ${error instanceof Error ? error.message : error}` }; } } /** * Launch browser */ private async launchBrowser(config: BrowserConfig): Promise<Browser> { const launchOptions: any = { headless: config.headless ?? true }; if (config.headless) { launchOptions.args = ['--no-sandbox', '--disable-setuid-sandbox']; } // Add proxy arguments for Chromium const effectiveProxy = this.getEffectiveProxy(config); if (effectiveProxy && config.browserType === 'chromium') { if (!launchOptions.args) { launchOptions.args = []; } launchOptions.args.push(`--proxy-server=${effectiveProxy}`); } switch (config.browserType) { case 'chromium': return await chromium.launch(launchOptions); case 'firefox': return await firefox.launch(launchOptions); case 'webkit': return await webkit.launch(launchOptions); default: throw new Error(`Unsupported browser type: ${config.browserType}`); } } /** * Start cleanup timer */ private startCleanupTimer(): void { this.cleanupTimer = setInterval(async () => { await this.cleanupInactiveInstances(); }, this.config.cleanupInterval); } /** * Clean up inactive instances */ private async cleanupInactiveInstances(): Promise<void> { const now = new Date(); const instancesToClose: string[] = []; for (const [id, instance] of this.instances.entries()) { const timeSinceLastUsed = now.getTime() - instance.lastUsed.getTime(); if (timeSinceLastUsed > this.config.instanceTimeout) { instancesToClose.push(id); } } for (const instanceId of instancesToClose) { await this.closeInstance(instanceId); console.log(`Cleaned up inactive instance: ${instanceId}`); } } /** * Destroy manager */ async destroy(): Promise<void> { if (this.cleanupTimer) { clearInterval(this.cleanupTimer); } await this.closeAllInstances(); } }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sailaoda/concurrent-browser-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server