mirror of
https://github.com/AgentDeskAI/browser-tools-mcp.git
synced 2025-11-20 12:08:31 +00:00
Refactor Lighthouse audit utilities and improve error handling
This commit is contained in:
parent
37269d1e2d
commit
0d995a3219
@ -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,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
@ -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<string, unknown>) =>
|
||||
({
|
||||
selector:
|
||||
((item.node as Record<string, unknown>)?.selector as string) ||
|
||||
(item.selector as string) ||
|
||||
"Unknown selector",
|
||||
snippet:
|
||||
((item.node as Record<string, unknown>)?.snippet as string) ||
|
||||
(item.snippet as string) ||
|
||||
"No snippet available",
|
||||
explanation:
|
||||
((item.node as Record<string, unknown>)
|
||||
?.explanation as string) ||
|
||||
(item.explanation as string) ||
|
||||
"No explanation available",
|
||||
url:
|
||||
(item.url as string) ||
|
||||
((item.node as Record<string, unknown>)?.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<string, unknown>)
|
||||
?.impact as string) || ImpactLevel.MODERATE,
|
||||
elements: detailed ? elements : elements.slice(0, 3),
|
||||
failureSummary:
|
||||
((details?.items?.[0] as Record<string, unknown>)
|
||||
?.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<string, unknown>)
|
||||
?.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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<LighthouseResult> {
|
||||
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<string, unknown>[] = [],
|
||||
detailed: boolean = false
|
||||
): ElementDetails[] {
|
||||
const elements = items.map(
|
||||
(item: Record<string, unknown>) =>
|
||||
({
|
||||
selector:
|
||||
((item.node as Record<string, unknown>)?.selector as string) ||
|
||||
(item.selector as string) ||
|
||||
"Unknown selector",
|
||||
snippet:
|
||||
((item.node as Record<string, unknown>)?.snippet as string) ||
|
||||
(item.snippet as string) ||
|
||||
"No snippet available",
|
||||
explanation:
|
||||
((item.node as Record<string, unknown>)?.explanation as string) ||
|
||||
(item.explanation as string) ||
|
||||
"No explanation available",
|
||||
url:
|
||||
(item.url as string) ||
|
||||
((item.node as Record<string, unknown>)?.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<string, unknown>)?.impact as string) ||
|
||||
ImpactLevel.MODERATE,
|
||||
elements: elements,
|
||||
failureSummary:
|
||||
((details?.items?.[0] as Record<string, unknown>)
|
||||
?.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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<AuditResult> {
|
||||
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<string, unknown>) => {
|
||||
try {
|
||||
return {
|
||||
selector:
|
||||
((item.node as Record<string, unknown>)
|
||||
?.selector as string) ||
|
||||
(item.selector as string) ||
|
||||
"Unknown selector",
|
||||
snippet:
|
||||
((item.node as Record<string, unknown>)
|
||||
?.snippet as string) ||
|
||||
(item.snippet as string) ||
|
||||
"No snippet available",
|
||||
explanation:
|
||||
((item.node as Record<string, unknown>)
|
||||
?.explanation as string) ||
|
||||
(item.explanation as string) ||
|
||||
"No explanation available",
|
||||
url:
|
||||
(item.url as string) ||
|
||||
((item.node as Record<string, unknown>)?.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<string, unknown>)
|
||||
?.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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user