runSEOAudit
Analyze and optimize webpage SEO by auditing on-page elements, meta tags, and content structure directly within the browser using BrowserTools MCP's Chrome extension.
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 server.tool registration for 'runSEOAudit' which proxies requests to the browser server /seo-audit endpointserver.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 runs Lighthouse SEO audit and processes resultsexport 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) }` ); } }
- extractAIOptimizedData helper that transforms raw Lighthouse results into structured AIOptimizedSEOReport/** * 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 SEOReportContent and AIOptimizedSEOReport (input/output schema)/** * 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>;
- browser-tools-server/browser-connector.ts:1316-1318 (registration)Endpoint registration for /seo-audit that calls lighthouse runSEOAudit (imported line 17)private setupSEOAudit() { this.setupAuditEndpoint(AuditCategory.SEO, "/seo-audit", runSEOAudit); }