runSEOAudit
Analyze webpage SEO performance by auditing on-page elements, content quality, and technical factors to identify optimization opportunities.
Instructions
Run an SEO audit on the current page
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- browser-tools-mcp/mcp-server.ts:518-578 (registration)MCP tool registration and handler for 'runSEOAudit'. Proxies request to browser server /seo-audit endpoint on current page.server.tool( "runSEOAudit", "Run an SEO audit on the current page", {}, async () => { return await withServerConnection(async () => { try { console.log( `Sending POST request to http://${discoveredHost}:${discoveredPort}/seo-audit` ); const response = await fetch( `http://${discoveredHost}:${discoveredPort}/seo-audit`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ category: AuditCategory.SEO, source: "mcp_tool", timestamp: Date.now(), }), } ); // Log the response status console.log(`SEO audit response status: ${response.status}`); if (!response.ok) { const errorText = await response.text(); console.error(`SEO audit error: ${errorText}`); throw new Error(`Server returned ${response.status}: ${errorText}`); } const json = await response.json(); 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 SEO audit:", errorMessage); return { content: [ { type: "text", text: `Failed to run SEO audit: ${errorMessage}`, }, ], }; } }); } );
- Core handler function runSEOAudit that executes Lighthouse SEO audit and processes results into AI-optimized report with issues, scores, and recommendations.export async function runSEOAudit(url: string): Promise<AIOptimizedSEOReport> { try { const lhr = await runLighthouseAudit(url, [AuditCategory.SEO]); return extractAIOptimizedData(lhr, url); } catch (error) { throw new Error( `SEO audit failed: ${ error instanceof Error ? error.message : String(error) }` ); } } /** * Extract AI-optimized SEO data from Lighthouse results */ const extractAIOptimizedData = ( lhr: LighthouseResult, url: string ): AIOptimizedSEOReport => { const categoryData = lhr.categories[AuditCategory.SEO]; 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: AISEOIssue[] = []; const categories: { [category: string]: { score: number; issues_count: number }; } = { content: { score: 0, issues_count: 0 }, mobile: { score: 0, issues_count: 0 }, crawlability: { score: 0, issues_count: 0 }, other: { score: 0, issues_count: 0 }, }; // 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++; } } // Categorize the issue let category = "other"; if ( ref.id.includes("crawl") || ref.id.includes("http") || ref.id.includes("redirect") || ref.id.includes("robots") ) { category = "crawlability"; } else if ( ref.id.includes("viewport") || ref.id.includes("font-size") || ref.id.includes("tap-targets") ) { category = "mobile"; } else if ( ref.id.includes("document") || ref.id.includes("meta") || ref.id.includes("description") || ref.id.includes("canonical") || ref.id.includes("title") || ref.id.includes("link") ) { category = "content"; } // Update category score and issues count if (audit.score !== null && audit.score < 0.9) { categories[category].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 failed audits - we'll filter dynamically based on impact .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"; } // Categorize the issue let category = "other"; if ( ref.id.includes("crawl") || ref.id.includes("http") || ref.id.includes("redirect") || ref.id.includes("robots") ) { category = "crawlability"; } else if ( ref.id.includes("viewport") || ref.id.includes("font-size") || ref.id.includes("tap-targets") ) { category = "mobile"; } else if ( ref.id.includes("document") || ref.id.includes("meta") || ref.id.includes("description") || ref.id.includes("canonical") || ref.id.includes("title") || ref.id.includes("link") ) { category = "content"; } // Extract details const details: { selector?: string; value?: string; issue?: string }[] = []; if (audit.details) { const auditDetails = audit.details as any; if (auditDetails.items && Array.isArray(auditDetails.items)) { // Determine item limit based on impact const itemLimit = DETAIL_LIMITS[impact]; auditDetails.items.slice(0, itemLimit).forEach((item: any) => { const detail: { selector?: string; value?: string; issue?: string; } = {}; if (item.selector) { detail.selector = item.selector; } if (item.value !== undefined) { detail.value = item.value; } if (item.issue) { detail.issue = item.issue; } if (Object.keys(detail).length > 0) { details.push(detail); } }); } } // Create the issue const issue: AISEOIssue = { id: ref.id, title: audit.title, impact, category, details: details.length > 0 ? details : 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]) => { if (data.issues_count === 0) return; let recommendation = ""; switch (category) { case "content": recommendation = `Improve SEO content (${data.issues_count} issues): titles, descriptions, and headers`; break; case "mobile": recommendation = `Optimize for mobile devices (${data.issues_count} issues)`; break; case "crawlability": recommendation = `Fix crawlability issues (${data.issues_count} issues): robots.txt, sitemaps, and redirects`; break; default: recommendation = `Fix ${data.issues_count} SEO issues in category: ${category}`; } prioritized_recommendations.push(recommendation); }); // Add specific high-impact recommendations if (issues.some((issue) => issue.id === "meta-description")) { prioritized_recommendations.push( "Add a meta description to improve click-through rate" ); } if (issues.some((issue) => issue.id === "document-title")) { prioritized_recommendations.push( "Add a descriptive page title with keywords" ); } if (issues.some((issue) => issue.id === "hreflang")) { prioritized_recommendations.push( "Fix hreflang implementation for international SEO" ); } if (issues.some((issue) => issue.id === "canonical")) { prioritized_recommendations.push("Implement proper canonical tags"); } // Create the report content const reportContent: SEOReportContent = { score, audit_counts: { failed: failedCount, passed: passedCount, manual: manualCount, informative: informativeCount, not_applicable: notApplicableCount, }, issues, categories, prioritized_recommendations: prioritized_recommendations.length > 0 ? prioritized_recommendations : undefined, }; // Return the full report following the LighthouseReport interface return { metadata, report: reportContent, }; };
- Type definitions for SEO report structure, including SEOReportContent, AIOptimizedSEOReport, and AISEOIssue interfaces./** * SEO-specific report content structure */ export interface SEOReportContent { 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: AISEOIssue[]; categories: { [category: string]: { score: number; issues_count: number; }; }; prioritized_recommendations?: string[]; // Ordered list of recommendations } /** * Full SEO report implementing the base LighthouseReport interface */ export type AIOptimizedSEOReport = LighthouseReport<SEOReportContent>; /** * AI-optimized SEO issue */ interface AISEOIssue { id: string; // e.g., "meta-description" title: string; // e.g., "Document has a meta description" impact: "critical" | "serious" | "moderate" | "minor"; category: string; // e.g., "content", "mobile", "crawlability" details?: { selector?: string; // CSS selector if applicable value?: string; // Current value issue?: string; // Description of the issue }[]; score: number | null; // 0-1 or null } // Original interfaces for backward compatibility interface SEOAudit {
- HTTP endpoint handler setup for POST /seo-audit that retrieves current page URL and calls runSEOAudit(url).private setupSEOAudit() { this.setupAuditEndpoint(AuditCategory.SEO, "/seo-audit", runSEOAudit); } // Add a setup method for Best Practices audit private setupBestPracticesAudit() { this.setupAuditEndpoint( AuditCategory.BEST_PRACTICES, "/best-practices-audit", runBestPracticesAudit ); } /** * Generic method to set up an audit endpoint * @param auditType The type of audit (accessibility, performance, SEO) * @param endpoint The endpoint path * @param auditFunction The audit function to call */ private setupAuditEndpoint( auditType: string, endpoint: string, auditFunction: (url: string) => Promise<LighthouseReport> ) { // Add server identity validation endpoint this.app.get("/.identity", (req, res) => { res.json({ signature: "mcp-browser-connector-24x7", version: "1.2.0", }); }); this.app.post(endpoint, async (req: any, res: any) => { try { console.log(`${auditType} audit request received`); // Get URL using our helper method const url = await this.getUrlForAudit(); if (!url) { console.log(`No URL available for ${auditType} audit`); return res.status(400).json({ error: `URL is required for ${auditType} audit. Make sure you navigate to a page in the browser first, and the browser-tool extension tab is open.`, }); } // If we're using the stored URL (not from request body), log it now if (!req.body?.url && url === currentUrl) { console.log(`Using stored URL for ${auditType} audit:`, url); } // Check if we're using the default URL if (url === "about:blank") { console.log(`Cannot run ${auditType} audit on about:blank`); return res.status(400).json({ error: `Cannot run ${auditType} audit on about:blank`, }); } console.log(`Preparing to run ${auditType} audit for: ${url}`); // Run the audit using the provided function try { const result = await auditFunction(url); console.log(`${auditType} audit completed successfully`); // Return the results res.json(result); } catch (auditError) { console.error(`${auditType} audit failed:`, auditError); const errorMessage = auditError instanceof Error ? auditError.message : String(auditError); res.status(500).json({ error: `Failed to run ${auditType} audit: ${errorMessage}`, }); } } catch (error) { console.error(`Error in ${auditType} audit endpoint:`, error); const errorMessage = error instanceof Error ? error.message : String(error); res.status(500).json({ error: `Error in ${auditType} audit endpoint: ${errorMessage}`, }); } }); }
- Helper function runLighthouseAudit that launches headless browser, runs Lighthouse core engine, used by all category-specific audits.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) }` ); } }