Skip to main content
Glama

firefox-devtools-mcp

attributeCollector.ts4.15 kB
/** * Attribute collection utilities (runs in browser context) */ import type { AriaAttributes, ComputedProperties } from '../types.js'; import { isFocusable, isInteractive } from './elementCollector.js'; /** * Max text length */ const MAX_TEXT_LENGTH = 100; /** * Get element name/label * Checks aria-label, associated label, placeholder, title, alt */ export function getElementName(el: Element): string | undefined { // aria-label if (el.hasAttribute('aria-label')) { return el.getAttribute('aria-label') || undefined; } // label for input const htmlEl = el as HTMLElement; const elId = htmlEl.id; if (elId) { const label = document.querySelector(`label[for="${elId}"]`); if (label?.textContent) { return label.textContent.trim(); } } // placeholder if (el.hasAttribute('placeholder')) { return el.getAttribute('placeholder') || undefined; } // title if (el.hasAttribute('title')) { return el.getAttribute('title') || undefined; } // alt for images if (el.hasAttribute('alt')) { return el.getAttribute('alt') || undefined; } // text content for buttons/links/headings const tag = el.tagName.toLowerCase(); if (['button', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].indexOf(tag) !== -1) { return getTextContent(el); } return undefined; } /** * Get direct text content (not from deep children) */ export function getTextContent(el: Element): string | undefined { let text = ''; for (let i = 0; i < el.childNodes.length; i++) { const node = el.childNodes[i]; if (node && node.nodeType === Node.TEXT_NODE) { text += node.textContent || ''; } } const trimmed = text.trim(); if (!trimmed) { return undefined; } return trimmed.substring(0, MAX_TEXT_LENGTH); } /** * Get ARIA attributes */ export function getAriaAttributes(el: Element): AriaAttributes | undefined { const aria: AriaAttributes = {}; let hasAny = false; // Boolean states const booleanAttrs: Array<'disabled' | 'hidden' | 'selected' | 'expanded'> = [ 'disabled', 'hidden', 'selected', 'expanded', ]; for (const attr of booleanAttrs) { const value = el.getAttribute(`aria-${attr}`); if (value !== null) { aria[attr] = value === 'true'; hasAny = true; } } // Mixed states (true/false/mixed) const mixedAttrs: Array<'checked' | 'pressed'> = ['checked', 'pressed']; for (const attr of mixedAttrs) { const value = el.getAttribute(`aria-${attr}`); if (value !== null) { if (value === 'mixed') { aria[attr] = 'mixed'; } else { aria[attr] = value === 'true'; } hasAny = true; } } // String properties const stringAttrs: Array< 'autocomplete' | 'haspopup' | 'invalid' | 'label' | 'labelledby' | 'describedby' | 'controls' > = ['autocomplete', 'haspopup', 'invalid', 'label', 'labelledby', 'describedby', 'controls']; for (const attr of stringAttrs) { const value = el.getAttribute(`aria-${attr}`); if (value) { if (attr === 'haspopup' || attr === 'invalid') { aria[attr] = value as boolean | string; } else { aria[attr] = value; } hasAny = true; } } // Numeric properties const levelValue = el.getAttribute('aria-level'); if (levelValue) { const level = parseInt(levelValue, 10); if (!isNaN(level)) { aria.level = level; hasAny = true; } } return hasAny ? aria : undefined; } /** * Get computed accessibility properties */ export function getComputedProperties(el: Element): ComputedProperties { const computed: ComputedProperties = {}; // Visible try { const style = window.getComputedStyle(el); computed.visible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; } catch (e) { computed.visible = false; } // Accessible (not aria-hidden and visible) computed.accessible = computed.visible && !el.getAttribute('aria-hidden'); // Focusable computed.focusable = isFocusable(el); // Interactive computed.interactive = isInteractive(el); return computed; }

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