Skip to main content
Glama

Web Inspector MCP

by antonzherdev
querySelectorAll.ts9.43 kB
import { BrowserToolBase } from './base.js'; import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js'; /** * Interface for element match data */ interface ElementMatch { tag: string; selector: string; testId?: string; classes: string; text: string; position: { x: number; y: number; width: number; height: number }; isVisible: boolean; isInteractive: boolean; opacity?: number; display?: string; attributes?: Record<string, string>; } /** * Tool for testing a selector and returning information about all matched elements * Useful for selector debugging and finding the right element to interact with */ export class QuerySelectorAllTool extends BrowserToolBase { /** * Execute the query selector all tool */ async execute(args: any, context: ToolContext): Promise<ToolResponse> { return this.safeExecute(context, async (page) => { const selector = this.normalizeSelector(args.selector); const limit = args.limit ?? 10; const onlyVisible = args.onlyVisible; // true = visible only, false = hidden only, undefined = all const showAttributes = args.showAttributes ? args.showAttributes.split(',').map((a: string) => a.trim()) : undefined; try { // Query all elements matching the selector const elements = await page.locator(selector).all(); const totalMatches = elements.length; if (totalMatches === 0) { return createSuccessResponse( `No elements found matching "${args.selector}"\n\nTip: Try using playwright_inspect_dom to explore the page structure.` ); } // Get detailed information for each element (all elements to allow filtering) const matchData: ElementMatch[] = []; for (let i = 0; i < elements.length; i++) { const element = elements[i]; try { const elementInfo = await element.evaluate((el, attrList) => { const rect = el.getBoundingClientRect(); const styles = window.getComputedStyle(el); const isVisible = styles.display !== 'none' && styles.visibility !== 'hidden' && parseFloat(styles.opacity) > 0 && rect.width > 0 && rect.height > 0; // Get test ID const testId = el.getAttribute('data-testid') || el.getAttribute('data-test') || el.getAttribute('data-cy') || undefined; // Get selector representation let selectorRepr = ''; if (testId) { const attrName = el.hasAttribute('data-testid') ? 'data-testid' : el.hasAttribute('data-test') ? 'data-test' : 'data-cy'; selectorRepr = `${attrName}="${testId}"`; } else if (el.id) { selectorRepr = `#${el.id}`; } else if (el.className && typeof el.className === 'string') { const classes = el.className.trim().split(/\s+/).slice(0, 3).join('.'); if (classes) { selectorRepr = `class="${classes}"`; } } // Check if interactive const tag = el.tagName.toLowerCase(); const interactiveTags = new Set(['button', 'a', 'input', 'select', 'textarea']); const isInteractive = interactiveTags.has(tag) || el.hasAttribute('onclick') || el.hasAttribute('contenteditable') || el.getAttribute('role') === 'button'; // Get text content (trimmed and limited) const text = el.textContent?.trim().slice(0, 100) || ''; // Get requested attributes if specified let attributes: Record<string, string> | undefined; if (attrList && attrList.length > 0) { attributes = {}; attrList.forEach((attr: string) => { const value = el.getAttribute(attr); if (value !== null) { attributes![attr] = value; } }); } return { tag: tag, selector: selectorRepr, testId, classes: Array.from(el.classList).join('.'), text, position: { x: Math.round(rect.x), y: Math.round(rect.y), width: Math.round(rect.width), height: Math.round(rect.height), }, isVisible, isInteractive, opacity: parseFloat(styles.opacity), display: styles.display, attributes, }; }, showAttributes); matchData.push(elementInfo); } catch (error) { // Skip elements that fail evaluation (e.g., detached from DOM) continue; } } // Filter by visibility if requested let filteredMatches = matchData; if (onlyVisible !== undefined) { filteredMatches = matchData.filter((match) => onlyVisible ? match.isVisible : !match.isVisible ); } // Apply limit after filtering const displayMatches = filteredMatches.slice(0, limit); // Format compact text output const lines: string[] = []; // Header with counts if (onlyVisible === true) { const visibleCount = filteredMatches.length; lines.push( `Found ${totalMatches} element${totalMatches > 1 ? 's' : ''} matching "${args.selector}" (${visibleCount} visible):` ); } else if (onlyVisible === false) { const hiddenCount = filteredMatches.length; lines.push( `Found ${totalMatches} element${totalMatches > 1 ? 's' : ''} matching "${args.selector}" (${hiddenCount} hidden):` ); } else { lines.push( `Found ${totalMatches} element${totalMatches > 1 ? 's' : ''} matching "${args.selector}":` ); } lines.push(''); displayMatches.forEach((match, index) => { const prefix = `[${index}]`; // Element tag with selector info let tagInfo = `<${match.tag}`; if (match.selector) { tagInfo += ` ${match.selector}`; } tagInfo += '>'; lines.push(`${prefix} ${tagInfo}`); // Position lines.push( ` @ (${match.position.x},${match.position.y}) ${match.position.width}x${match.position.height}px` ); // Text content if (match.text) { const displayText = match.text.length > 50 ? match.text.slice(0, 47) + '...' : match.text; lines.push(` "${displayText}"`); } // Attributes (if requested) if (match.attributes && Object.keys(match.attributes).length > 0) { Object.entries(match.attributes).forEach(([attr, value]) => { const displayValue = value.length > 50 ? value.slice(0, 47) + '...' : value; lines.push(` ${attr}: "${displayValue}"`); }); } // Status symbols const statusParts: string[] = []; statusParts.push(match.isVisible ? '✓ visible' : '✗ hidden'); if (!match.isVisible) { // Add reason for being hidden if (match.display === 'none') { statusParts.push('display: none'); } else if (match.opacity === 0) { statusParts.push('opacity: 0'); } else if (match.position.width === 0 || match.position.height === 0) { statusParts.push('zero size'); } } if (match.isInteractive) { statusParts.push('⚡ interactive'); } lines.push(` ${statusParts.join(', ')}`); lines.push(''); }); // Show omitted count and summary if (filteredMatches.length > limit) { const omitted = filteredMatches.length - limit; const matchType = onlyVisible === true ? 'visible ' : onlyVisible === false ? 'hidden ' : ''; lines.push( `Showing ${limit} of ${filteredMatches.length} ${matchType}matches (${omitted} omitted)` ); lines.push( `Use limit parameter to show more: { selector: "${args.selector}", limit: ${Math.min(filteredMatches.length, 50)} }` ); } else { const matchWord = filteredMatches.length === 1 ? 'match' : 'matches'; if (onlyVisible === true) { lines.push(`Showing ${filteredMatches.length} visible ${matchWord}`); } else if (onlyVisible === false) { lines.push(`Showing ${filteredMatches.length} hidden ${matchWord}`); } else { lines.push(`Showing all ${filteredMatches.length} ${matchWord}`); } } return createSuccessResponse(lines.join('\n')); } catch (error) { return createErrorResponse(`Failed to query selector: ${(error as Error).message}`); } }); } }

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/antonzherdev/mcp-web-inspector'

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