Skip to main content
Glama

firefox-devtools-mcp

dom.ts10.7 kB
/** * DOM interactions: evaluate, element lookup, input actions */ import { By, Key, until, type WebDriver, type WebElement } from 'selenium-webdriver'; export class DomInteractions { constructor( private driver: WebDriver, private resolveUid?: (uid: string) => Promise<WebElement> ) {} /** * Evaluate JavaScript - direct passthrough to executeScript */ async evaluate(script: string): Promise<unknown> { return await this.driver.executeScript(script); } /** * Get page HTML content */ async getContent(): Promise<string> { const html = await this.evaluate('return document.documentElement.outerHTML'); return String(html); } /** * Click element by CSS selector */ async clickBySelector(selector: string): Promise<void> { const el = await this.driver.wait(until.elementLocated(By.css(selector)), 5000); await this.driver.wait(until.elementIsVisible(el), 5000).catch(() => {}); await el.click(); } /** * Hover over element by CSS selector */ async hoverBySelector(selector: string): Promise<void> { const el = await this.driver.wait(until.elementLocated(By.css(selector)), 5000); await this.driver.actions({ async: true }).move({ origin: el }).perform(); } /** * Fill input field by CSS selector */ async fillBySelector(selector: string, text: string): Promise<void> { const el = await this.driver.wait(until.elementLocated(By.css(selector)), 5000); try { await el.clear(); } catch { // Some inputs may not support clear(); fall back to select-all + delete await el.sendKeys(Key.chord(Key.CONTROL, 'a'), Key.DELETE); } await el.sendKeys(text); } /** * Drag & drop using JS events fallback (DataTransfer). * Works on simple pages; not guaranteed for all custom DnD libs. */ async dragAndDropBySelectors(sourceSelector: string, targetSelector: string): Promise<void> { await this.driver.executeScript( (srcSel: string, tgtSel: string) => { const src = document.querySelector(srcSel); const tgt = document.querySelector(tgtSel); if (!src || !tgt) { throw new Error('dragAndDrop: element not found'); } function dispatch(type: string, target: Element, dataTransfer?: DataTransfer) { const evt = new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer, } as DragEventInit); return target.dispatchEvent(evt); } // Create DataTransfer if available const dt = typeof DataTransfer !== 'undefined' ? new DataTransfer() : undefined; dispatch('dragstart', src, dt); dispatch('dragenter', tgt, dt); dispatch('dragover', tgt, dt); dispatch('drop', tgt, dt); dispatch('dragend', src, dt); }, sourceSelector, targetSelector ); } /** * File upload: unhide if needed, then send local path to <input type=file>. */ async uploadFileBySelector(selector: string, filePath: string): Promise<void> { // Try to locate input element const el = await this.driver.wait(until.elementLocated(By.css(selector)), 5000); // Ensure it's an <input type=file>; if hidden, unhide via JS await this.driver.executeScript((sel: string) => { const e = document.querySelector(sel); if (!e) { throw new Error('uploadFile: element not found'); } if (e.tagName !== 'INPUT' || (e as HTMLInputElement).type !== 'file') { throw new Error('uploadFile: selector must target <input type=file>'); } const style = window.getComputedStyle(e); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { const s = (e as HTMLElement).style; s.display = 'block'; s.visibility = 'visible'; s.opacity = '1'; s.position = 'fixed'; s.left = '0px'; s.top = '0px'; s.zIndex = '2147483647'; } }, selector); await el.sendKeys(filePath); } // ============================================================================ // UID-based input methods // ============================================================================ /** * Click element by UID * Requires resolveUid callback to be set (from SnapshotManager) */ async clickByUid(uid: string, dblClick = false): Promise<void> { if (!this.resolveUid) { throw new Error('clickByUid: resolveUid callback not set. Ensure snapshot is initialized.'); } const el = await this.resolveUid(uid); await this.driver.wait(until.elementIsVisible(el), 5000).catch(() => {}); if (dblClick) { await this.driver.actions({ async: true }).doubleClick(el).perform(); } else { await el.click(); } // Wait for events to propagate await this.waitForEventsAfterAction(); } /** * Hover over element by UID */ async hoverByUid(uid: string): Promise<void> { if (!this.resolveUid) { throw new Error('hoverByUid: resolveUid callback not set. Ensure snapshot is initialized.'); } const el = await this.resolveUid(uid); await this.driver.actions({ async: true }).move({ origin: el }).perform(); // Wait for events to propagate await this.waitForEventsAfterAction(); } /** * Fill input field by UID */ async fillByUid(uid: string, value: string): Promise<void> { if (!this.resolveUid) { throw new Error('fillByUid: resolveUid callback not set. Ensure snapshot is initialized.'); } const el = await this.resolveUid(uid); try { await el.clear(); } catch { // Some inputs may not support clear(); fall back to select-all + delete await el.sendKeys(Key.chord(Key.CONTROL, 'a'), Key.DELETE); } await el.sendKeys(value); // Wait for events to propagate await this.waitForEventsAfterAction(); } /** * Drag & drop by UIDs * Uses JS events fallback for better compatibility */ async dragByUidToUid(fromUid: string, toUid: string): Promise<void> { if (!this.resolveUid) { throw new Error( 'dragByUidToUid: resolveUid callback not set. Ensure snapshot is initialized.' ); } const fromEl = await this.resolveUid(fromUid); const toEl = await this.resolveUid(toUid); // Use JS drag events fallback for compatibility (Actions DnD not used) await this.driver.executeScript( (srcEl: Element, tgtEl: Element) => { if (!srcEl || !tgtEl) { throw new Error('dragAndDrop: element not found'); } function dispatch(type: string, target: Element, dataTransfer?: DataTransfer) { const evt = new DragEvent(type, { bubbles: true, cancelable: true, dataTransfer, } as DragEventInit); return target.dispatchEvent(evt); } // Create DataTransfer if available const dt = typeof DataTransfer !== 'undefined' ? new DataTransfer() : undefined; dispatch('dragstart', srcEl, dt); dispatch('dragenter', tgtEl, dt); dispatch('dragover', tgtEl, dt); dispatch('drop', tgtEl, dt); dispatch('dragend', srcEl, dt); }, fromEl, toEl ); // Wait for events to propagate await this.waitForEventsAfterAction(); } /** * Fill multiple form fields by UIDs */ async fillFormByUid(elements: Array<{ uid: string; value: string }>): Promise<void> { if (!this.resolveUid) { throw new Error( 'fillFormByUid: resolveUid callback not set. Ensure snapshot is initialized.' ); } for (const { uid, value } of elements) { await this.fillByUid(uid, value); } } /** * Upload file by UID * Handles hidden file inputs by making them visible */ async uploadFileByUid(uid: string, filePath: string): Promise<void> { if (!this.resolveUid) { throw new Error( 'uploadFileByUid: resolveUid callback not set. Ensure snapshot is initialized.' ); } const el = await this.resolveUid(uid); // Ensure it's an <input type=file>; if hidden, unhide via JS await this.driver.executeScript((element: Element) => { if (!element) { throw new Error('uploadFile: element not found'); } if (element.tagName !== 'INPUT' || (element as HTMLInputElement).type !== 'file') { throw new Error('uploadFile: element must be <input type=file>'); } const style = window.getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { const s = (element as HTMLElement).style; s.display = 'block'; s.visibility = 'visible'; s.opacity = '1'; s.position = 'fixed'; s.left = '0px'; s.top = '0px'; s.zIndex = '2147483647'; } }, el); await el.sendKeys(filePath); // Wait for events to propagate await this.waitForEventsAfterAction(); } /** * Wait for events to propagate after user action * Gives the page time to respond to interactions */ private async waitForEventsAfterAction(): Promise<void> { // Wait for microtask/raf to allow event handlers to fire await this.driver.executeScript('return new Promise(r => requestAnimationFrame(() => r()))'); // Small additional delay for good measure await new Promise((resolve) => setTimeout(resolve, 50)); } // ============================================================================ // Screenshot // ============================================================================ /** * Take screenshot of the entire page * @returns PNG as base64 string */ async takeScreenshotPage(): Promise<string> { return await this.driver.takeScreenshot(); } /** * Take screenshot of element by UID * Scrolls element into view, then captures it * @param uid Element UID from snapshot * @returns PNG as base64 string */ async takeScreenshotByUid(uid: string): Promise<string> { if (!this.resolveUid) { throw new Error( 'takeScreenshotByUid: resolveUid callback not set. Ensure snapshot is initialized.' ); } const el = await this.resolveUid(uid); // Scroll element into view await this.driver.executeScript( 'arguments[0].scrollIntoView({block: "center", inline: "center"});', el ); // Wait for scroll to complete await new Promise((resolve) => setTimeout(resolve, 100)); // Take screenshot of element (Selenium automatically crops to element bounds) return await el.takeScreenshot(); } }

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