Skip to main content
Glama

firefox-devtools-mcp

index.ts10.9 kB
/** * Firefox Client - Public facade for modular Firefox automation */ import type { FirefoxLaunchOptions, ConsoleMessage } from './types.js'; import { FirefoxCore } from './core.js'; import { ConsoleEvents, NetworkEvents } from './events/index.js'; import { DomInteractions } from './dom.js'; import { PageManagement } from './pages.js'; import { SnapshotManager, type Snapshot } from './snapshot/index.js'; /** * Main Firefox Client facade * Delegates to modular components for clean separation of concerns */ export class FirefoxClient { private core: FirefoxCore; private consoleEvents: ConsoleEvents | null = null; private networkEvents: NetworkEvents | null = null; private dom: DomInteractions | null = null; private pages: PageManagement | null = null; private snapshot: SnapshotManager | null = null; constructor(options: FirefoxLaunchOptions) { this.core = new FirefoxCore(options); } /** * Connect and initialize all modules */ async connect(): Promise<void> { await this.core.connect(); const driver = this.core.getDriver(); // Initialize snapshot manager first this.snapshot = new SnapshotManager(driver); // Create centralized navigation handler for lifecycle hooks const onNavigate = () => { // Clear snapshot on any navigation if (this.snapshot) { this.snapshot.clear(); } }; // Initialize event modules with lifecycle hooks // Note: autoClearOnNavigate is false to preserve logs across all tabs // Users can manually call clearConsoleMessages() if needed this.consoleEvents = new ConsoleEvents(driver, { onNavigate, autoClearOnNavigate: false, }); this.networkEvents = new NetworkEvents(driver, { onNavigate, autoClearOnNavigate: false, }); // Initialize DOM with UID resolver callback this.dom = new DomInteractions(driver, (uid: string) => this.snapshot!.resolveUidToElement(uid) ); this.pages = new PageManagement( driver, () => this.core.getCurrentContextId(), (id: string) => this.core.setCurrentContextId(id) ); // Subscribe to console and network events for ALL contexts (not just current) // This ensures we capture logs from all tabs, not just the initial one await this.consoleEvents.subscribe(undefined); await this.networkEvents.subscribe(undefined); } // ============================================================================ // DOM / Evaluate // ============================================================================ async evaluate(script: string): Promise<unknown> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.evaluate(script); } async getContent(): Promise<string> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.getContent(); } async clickBySelector(selector: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.clickBySelector(selector); } async hoverBySelector(selector: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.hoverBySelector(selector); } async fillBySelector(selector: string, text: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.fillBySelector(selector, text); } async dragAndDropBySelectors(sourceSelector: string, targetSelector: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.dragAndDropBySelectors(sourceSelector, targetSelector); } async uploadFileBySelector(selector: string, filePath: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.uploadFileBySelector(selector, filePath); } // UID-based input methods async clickByUid(uid: string, dblClick = false): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.clickByUid(uid, dblClick); } async hoverByUid(uid: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.hoverByUid(uid); } async fillByUid(uid: string, value: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.fillByUid(uid, value); } async dragByUidToUid(fromUid: string, toUid: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.dragByUidToUid(fromUid, toUid); } async fillFormByUid(elements: Array<{ uid: string; value: string }>): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.fillFormByUid(elements); } async uploadFileByUid(uid: string, filePath: string): Promise<void> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.uploadFileByUid(uid, filePath); } // ============================================================================ // Console // ============================================================================ async getConsoleMessages(): Promise<ConsoleMessage[]> { if (!this.consoleEvents) { throw new Error('Not connected'); } return this.consoleEvents.getMessages(); } clearConsoleMessages(): void { if (!this.consoleEvents) { throw new Error('Not connected'); } this.consoleEvents.clearMessages(); } // ============================================================================ // Pages / Navigation // ============================================================================ async navigate(url: string): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } await this.pages.navigate(url); // Clear snapshot on navigation (but NOT console - users can manually clear if needed) this.clearSnapshot(); } async navigateBack(): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.navigateBack(); } async navigateForward(): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.navigateForward(); } async setViewportSize(width: number, height: number): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.setViewportSize(width, height); } async acceptDialog(promptText?: string): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.acceptDialog(promptText); } async dismissDialog(): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.dismissDialog(); } getTabs(): Array<{ actor: string; title: string; url: string }> { if (!this.pages) { throw new Error('Not connected'); } return this.pages.getTabs(); } getSelectedTabIdx(): number { if (!this.pages) { throw new Error('Not connected'); } return this.pages.getSelectedTabIdx(); } async refreshTabs(): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.refreshTabs(); } async selectTab(index: number): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.selectTab(index); } async createNewPage(url: string): Promise<number> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.createNewPage(url); } async closeTab(index: number): Promise<void> { if (!this.pages) { throw new Error('Not connected'); } return await this.pages.closeTab(index); } // ============================================================================ // Network // ============================================================================ async startNetworkMonitoring(): Promise<void> { if (!this.networkEvents) { throw new Error('Not connected'); } this.networkEvents.startMonitoring(); } async stopNetworkMonitoring(): Promise<void> { if (!this.networkEvents) { throw new Error('Not connected'); } this.networkEvents.stopMonitoring(); } async getNetworkRequests(): Promise<any[]> { if (!this.networkEvents) { throw new Error('Not connected'); } return this.networkEvents.getRequests(); } clearNetworkRequests(): void { if (!this.networkEvents) { throw new Error('Not connected'); } this.networkEvents.clearRequests(); } // ============================================================================ // Snapshot // ============================================================================ async takeSnapshot(): Promise<Snapshot> { if (!this.snapshot) { throw new Error('Not connected'); } return await this.snapshot.takeSnapshot(); } resolveUidToSelector(uid: string): string { if (!this.snapshot) { throw new Error('Not connected'); } return this.snapshot.resolveUidToSelector(uid); } async resolveUidToElement(uid: string): Promise<any> { if (!this.snapshot) { throw new Error('Not connected'); } return await this.snapshot.resolveUidToElement(uid); } clearSnapshot(): void { if (!this.snapshot) { throw new Error('Not connected'); } this.snapshot.clear(); } // ============================================================================ // Screenshot // ============================================================================ async takeScreenshotPage(): Promise<string> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.takeScreenshotPage(); } async takeScreenshotByUid(uid: string): Promise<string> { if (!this.dom) { throw new Error('Not connected'); } return await this.dom.takeScreenshotByUid(uid); } // ============================================================================ // Internal / Advanced // ============================================================================ /** * Get WebDriver instance (for advanced operations) * @internal */ getDriver(): any { return this.core.getDriver(); } // ============================================================================ // Cleanup // ============================================================================ async close(): Promise<void> { await this.core.close(); } } // Re-export types export type { Snapshot } from './snapshot/index.js'; // Re-export for backward compatibility export { FirefoxClient as FirefoxDevTools };

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/freema/firefox-devtools-mcp'

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