diff --git a/browser-tools-mcp/mcp-server.ts b/browser-tools-mcp/mcp-server.ts index 6a9e2f8..4ca1981 100644 --- a/browser-tools-mcp/mcp-server.ts +++ b/browser-tools-mcp/mcp-server.ts @@ -2,8 +2,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import path from "path"; -import { z } from "zod"; // import { z } from "zod"; // import fs from "fs"; @@ -13,14 +11,14 @@ const server = new McpServer({ version: "1.0.9", }); -// Define audit categories as constants to avoid importing from the types file -const AUDIT_CATEGORIES = { - ACCESSIBILITY: "accessibility", - PERFORMANCE: "performance", - SEO: "seo", - BEST_PRACTICES: "best-practices", - PWA: "pwa", -}; +// Define audit categories as enum to match the server's AuditCategory enum +enum AuditCategory { + ACCESSIBILITY = "accessibility", + PERFORMANCE = "performance", + SEO = "seo", + BEST_PRACTICES = "best-practices", + PWA = "pwa", +} // Function to get the port from the .port file // function getPort(): number { @@ -197,7 +195,7 @@ server.tool("wipeLogs", "Wipe all browser logs from memory", async () => { }; }); -// Add tool for accessibility audits +// Add tool for accessibility audits, launches a headless browser instance server.tool( "runAccessibilityAudit", "Run a WCAG-compliant accessibility audit on the current page", @@ -210,7 +208,7 @@ server.tool( method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - category: AUDIT_CATEGORIES.ACCESSIBILITY, + category: AuditCategory.ACCESSIBILITY, }), } ); @@ -248,7 +246,7 @@ server.tool( } ); -// Add tool for performance audits +// Add tool for performance audits, launches a headless browser instance server.tool( "runPerformanceAudit", "Run a performance audit on the current page", @@ -262,7 +260,7 @@ server.tool( method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - category: AUDIT_CATEGORIES.PERFORMANCE, + category: AuditCategory.PERFORMANCE, }), } ); diff --git a/browser-tools-server/lighthouse/accessibility.ts b/browser-tools-server/lighthouse/accessibility.ts index c762357..2bf7288 100644 --- a/browser-tools-server/lighthouse/accessibility.ts +++ b/browser-tools-server/lighthouse/accessibility.ts @@ -3,11 +3,15 @@ import { AuditResult, AuditIssue, LighthouseDetails, - ElementDetails, ImpactLevel, AuditCategory, } from "./types.js"; -import { runLighthouseOnExistingTab } from "./index.js"; +import { + runLighthouseOnExistingTab, + mapAuditItemsToElements, + createAuditIssue, + createAuditMetadata, +} from "./index.js"; /** * Extracts simplified accessibility issues from Lighthouse results @@ -44,56 +48,30 @@ export function extractAccessibilityIssues( failedAudits.forEach(({ ref, audit }) => { const details = audit.details as LighthouseDetails; - // Extract actionable elements that need fixing - const elements = (details?.items || []).map( - (item: Record) => - ({ - selector: - ((item.node as Record)?.selector as string) || - (item.selector as string) || - "Unknown selector", - snippet: - ((item.node as Record)?.snippet as string) || - (item.snippet as string) || - "No snippet available", - explanation: - ((item.node as Record) - ?.explanation as string) || - (item.explanation as string) || - "No explanation available", - url: - (item.url as string) || - ((item.node as Record)?.url as string) || - "", - size: - (item.totalBytes as number) || - (item.transferSize as number) || - 0, - wastedMs: (item.wastedMs as number) || 0, - wastedBytes: (item.wastedBytes as number) || 0, - } as ElementDetails) + // Use the shared helper function to extract elements + const elements = mapAuditItemsToElements( + details?.items || [], + detailed ); if (elements.length > 0 || (audit.score || 0) < 1) { - const issue: AuditIssue = { - id: audit.id, - title: audit.title, - description: audit.description, - score: audit.score || 0, - details: detailed ? details : { type: details.type }, - category: categoryName, - wcagReference: ref.relevantAudits || [], - impact: - ((details?.items?.[0] as Record) - ?.impact as string) || ImpactLevel.MODERATE, - elements: detailed ? elements : elements.slice(0, 3), - failureSummary: - ((details?.items?.[0] as Record) - ?.failureSummary as string) || - audit.explanation || - "No failure summary available", - recommendations: [], - }; + // Use the shared helper function to create an audit issue + const impact = + ((details?.items?.[0] as Record) + ?.impact as string) || ImpactLevel.MODERATE; + const issue = createAuditIssue( + audit, + ref, + details, + elements, + categoryName, + impact + ); + + // Add detailed details if requested + if (detailed) { + issue.details = details; + } allIssues.push(issue); } @@ -124,19 +102,7 @@ export function extractAccessibilityIssues( categoryScores, issues: limitedIssues, ...(detailed && { - auditMetadata: { - fetchTime: lhr.fetchTime || new Date().toISOString(), - url: lhr.finalUrl || "Unknown URL", - deviceEmulation: "desktop", - categories: Object.keys(lhr.categories), - totalAudits: Object.keys(lhr.audits).length, - passedAudits: Object.values(lhr.audits).filter( - (audit) => audit.score === 1 - ).length, - failedAudits: Object.values(lhr.audits).filter( - (audit) => audit.score !== null && audit.score < 1 - ).length, - }, + auditMetadata: createAuditMetadata(lhr), }), }; } diff --git a/browser-tools-server/lighthouse/index.ts b/browser-tools-server/lighthouse/index.ts index 0839ff0..3e61c92 100644 --- a/browser-tools-server/lighthouse/index.ts +++ b/browser-tools-server/lighthouse/index.ts @@ -4,7 +4,13 @@ import { connectToHeadlessBrowser, scheduleBrowserCleanup, } from "../browser-utils.js"; -import { LighthouseConfig, AuditCategory } from "./types.js"; +import { + LighthouseConfig, + AuditCategory, + AuditIssue, + LighthouseDetails, + ImpactLevel, +} from "./types.js"; // ===== Type Definitions ===== @@ -67,9 +73,11 @@ export async function runLighthouseOnExistingTab( ): Promise { console.log(`Starting Lighthouse ${categories.join(", ")} audit for: ${url}`); - if (!url) { - console.error("URL is required for Lighthouse audit"); - throw new Error("URL is required for Lighthouse audit"); + 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 { @@ -81,32 +89,53 @@ export async function runLighthouseOnExistingTab( // For performance audits, we want to load all resources // For accessibility or other audits, we can block non-essential resources - const { port } = await connectToHeadlessBrowser(url, { - blockResources: !isPerformanceAudit, - // Don't pass an audit type - the blockResources flag is what matters - }); + try { + const { port } = await connectToHeadlessBrowser(url, { + blockResources: !isPerformanceAudit, + // Don't pass an audit type - the blockResources flag is what matters + }); - console.log(`Connected to browser on port: ${port}`); + console.log(`Connected to browser on port: ${port}`); - // Create Lighthouse config - const { flags, config } = createLighthouseConfig(categories); - flags.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 audit completed"); + console.log( + `Running Lighthouse with categories: ${categories.join(", ")}` + ); + const runnerResult = await lighthouse(url, flags as Flags, config); + console.log("Lighthouse audit completed"); - if (!runnerResult?.lhr) { - console.error("Lighthouse audit failed to produce results"); - throw new Error("Lighthouse audit failed to produce results"); + 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; } - - // Schedule browser cleanup after a delay to allow for subsequent audits - scheduleBrowserCleanup(); - - // Return the result - const result = runnerResult.lhr; - return result; } catch (error) { console.error("Lighthouse audit failed:", error); // Schedule browser cleanup even if the audit fails @@ -123,3 +152,102 @@ export async function runLighthouseOnExistingTab( export * from "./accessibility.js"; export * from "./performance.js"; export * from "./types.js"; + +/** + * Maps Lighthouse audit items to ElementDetails objects + * @param items Array of audit items from Lighthouse + * @param detailed Whether to include all items or limit them + * @returns Array of ElementDetails objects + */ +export function mapAuditItemsToElements( + items: Record[] = [], + detailed: boolean = false +): ElementDetails[] { + const elements = items.map( + (item: Record) => + ({ + selector: + ((item.node as Record)?.selector as string) || + (item.selector as string) || + "Unknown selector", + snippet: + ((item.node as Record)?.snippet as string) || + (item.snippet as string) || + "No snippet available", + explanation: + ((item.node as Record)?.explanation as string) || + (item.explanation as string) || + "No explanation available", + url: + (item.url as string) || + ((item.node as Record)?.url as string) || + "", + size: (item.totalBytes as number) || (item.transferSize as number) || 0, + wastedMs: (item.wastedMs as number) || 0, + wastedBytes: (item.wastedBytes as number) || 0, + } as ElementDetails) + ); + + return detailed ? elements : elements.slice(0, 3); +} + +/** + * Creates an AuditIssue object from Lighthouse audit data + * @param audit The Lighthouse audit object + * @param ref The audit reference object + * @param details The audit details object + * @param elements Array of ElementDetails objects + * @param categoryName The category name + * @param impact The impact level (optional) + * @returns An AuditIssue object + */ +export function createAuditIssue( + audit: any, + ref: any, + details: LighthouseDetails, + elements: ElementDetails[], + categoryName: string, + impact?: string +): AuditIssue { + return { + id: audit.id, + title: audit.title, + description: audit.description, + score: audit.score || 0, + details: { type: details?.type || "unknown" }, + category: categoryName, + wcagReference: ref.relevantAudits || [], + impact: + impact || + ((details?.items?.[0] as Record)?.impact as string) || + ImpactLevel.MODERATE, + elements: elements, + failureSummary: + ((details?.items?.[0] as Record) + ?.failureSummary as string) || + audit.explanation || + "No failure summary available", + recommendations: [], + }; +} + +/** + * Creates audit metadata from Lighthouse results + * @param lhr The Lighthouse result object + * @returns Audit metadata object + */ +export function createAuditMetadata(lhr: LighthouseResult): any { + return { + fetchTime: lhr.fetchTime || new Date().toISOString(), + url: lhr.finalUrl || "Unknown URL", + deviceEmulation: "desktop", + categories: Object.keys(lhr.categories), + totalAudits: Object.keys(lhr.audits || {}).length, + passedAudits: Object.values(lhr.audits || {}).filter( + (audit) => audit.score === 1 + ).length, + failedAudits: Object.values(lhr.audits || {}).filter( + (audit) => audit.score !== null && audit.score < 1 + ).length, + }; +} diff --git a/browser-tools-server/lighthouse/performance.ts b/browser-tools-server/lighthouse/performance.ts index 57660e1..ba3bab0 100644 --- a/browser-tools-server/lighthouse/performance.ts +++ b/browser-tools-server/lighthouse/performance.ts @@ -7,7 +7,12 @@ import { ImpactLevel, AuditCategory, } from "./types.js"; -import { runLighthouseOnExistingTab } from "./index.js"; +import { + runLighthouseOnExistingTab, + mapAuditItemsToElements, + createAuditIssue, + createAuditMetadata, +} from "./index.js"; /** * Extracts performance issues from Lighthouse results @@ -21,8 +26,6 @@ export function extractPerformanceIssues( limit: number = 5, detailed: boolean = false ): Partial { - console.log("Processing performance audit results"); - const allIssues: AuditIssue[] = []; const categoryScores: { [key: string]: number } = {}; @@ -67,8 +70,6 @@ export function extractPerformanceIssues( ((item.audit.details as LighthouseDetails)?.items?.length || 0) > 0) ); - console.log(`Found ${failedAudits.length} performance issues`); - if (failedAudits.length > 0) { failedAudits.forEach(({ ref, audit }) => { try { @@ -80,70 +81,28 @@ export function extractPerformanceIssues( return; } - // Extract actionable elements that need fixing - const elements = (details.items || []).map( - (item: Record) => { - try { - return { - selector: - ((item.node as Record) - ?.selector as string) || - (item.selector as string) || - "Unknown selector", - snippet: - ((item.node as Record) - ?.snippet as string) || - (item.snippet as string) || - "No snippet available", - explanation: - ((item.node as Record) - ?.explanation as string) || - (item.explanation as string) || - "No explanation available", - url: - (item.url as string) || - ((item.node as Record)?.url as string) || - "", - size: - (item.totalBytes as number) || - (item.transferSize as number) || - 0, - wastedMs: (item.wastedMs as number) || 0, - wastedBytes: (item.wastedBytes as number) || 0, - } as ElementDetails; - } catch (error) { - console.error(`Error processing element: ${error}`); - return { - selector: "Error processing element", - snippet: "Error", - explanation: "Error processing element details", - url: "", - size: 0, - wastedMs: 0, - wastedBytes: 0, - } as ElementDetails; - } - } + // Use the shared helper function to extract elements + const elements = mapAuditItemsToElements( + details.items || [], + detailed ); if (elements.length > 0 || (audit.score || 0) < 0.9) { - const issue: AuditIssue = { - id: audit.id, - title: audit.title, - description: audit.description, - score: audit.score || 0, - details: detailed ? details : { type: details.type || "unknown" }, - category: categoryName, - wcagReference: ref.relevantAudits || [], - impact: getPerformanceImpact(audit.score || 0), - elements: detailed ? elements : elements.slice(0, 3), - failureSummary: - ((details?.items?.[0] as Record) - ?.failureSummary as string) || - audit.explanation || - "No failure summary available", - recommendations: [], - }; + // Use the shared helper function to create an audit issue + const impact = getPerformanceImpact(audit.score || 0); + const issue = createAuditIssue( + audit, + ref, + details, + elements, + categoryName, + impact + ); + + // Add detailed details if requested + if (detailed) { + issue.details = details; + } allIssues.push(issue); } @@ -160,26 +119,12 @@ export function extractPerformanceIssues( // Return only the specified number of issues const limitedIssues = allIssues.slice(0, limit); - console.log(`Returning ${limitedIssues.length} performance issues`); - return { score: categoryScores.performance || 0, categoryScores, issues: limitedIssues, ...(detailed && { - auditMetadata: { - fetchTime: lhr.fetchTime || new Date().toISOString(), - url: lhr.finalUrl || "Unknown URL", - deviceEmulation: "desktop", - categories: Object.keys(lhr.categories), - totalAudits: Object.keys(lhr.audits || {}).length, - passedAudits: Object.values(lhr.audits || {}).filter( - (audit) => audit.score === 1 - ).length, - failedAudits: Object.values(lhr.audits || {}).filter( - (audit) => audit.score !== null && audit.score < 1 - ).length, - }, + auditMetadata: createAuditMetadata(lhr), }), }; }