Skip to main content
Glama

LCBro

by lcbro
browser-manager.ts•9.98 kB
import { Browser, BrowserContext, Page, chromium, LaunchOptions } from 'playwright'; import { Logger } from 'pino'; import { PageId, ProxyConfig, Viewport } from '../types/index.js'; import { createHash } from 'crypto'; import path from 'path'; import { CDPBrowserManager, CDPBrowserInfo, CDPContext } from './cdp-browser-manager.js'; export interface BrowserConfig { headless: boolean; maxContexts: number; storageDir: string; defaultTimeoutMs: number; cdp?: { enabled: boolean; host: string; port: number; autoDetect: boolean; maxRetries: number; retryDelay: number; remote: { enabled: boolean; url: string | null; sslMode: 'auto' | 'enabled' | 'disabled' | 'insecure'; apiKey: string | null; headers: Record<string, string>; }; detection: { enabled: boolean; ports: number[]; timeout: number; useRemote: boolean; }; launch: { autoLaunch: boolean; browserPath: string | null; userDataDir: string | null; additionalArgs: string[]; }; connection: { timeout: number; keepAlive: boolean; reconnect: boolean; maxReconnects: number; }; }; } export interface ContextOptions { viewport?: Viewport; userAgent?: string; proxy?: ProxyConfig; extraHeaders?: Record<string, string>; persistSessionKey?: string; } export interface PageInfo { id: PageId; page: Page; context: BrowserContext; createdAt: Date; sessionKey?: string; } export class BrowserManager { private browser: Browser | null = null; private contexts = new Map<string, BrowserContext>(); private pages = new Map<PageId, PageInfo>(); private contextCounter = 0; private cdpManager: CDPBrowserManager | null = null; private engine: 'playwright' | 'cdp' = 'playwright'; constructor( private config: BrowserConfig, private logger: Logger ) { // Initialize CDP manager if enabled if (this.config.cdp?.enabled) { this.engine = 'cdp'; this.cdpManager = new CDPBrowserManager(this.logger, { browser: { cdp: this.config.cdp } } as any); this.logger.info('CDP browser manager initialized'); } } async initialize(): Promise<void> { if (this.engine === 'cdp') { await this.initializeCDP(); } else { await this.initializePlaywright(); } } private async initializeCDP(): Promise<void> { if (!this.cdpManager) { throw new Error('CDP manager not initialized'); } this.logger.info('Initializing CDP browser connection'); // Auto-detect browsers if enabled if (this.config.cdp?.autoDetect) { const browsers = await this.cdpManager.detectBrowsers(); if (browsers.length > 0) { this.logger.info({ count: browsers.length }, 'Found CDP browsers, connecting to first available'); const browser = browsers[0]; await this.cdpManager.connectToBrowser(browser); this.logger.info('CDP browser connection established'); return; } } // Auto-launch browser if enabled if (this.config.cdp?.launch?.autoLaunch) { await this.launchCDPBrowser(); } else { throw new Error('No CDP browsers found and auto-launch is disabled'); } } private async initializePlaywright(): Promise<void> { const launchOptions: LaunchOptions = { headless: this.config.headless, args: [ '--disable-gpu', '--disable-dev-shm-usage', '--disable-setuid-sandbox', '--no-first-run', '--no-zygote', '--single-process' ] }; if (process.env.NODE_ENV === 'production') { launchOptions.args?.push('--no-sandbox'); } this.browser = await chromium.launch(launchOptions); this.logger.info('Playwright browser launched successfully'); } private async launchCDPBrowser(): Promise<void> { // This would launch a browser with CDP enabled // For now, we'll throw an error as this requires external browser launch throw new Error('Auto-launching CDP browser is not implemented. Please start a browser manually with --remote-debugging-port=9222'); } async createContext(options: ContextOptions = {}): Promise<BrowserContext | string> { if (this.engine === 'cdp') { return this.createCDPContext(options); } else { return this.createPlaywrightContext(options); } } private async createCDPContext(options: ContextOptions = {}): Promise<string> { if (!this.cdpManager) { throw new Error('CDP manager not initialized'); } // For CDP, we return a context ID that can be used for operations // The actual context creation happens during connection const contextId = `cdp_context_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; this.logger.info({ contextId }, 'Created CDP context'); return contextId; } private async createPlaywrightContext(options: ContextOptions = {}): Promise<BrowserContext> { if (!this.browser) { throw new Error('Browser not initialized'); } if (this.contextCounter >= this.config.maxContexts) { throw new Error(`Maximum number of contexts (${this.config.maxContexts}) reached`); } let context: BrowserContext; const contextKey = this.getContextKey(options); // Try to reuse existing context for session persistence if (options.persistSessionKey && this.contexts.has(contextKey)) { context = this.contexts.get(contextKey)!; this.logger.info({ sessionKey: options.persistSessionKey }, 'Reusing existing context'); } else { // Create new context const contextOptions: any = { viewport: options.viewport || { width: 1280, height: 800 }, userAgent: options.userAgent, extraHTTPHeaders: options.extraHeaders, }; if (options.proxy) { contextOptions.proxy = { server: options.proxy.server, username: options.proxy.username, password: options.proxy.password }; } if (options.persistSessionKey) { const storageDir = path.join( this.config.storageDir, this.sanitizeSessionKey(options.persistSessionKey) ); contextOptions.storageState = storageDir; } context = await this.browser.newContext(contextOptions); context.setDefaultTimeout(this.config.defaultTimeoutMs); if (options.persistSessionKey) { this.contexts.set(contextKey, context); } this.contextCounter++; this.logger.info({ sessionKey: options.persistSessionKey, contextCount: this.contextCounter }, 'New context created'); } return context; } async createPage(context: BrowserContext, sessionKey?: string): Promise<PageInfo> { const page = await context.newPage(); const pageId = this.generatePageId(); const pageInfo: PageInfo = { id: pageId, page, context, createdAt: new Date(), sessionKey }; this.pages.set(pageId, pageInfo); this.logger.info({ pageId, sessionKey, totalPages: this.pages.size }, 'Page created'); return pageInfo; } getPage(pageId: PageId): PageInfo | undefined { return this.pages.get(pageId); } async closePage(pageId: PageId): Promise<void> { const pageInfo = this.pages.get(pageId); if (!pageInfo) { return; } try { await pageInfo.page.close(); } catch (error) { this.logger.warn({ pageId, error }, 'Error closing page'); } this.pages.delete(pageId); // Close context if no session persistence and no other pages if (!pageInfo.sessionKey) { const contextPages = Array.from(this.pages.values()) .filter(p => p.context === pageInfo.context); if (contextPages.length === 0) { try { await pageInfo.context.close(); this.contextCounter = Math.max(0, this.contextCounter - 1); } catch (error) { this.logger.warn({ pageId, error }, 'Error closing context'); } } } this.logger.info({ pageId }, 'Page closed'); } async cleanup(): Promise<void> { this.logger.info('Starting browser cleanup'); // Close all pages const closePromises = Array.from(this.pages.keys()).map(pageId => this.closePage(pageId) ); await Promise.allSettled(closePromises); // Close all non-persistent contexts const contextClosePromises = Array.from(this.contexts.values()).map(context => context.close().catch(err => this.logger.warn({ error: err }, 'Error closing context during cleanup') ) ); await Promise.allSettled(contextClosePromises); // Close browser if (this.browser) { try { await this.browser.close(); this.browser = null; } catch (error) { this.logger.warn({ error }, 'Error closing browser'); } } this.pages.clear(); this.contexts.clear(); this.contextCounter = 0; this.logger.info('Browser cleanup completed'); } getStats() { return { totalPages: this.pages.size, totalContexts: this.contextCounter, maxContexts: this.config.maxContexts, persistentContexts: this.contexts.size }; } private generatePageId(): PageId { return crypto.randomUUID(); } private getContextKey(options: ContextOptions): string { if (!options.persistSessionKey) { return ''; } const contextData = { sessionKey: options.persistSessionKey, proxy: options.proxy?.server, userAgent: options.userAgent }; return createHash('sha256') .update(JSON.stringify(contextData)) .digest('hex'); } private sanitizeSessionKey(sessionKey: string): string { return sessionKey.replace(/[^a-zA-Z0-9_-]/g, '_'); } }

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/lcbro/lcbro-mcp'

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