Skip to main content
Glama

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
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • 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)
          }`
        );
      }
    }

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