Skip to main content
Glama

runAccessibilityAudit

Analyze webpage accessibility to ensure compliance with standards, identifying issues for improved user experience and inclusivity.

Instructions

Run an accessibility audit on the current page

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • Core handler function runAccessibilityAudit that executes Lighthouse accessibility audit, processes raw results into AI-optimized structured report with issues, scores, recommendations, and element details.
    export async function runAccessibilityAudit( url: string ): Promise<AIOptimizedAccessibilityReport> { try { const lhr = await runLighthouseAudit(url, [AuditCategory.ACCESSIBILITY]); return extractAIOptimizedData(lhr, url); } catch (error) { throw new Error( `Accessibility audit failed: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Extract AI-optimized accessibility data from Lighthouse results */ const extractAIOptimizedData = ( lhr: LighthouseResult, url: string ): AIOptimizedAccessibilityReport => { const categoryData = lhr.categories[AuditCategory.ACCESSIBILITY]; const audits = lhr.audits || {}; // Add metadata const metadata = { url, timestamp: lhr.fetchTime || new Date().toISOString(), device: "desktop", // This could be made configurable lighthouseVersion: lhr.lighthouseVersion, }; // Initialize variables const issues: AIAccessibilityIssue[] = []; const criticalElements: AIAccessibilityElement[] = []; const categories: { [category: string]: { score: number; issues_count: number }; } = {}; // Count audits by type let failedCount = 0; let passedCount = 0; let manualCount = 0; let informativeCount = 0; let notApplicableCount = 0; // Process audit refs const auditRefs = categoryData?.auditRefs || []; // First pass: count audits by type and initialize categories auditRefs.forEach((ref) => { const audit = audits[ref.id]; if (!audit) return; // Count by scoreDisplayMode if (audit.scoreDisplayMode === "manual") { manualCount++; } else if (audit.scoreDisplayMode === "informative") { informativeCount++; } else if (audit.scoreDisplayMode === "notApplicable") { notApplicableCount++; } else if (audit.score !== null) { // Binary pass/fail if (audit.score >= 0.9) { passedCount++; } else { failedCount++; } } // Process categories if (ref.group) { // Initialize category if not exists if (!categories[ref.group]) { categories[ref.group] = { score: 0, issues_count: 0 }; } // Update category score and issues count if (audit.score !== null && audit.score < 0.9) { categories[ref.group].issues_count++; } } }); // Second pass: process failed audits into AI-friendly format auditRefs .filter((ref) => { const audit = audits[ref.id]; return audit && audit.score !== null && audit.score < 0.9; }) .sort((a, b) => (b.weight || 0) - (a.weight || 0)) // No limit on number of failed audits - we'll show them all .forEach((ref) => { const audit = audits[ref.id]; // Determine impact level based on score and weight let impact: "critical" | "serious" | "moderate" | "minor" = "moderate"; if (audit.score === 0) { impact = "critical"; } else if (audit.score !== null && audit.score <= 0.5) { impact = "serious"; } else if (audit.score !== null && audit.score > 0.7) { impact = "minor"; } // Create elements array const elements: AIAccessibilityElement[] = []; if (audit.details) { const details = audit.details as any; if (details.items && Array.isArray(details.items)) { const items = details.items; // Apply limits based on impact level const itemLimit = DETAIL_LIMITS[impact]; items.slice(0, itemLimit).forEach((item: any) => { if (item.node) { const element: AIAccessibilityElement = { selector: item.node.selector, snippet: item.node.snippet, label: item.node.nodeLabel, issue_description: item.node.explanation || item.explanation, }; if (item.value !== undefined) { element.value = item.value; } elements.push(element); // Add to critical elements if impact is critical or serious if (impact === "critical" || impact === "serious") { criticalElements.push(element); } } }); } } // Create the issue const issue: AIAccessibilityIssue = { id: ref.id, title: audit.title, impact, category: ref.group || "other", elements: elements.length > 0 ? elements : undefined, score: audit.score, }; issues.push(issue); }); // Calculate overall score const score = Math.round((categoryData?.score || 0) * 100); // Generate prioritized recommendations const prioritized_recommendations: string[] = []; // Add category-specific recommendations Object.entries(categories) .filter(([_, data]) => data.issues_count > 0) .sort(([_, a], [__, b]) => b.issues_count - a.issues_count) .forEach(([category, data]) => { let recommendation = ""; switch (category) { case "a11y-color-contrast": recommendation = "Improve color contrast for better readability"; break; case "a11y-names-labels": recommendation = "Add proper labels to all interactive elements"; break; case "a11y-aria": recommendation = "Fix ARIA attributes and roles"; break; case "a11y-navigation": recommendation = "Improve keyboard navigation and focus management"; break; case "a11y-language": recommendation = "Add proper language attributes to HTML"; break; case "a11y-tables-lists": recommendation = "Fix table and list structures for screen readers"; break; default: recommendation = `Fix ${data.issues_count} issues in ${category}`; } prioritized_recommendations.push(recommendation); }); // Add specific high-impact recommendations if (issues.some((issue) => issue.id === "color-contrast")) { prioritized_recommendations.push( "Fix low contrast text for better readability" ); } if (issues.some((issue) => issue.id === "document-title")) { prioritized_recommendations.push("Add a descriptive page title"); } if (issues.some((issue) => issue.id === "image-alt")) { prioritized_recommendations.push("Add alt text to all images"); } // Create the report content const reportContent: AccessibilityReportContent = { score, audit_counts: { failed: failedCount, passed: passedCount, manual: manualCount, informative: informativeCount, not_applicable: notApplicableCount, }, issues, categories, critical_elements: criticalElements, prioritized_recommendations: prioritized_recommendations.length > 0 ? prioritized_recommendations : undefined, }; // Return the full report following the LighthouseReport interface return { metadata, report: reportContent, }; };
  • MCP tool registration: server.tool('runAccessibilityAudit') that discovers browser server and forwards request to POST /accessibility-audit HTTP endpoint.
    server.tool( "runAccessibilityAudit", "Run an accessibility audit on the current page", {}, async () => { return await withServerConnection(async () => { try { // Simplified approach - let the browser connector handle the current tab and URL console.log( `Sending POST request to http://${discoveredHost}:${discoveredPort}/accessibility-audit` ); const response = await fetch( `http://${discoveredHost}:${discoveredPort}/accessibility-audit`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ category: AuditCategory.ACCESSIBILITY, source: "mcp_tool", timestamp: Date.now(), }), } ); // Log the response status console.log(`Accessibility audit response status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); console.error(`Accessibility audit error: ${errorText}`); throw new Error(`Server returned ${response.status}: ${errorText}`); } const json = await response.json(); // flatten it by merging metadata with the report contents if (json.report) { const { metadata, report } = json; const flattened = { ...metadata, ...report, }; return { content: [ { type: "text", text: JSON.stringify(flattened, null, 2), }, ], }; } else { // Return as-is if it's not in the new format return { content: [ { type: "text", text: JSON.stringify(json, null, 2), }, ], }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error("Error in accessibility audit:", errorMessage); return { content: [ { type: "text", text: `Failed to run accessibility audit: ${errorMessage}`, }, ], }; } }); } );
  • HTTP POST /accessibility-audit endpoint registration in browser server that receives requests from MCP and calls runAccessibilityAudit(url).
    // Set up accessibility audit endpoint this.setupAccessibilityAudit(); // Set up performance audit endpoint this.setupPerformanceAudit(); // Set up SEO audit endpoint this.setupSEOAudit(); // Set up Best Practices audit endpoint this.setupBestPracticesAudit(); // Handle upgrade requests for WebSocket
  • Core type definitions: AuditCategory enum, LighthouseReport interface, LighthouseConfig.
    /** * Audit categories available in Lighthouse */ export enum AuditCategory { ACCESSIBILITY = "accessibility", PERFORMANCE = "performance", SEO = "seo", BEST_PRACTICES = "best-practices", // Not yet implemented PWA = "pwa", // Not yet implemented } /** * Base interface for Lighthouse report metadata */ export interface LighthouseReport<T = any> { metadata: { url: string; timestamp: string; // ISO 8601, e.g., "2025-02-27T14:30:00Z" device: string; // e.g., "mobile", "desktop" lighthouseVersion: string; // e.g., "10.4.0" }; // For backward compatibility with existing report formats overallScore?: number; failedAuditsCount?: number; passedAuditsCount?: number; manualAuditsCount?: number; informativeAuditsCount?: number; notApplicableAuditsCount?: number; failedAudits?: any[]; // New format for specialized reports report?: T; // Generic report data that will be specialized by each audit type } /** * Configuration options for Lighthouse audits */ export interface LighthouseConfig { flags: { output: string[]; onlyCategories: string[]; formFactor: string; port: number | undefined; screenEmulation: { mobile: boolean; width: number; height: number; deviceScaleFactor: number; disabled: boolean; }; }; config: { extends: string; settings: { onlyCategories: string[]; emulatedFormFactor: string; throttling: { cpuSlowdownMultiplier: number; }; }; }; }
  • Accessibility-specific schema: AccessibilityReportContent, AIOptimizedAccessibilityReport, AIAccessibilityIssue, AIAccessibilityElement interfaces.
    /** * Accessibility-specific report content structure */ export interface AccessibilityReportContent { score: number; // Overall score (0-100) audit_counts: { // Counts of different audit types failed: number; passed: number; manual: number; informative: number; not_applicable: number; }; issues: AIAccessibilityIssue[]; categories: { [category: string]: { score: number; issues_count: number; }; }; critical_elements: AIAccessibilityElement[]; prioritized_recommendations?: string[]; // Ordered list of recommendations } /** * Full accessibility report implementing the base LighthouseReport interface */ export type AIOptimizedAccessibilityReport = LighthouseReport<AccessibilityReportContent>; /** * AI-optimized accessibility issue */ interface AIAccessibilityIssue { id: string; // e.g., "color-contrast" title: string; // e.g., "Color contrast is sufficient" impact: "critical" | "serious" | "moderate" | "minor"; category: string; // e.g., "contrast", "aria", "forms", "keyboard" elements?: AIAccessibilityElement[]; // Elements with issues score: number | null; // 0-1 or null } /** * Accessibility element with issues */ interface AIAccessibilityElement { selector: string; // CSS selector snippet?: string; // HTML snippet label?: string; // Element label issue_description?: string; // Description of the issue value?: string | number; // Current value (e.g., contrast ratio) } // Original interfaces for backward compatibility interface AccessibilityAudit { id: string; // e.g., "color-contrast" title: string; // e.g., "Color contrast is sufficient" description: string; // e.g., "Ensures text is readable..." score: number | null; // 0-1 (normalized), null for manual/informative scoreDisplayMode: string; // e.g., "binary", "numeric", "manual" details?: AuditDetails; // Optional, structured details weight?: number; // Optional, audit weight for impact calculation } type AuditDetails = { items?: Array<{ node?: { selector: string; // e.g., ".my-class" snippet?: string; // HTML snippet nodeLabel?: string; // e.g., "Modify logging size limits / truncation" explanation?: string; // Explanation of why the node fails the audit }; value?: string | number; // Specific value (e.g., contrast ratio) explanation?: string; // Explanation at the item level }>; debugData?: string; // Optional, debug information [key: string]: any; // Flexible for other detail types (tables, etc.) }; // Original limits were optimized for human consumption // This ensures we always include critical issues while limiting less important ones const DETAIL_LIMITS = { critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues serious: 15, // Up to 15 items for serious issues moderate: 10, // Up to 10 items for moderate issues minor: 3, // Up to 3 items for minor issues };
  • Helper function runLighthouseAudit: launches dedicated headless browser, runs Lighthouse with specified categories, handles cleanup.
    export async function runLighthouseAudit( url: string, categories: string[] ): Promise<LighthouseResult> { console.log(`Starting Lighthouse ${categories.join(", ")} audit for: ${url}`); if (!url || url === "about:blank") { console.error("Invalid URL for Lighthouse audit"); throw new Error( "Cannot run audit on an empty page or about:blank. Please navigate to a valid URL first." ); } try { // Always use a dedicated headless browser for audits console.log("Using dedicated headless browser for audit"); // Determine if this is a performance audit - we need to load all resources for performance audits const isPerformanceAudit = categories.includes(AuditCategory.PERFORMANCE); // For performance audits, we want to load all resources // For accessibility or other audits, we can block non-essential resources try { const { port } = await connectToHeadlessBrowser(url, { blockResources: !isPerformanceAudit, }); console.log(`Connected to browser on port: ${port}`); // Create Lighthouse config const { flags, config } = createLighthouseConfig(categories); flags.port = port; console.log( `Running Lighthouse with categories: ${categories.join(", ")}` ); const runnerResult = await lighthouse(url, flags as Flags, config); console.log("Lighthouse scan completed"); if (!runnerResult?.lhr) { console.error("Lighthouse audit failed to produce results"); throw new Error("Lighthouse audit failed to produce results"); } // Schedule browser cleanup after a delay to allow for subsequent audits scheduleBrowserCleanup(); // Return the result const result = runnerResult.lhr; return result; } catch (browserError) { // Check if the error is related to Chrome/Edge not being available const errorMessage = browserError instanceof Error ? browserError.message : String(browserError); if ( errorMessage.includes("Chrome could not be found") || errorMessage.includes("Failed to launch browser") || errorMessage.includes("spawn ENOENT") ) { throw new Error( "Chrome or Edge browser could not be found. Please ensure that Chrome or Edge is installed on your system to run audits." ); } // Re-throw other errors throw browserError; } } catch (error) { console.error("Lighthouse audit failed:", error); // Schedule browser cleanup even if the audit fails scheduleBrowserCleanup(); throw new Error( `Lighthouse audit failed: ${ error instanceof Error ? error.message : String(error) }` ); } }

Latest Blog Posts

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/oenius/browser-tools-mcp'

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