runAccessibilityAudit
Audit webpage accessibility to identify and address compliance issues for improved user experience.
Instructions
Run an accessibility audit on the current page
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- Core handler function that runs Lighthouse accessibility audit and extracts AI-optimized dataexport 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) }` ); } }
- Supporting handler function that processes raw Lighthouse results into structured AI-optimized accessibility report with issues, scores, and recommendationsconst 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, }; };
- browser-tools-mcp/mcp-server.ts:352-432 (registration)MCP tool registration and proxy handler that forwards to browser-tools-server /accessibility-audit endpointserver.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}`, }, ], }; } }); } );
- browser-tools-server/browser-connector.ts:1298-1303 (registration)HTTP endpoint registration for /accessibility-audit that invokes the lighthouse runAccessibilityAudit functionprivate setupAccessibilityAudit() { this.setupAuditEndpoint( AuditCategory.ACCESSIBILITY, "/accessibility-audit", runAccessibilityAudit );
- Input/output schema and type definitions for accessibility reports and issuesexport 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) }