Skip to main content
Glama

runAccessibilityAudit

Audit webpage accessibility to identify compliance issues and improve user experience for people with disabilities.

Instructions

Run an accessibility audit on the current page

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • MCP server tool registration for 'runAccessibilityAudit'. This handler proxies the request to the browser connector server's /accessibility-audit endpoint, processes the JSON response, and returns it in MCP format.
    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}`, }, ], }; } }); } );
  • Core handler function that executes the Lighthouse accessibility audit on the given URL and processes results using internal extractor.
    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) }` ); } }
  • Helper function that extracts and optimizes Lighthouse accessibility audit data into a structured AI-friendly report including scores, categorized issues, critical elements, and prioritized recommendations.
    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, }; };
  • Type definitions for the AI-optimized accessibility report structure, including content schema with scores, audit counts, issues, categories, and elements.
    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>;
  • Generic helper that runs Lighthouse audits using Puppeteer headless browser, configurable for different categories including accessibility.
    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