Skip to main content
Glama
kbyk004-diy

Playwright-Lighthouse MCP Server

by kbyk004-diy

run-lighthouse

Analyze website performance, accessibility, SEO, and best practices using Lighthouse on the currently open page to identify improvement opportunities.

Instructions

Runs a Lighthouse performance analysis on the currently open page

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
urlYesURL of the website you want to analyze
categoriesNoCategories to analyze (performance, accessibility, best-practices, seo, pwa)
maxItemsNoMaximum number of improvement items to display for each category

Implementation Reference

  • src/index.ts:76-442 (registration)
    Registration of the 'run-lighthouse' tool using McpServer.tool method, including description, input schema, and inline handler function.
    server.tool(
      "run-lighthouse",
      "Runs a Lighthouse performance analysis on the currently open page",
      {
        url: z.string().url().describe("URL of the website you want to analyze"),
        categories: z.array(z.enum(["performance", "accessibility", "best-practices", "seo", "pwa"]))
          .default(["performance"])
          .describe("Categories to analyze (performance, accessibility, best-practices, seo, pwa)"),
        maxItems: z.number().min(1).max(5).default(3)
          .describe("Maximum number of improvement items to display for each category"),
      },
      async (params, extra): Promise<{
        content: { type: "text"; text: string }[];
        isError?: boolean;
      }> => {
        try {
          // Automatically launch browser and navigate to URL
          await navigateToUrl(params.url);
    
          const url = page!.url();
    
          try {
            // CDP connection method for the latest Playwright version
            const browserContext = browser!.contexts()[0];
            const cdpSession = await browserContext.newCDPSession(page!);
            
            // Get browser version information to check debug port
            const versionInfo = await cdpSession.send('Browser.getVersion');
            
            // Get port number from WebSocket debugger URL
            // Note: Using the port specified at launch (9222)
            const port = 9222;
    
            // Function to run Lighthouse audit
            const runAudit = async () => {
              try {
                // Create report path
                const hostname = new URL(url).hostname.replace(/\./g, '-');
                const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
                const reportPath = path.join(__dirname, `../reports/lighthouse-${hostname}-${timestamp}.json`);
                
                try {
                  // Run Lighthouse audit
                  const results = await playAudit({
                    page: page!,
                    port: port,
                    thresholds: {
                      performance: 0,
                      accessibility: 0,
                      'best-practices': 0,
                      seo: 0,
                      pwa: 0
                    },
                    reports: {
                      formats: {
                        html: false,
                        json: true
                      },
                      directory: path.join(__dirname, "../reports"),
                      name: `lighthouse-${hostname}-${timestamp}`
                    },
                    ignoreError: true,
                    config: {
                      extends: 'lighthouse:default'
                    }
                  });
                  
                  // Function to represent score evaluation with color
                  const getScoreEmoji = (score: number): string => {
                    if (score >= 90) return "🟢"; // Good
                    if (score >= 50) return "🟠"; // Average
                    return "🔴"; // Poor
                  };
    
                  // Process results directly
                  let scoreText = "📊 Lighthouse Scores:\n";
                  let improvementText = "\n\n🔍 Key Improvement Areas:";
                  
                  // Prepare arrays to store improvement items
                  const improvementItems: { category: string; title: string; description: string }[] = [];
                  
                  // Check if results are available directly
                  if (results && results.lhr && results.lhr.categories) {
                    // Get selected categories from the direct results
                    const availableCategories = Object.keys(results.lhr.categories);
                    
                    // Filter categories based on user selection
                    const selectedCategories = params.categories.filter(cat => 
                      availableCategories.includes(cat)
                    );
                    
                    // Process each category
                    for (const category of selectedCategories) {
                      const categoryData = results.lhr.categories[category];
                      
                      if (categoryData) {
                        // Get all audits for this category
                        const audits = results.lhr.audits;
                        const categoryAudits = Object.keys(audits).filter(
                          auditId => {
                            const audit = audits[auditId];
                            return audit.details && 
                                   categoryData.auditRefs.some((ref: any) => ref.id === auditId);
                          }
                        );
                        
                        // Get score
                        let scoreDisplay = '';
                        
                        if (categoryData.score === null) {
                          // When score cannot be calculated
                          scoreDisplay = `⚪️ ${category.charAt(0).toUpperCase() + category.slice(1)}: Not measurable`;
                        } else {
                          // When score can be calculated
                          const score = Math.round(categoryData.score * 100);
                          scoreDisplay = `${getScoreEmoji(score)} ${category.charAt(0).toUpperCase() + category.slice(1)}: ${score}/100`;
                        }
                        
                        // Add score to response
                        scoreText += scoreDisplay + '\n';
                        
                        // Collect improvement items
                        for (const auditId of categoryAudits) {
                          const audit = audits[auditId];
                          if ((audit.score || 0) < 0.9) {
                            improvementItems.push({
                              category,
                              title: audit.title,
                              description: audit.description,
                            });
                          }
                        }
                      }
                    }
                  } else {
                    // Fallback to reading from file if direct results are not available
                    try {
                      // Load JSON file
                      if (existsSync(reportPath)) {
                        // Read and parse JSON file
                        const jsonData = JSON.parse(readFileSync(reportPath, 'utf8'));
                        
                        if (jsonData && jsonData.categories) {
                          // Get selected categories from the report
                          const availableCategories = Object.keys(jsonData.categories);
                          
                          // Filter categories based on user selection
                          const selectedCategories = params.categories.filter(cat => 
                            availableCategories.includes(cat)
                          );
                          
                          // Process each category
                          for (const category of selectedCategories) {
                            const categoryData = jsonData.categories[category];
                            
                            if (categoryData) {
                              // Get all audits for this category
                              const audits = jsonData.audits;
                              const categoryAudits = Object.keys(audits).filter(
                                auditId => {
                                  const audit = audits[auditId];
                                  return audit.details && 
                                         categoryData.auditRefs.some((ref: any) => ref.id === auditId);
                                }
                              );
                              
                              // Get score
                              let scoreDisplay = '';
                              
                              if (categoryData.score === null) {
                                // When score cannot be calculated
                                scoreDisplay = `⚪️ ${category.charAt(0).toUpperCase() + category.slice(1)}: Not measurable`;
                              } else {
                                // When score can be calculated
                                const score = Math.round(categoryData.score * 100);
                                scoreDisplay = `${getScoreEmoji(score)} ${category.charAt(0).toUpperCase() + category.slice(1)}: ${score}/100`;
                              }
                              
                              // Add score to response
                              scoreText += scoreDisplay + '\n';
                              
                              // Collect improvement items
                              for (const auditId of categoryAudits) {
                                const audit = audits[auditId];
                                if ((audit.score || 0) < 0.9) {
                                  improvementItems.push({
                                    category,
                                    title: audit.title,
                                    description: audit.description,
                                  });
                                }
                              }
                            }
                          }
                        }
                      } else {
                        // List all files in directory
                        const files = readdirSync(path.join(__dirname, "../reports"));
                        
                        // Find the latest JSON file
                        const jsonFiles = files.filter(file => file.endsWith('.json'));
                        if (jsonFiles.length > 0) {
                          const latestFile = jsonFiles.sort().pop();
                          
                          // Use the latest file
                          const latestPath = path.join(__dirname, "../reports", latestFile || '');
                          try {
                            const latestData = JSON.parse(readFileSync(latestPath, 'utf8'));
                            
                            if (latestData && latestData.categories) {
                              // Process each category
                              for (const category of params.categories) {
                                const categoryData = latestData.categories[category];
                                
                                if (categoryData) {
                                  // Get all audits for this category
                                  const audits = latestData.audits;
                                  const categoryAudits = Object.keys(audits).filter(
                                    auditId => {
                                      const audit = audits[auditId];
                                      return audit.details && 
                                             categoryData.auditRefs.some((ref: any) => ref.id === auditId);
                                    }
                                  );
                                  
                                  // Get score
                                  let scoreDisplay = '';
                                  
                                  if (categoryData.score === null) {
                                    // When score cannot be calculated
                                    scoreDisplay = `⚪️ ${category.charAt(0).toUpperCase() + category.slice(1)}: Not measurable`;
                                  } else {
                                    // When score can be calculated
                                    const score = Math.round(categoryData.score * 100);
                                    scoreDisplay = `${getScoreEmoji(score)} ${category.charAt(0).toUpperCase() + category.slice(1)}: ${score}/100`;
                                  }
                                  
                                  // Add score to response
                                  scoreText += scoreDisplay + '\n';
                                  
                                  // Collect improvement items
                                  for (const auditId of categoryAudits) {
                                    const audit = audits[auditId];
                                    if ((audit.score || 0) < 0.9) {
                                      improvementItems.push({
                                        category,
                                        title: audit.title,
                                        description: audit.description,
                                      });
                                    }
                                  }
                                }
                              }
                            }
                          } catch (err: any) {
                            throw new Error(`Failed to read latest JSON file: ${err.message}`);
                          }
                        } else {
                          throw new Error('Lighthouse report file not found.');
                        }
                      }
                    } catch (error) {
                      throw error; // Propagate to higher error handler
                    }
                  }
    
                  // Display improvement points (sorted by weight)
                  if (improvementItems.length > 0) {
                    // Sort by category
                    improvementItems.sort((a, b) => {
                      if (a.category !== b.category) {
                        return a.category.localeCompare(b.category);
                      }
                      return a.title.localeCompare(b.title);
                    });
                    
                    // Group and display
                    let currentCategory = '';
                    for (const imp of improvementItems.slice(0, params.maxItems * params.categories.length)) {
                      if (currentCategory !== imp.category) {
                        currentCategory = imp.category;
                        // Display category name appropriately
                        const categoryDisplayName = {
                          'performance': 'Performance',
                          'accessibility': 'Accessibility',
                          'best-practices': 'Best Practices',
                          'seo': 'SEO',
                          'pwa': 'PWA'
                        }[imp.category] || imp.category;
                        
                        improvementText += `\n\n【${categoryDisplayName}】Improvement items:`;
                      }
                      improvementText += `\n・${imp.title}`;
                    }
                  } else {
                    improvementText += "\n\nNo improvement items found.";
                  }
    
                  // Close browser automatically after analysis is complete
                  await closeBrowser();
    
                  // Return the results
                  return {
                    content: [
                      {
                        type: "text" as const,
                        text: scoreText + improvementText,
                      },
                      {
                        type: "text" as const,
                        text: `report save path: ${reportPath}`,
                      },
                    ],
                  };
                } catch (error: any) {
                  // Close browser even when an error occurs
                  await closeBrowser();
                  
                  throw error; // Propagate to higher error handler
                }
              } catch (error: any) {
                // Close browser even when an error occurs
                await closeBrowser();
                
                return {
                  content: [
                    {
                      type: "text" as const,
                      text: `An error occurred during Lighthouse analysis: ${error instanceof Error ? error.message : String(error)}`,
                    },
                  ],
                  isError: true,
                };
              }
            };
    
            return await runAudit();
          } catch (error: any) {
            // Close browser even when an error occurs
            await closeBrowser();
            
            return {
              content: [
                {
                  type: "text" as const,
                  text: `An error occurred during Lighthouse analysis: ${error instanceof Error ? error.message : String(error)}`,
                },
              ],
              isError: true,
            };
          }
        } catch (error: any) {
          // Close browser even when an error occurs
          await closeBrowser();
    
          return {
            content: [
              {
                type: "text" as const,
                text: `An error occurred during Lighthouse analysis: ${error instanceof Error ? error.message : String(error)}`,
              },
            ],
            isError: true,
          };
        }
      }
    );
  • Zod schema defining input parameters for the run-lighthouse tool: url (required), categories (array of enums, default ['performance']), maxItems (number 1-5, default 3).
    {
      url: z.string().url().describe("URL of the website you want to analyze"),
      categories: z.array(z.enum(["performance", "accessibility", "best-practices", "seo", "pwa"]))
        .default(["performance"])
        .describe("Categories to analyze (performance, accessibility, best-practices, seo, pwa)"),
      maxItems: z.number().min(1).max(5).default(3)
        .describe("Maximum number of improvement items to display for each category"),
    },
  • Core handler logic: launches browser/page if needed, navigates to URL, sets up CDP for port 9222, runs playAudit from playwright-lighthouse to generate JSON report, parses Lighthouse results (lhr.categories, audits), computes scores (0-100 with emojis), collects low-score audit improvements limited by maxItems per category, formats text output, saves report to ../reports/, closes browser.
    async (params, extra): Promise<{
      content: { type: "text"; text: string }[];
      isError?: boolean;
    }> => {
      try {
        // Automatically launch browser and navigate to URL
        await navigateToUrl(params.url);
    
        const url = page!.url();
    
        try {
          // CDP connection method for the latest Playwright version
          const browserContext = browser!.contexts()[0];
          const cdpSession = await browserContext.newCDPSession(page!);
          
          // Get browser version information to check debug port
          const versionInfo = await cdpSession.send('Browser.getVersion');
          
          // Get port number from WebSocket debugger URL
          // Note: Using the port specified at launch (9222)
          const port = 9222;
    
          // Function to run Lighthouse audit
          const runAudit = async () => {
            try {
              // Create report path
              const hostname = new URL(url).hostname.replace(/\./g, '-');
              const timestamp = new Date().toISOString().replace(/:/g, '-').split('.')[0];
              const reportPath = path.join(__dirname, `../reports/lighthouse-${hostname}-${timestamp}.json`);
              
              try {
                // Run Lighthouse audit
                const results = await playAudit({
                  page: page!,
                  port: port,
                  thresholds: {
                    performance: 0,
                    accessibility: 0,
                    'best-practices': 0,
                    seo: 0,
                    pwa: 0
                  },
                  reports: {
                    formats: {
                      html: false,
                      json: true
                    },
                    directory: path.join(__dirname, "../reports"),
                    name: `lighthouse-${hostname}-${timestamp}`
                  },
                  ignoreError: true,
                  config: {
                    extends: 'lighthouse:default'
                  }
                });
                
                // Function to represent score evaluation with color
                const getScoreEmoji = (score: number): string => {
                  if (score >= 90) return "🟢"; // Good
                  if (score >= 50) return "🟠"; // Average
                  return "🔴"; // Poor
                };
    
                // Process results directly
                let scoreText = "📊 Lighthouse Scores:\n";
                let improvementText = "\n\n🔍 Key Improvement Areas:";
                
                // Prepare arrays to store improvement items
                const improvementItems: { category: string; title: string; description: string }[] = [];
                
                // Check if results are available directly
                if (results && results.lhr && results.lhr.categories) {
                  // Get selected categories from the direct results
                  const availableCategories = Object.keys(results.lhr.categories);
                  
                  // Filter categories based on user selection
                  const selectedCategories = params.categories.filter(cat => 
                    availableCategories.includes(cat)
                  );
                  
                  // Process each category
                  for (const category of selectedCategories) {
                    const categoryData = results.lhr.categories[category];
                    
                    if (categoryData) {
                      // Get all audits for this category
                      const audits = results.lhr.audits;
                      const categoryAudits = Object.keys(audits).filter(
                        auditId => {
                          const audit = audits[auditId];
                          return audit.details && 
                                 categoryData.auditRefs.some((ref: any) => ref.id === auditId);
                        }
                      );
                      
                      // Get score
                      let scoreDisplay = '';
                      
                      if (categoryData.score === null) {
                        // When score cannot be calculated
                        scoreDisplay = `⚪️ ${category.charAt(0).toUpperCase() + category.slice(1)}: Not measurable`;
                      } else {
                        // When score can be calculated
                        const score = Math.round(categoryData.score * 100);
                        scoreDisplay = `${getScoreEmoji(score)} ${category.charAt(0).toUpperCase() + category.slice(1)}: ${score}/100`;
                      }
                      
                      // Add score to response
                      scoreText += scoreDisplay + '\n';
                      
                      // Collect improvement items
                      for (const auditId of categoryAudits) {
                        const audit = audits[auditId];
                        if ((audit.score || 0) < 0.9) {
                          improvementItems.push({
                            category,
                            title: audit.title,
                            description: audit.description,
                          });
                        }
                      }
                    }
                  }
                } else {
                  // Fallback to reading from file if direct results are not available
                  try {
                    // Load JSON file
                    if (existsSync(reportPath)) {
                      // Read and parse JSON file
                      const jsonData = JSON.parse(readFileSync(reportPath, 'utf8'));
                      
                      if (jsonData && jsonData.categories) {
                        // Get selected categories from the report
                        const availableCategories = Object.keys(jsonData.categories);
                        
                        // Filter categories based on user selection
                        const selectedCategories = params.categories.filter(cat => 
                          availableCategories.includes(cat)
                        );
                        
                        // Process each category
                        for (const category of selectedCategories) {
                          const categoryData = jsonData.categories[category];
                          
                          if (categoryData) {
                            // Get all audits for this category
                            const audits = jsonData.audits;
                            const categoryAudits = Object.keys(audits).filter(
                              auditId => {
                                const audit = audits[auditId];
                                return audit.details && 
                                       categoryData.auditRefs.some((ref: any) => ref.id === auditId);
                              }
                            );
                            
                            // Get score
                            let scoreDisplay = '';
                            
                            if (categoryData.score === null) {
                              // When score cannot be calculated
                              scoreDisplay = `⚪️ ${category.charAt(0).toUpperCase() + category.slice(1)}: Not measurable`;
                            } else {
                              // When score can be calculated
                              const score = Math.round(categoryData.score * 100);
                              scoreDisplay = `${getScoreEmoji(score)} ${category.charAt(0).toUpperCase() + category.slice(1)}: ${score}/100`;
                            }
                            
                            // Add score to response
                            scoreText += scoreDisplay + '\n';
                            
                            // Collect improvement items
                            for (const auditId of categoryAudits) {
                              const audit = audits[auditId];
                              if ((audit.score || 0) < 0.9) {
                                improvementItems.push({
                                  category,
                                  title: audit.title,
                                  description: audit.description,
                                });
                              }
                            }
                          }
                        }
                      }
                    } else {
                      // List all files in directory
                      const files = readdirSync(path.join(__dirname, "../reports"));
                      
                      // Find the latest JSON file
                      const jsonFiles = files.filter(file => file.endsWith('.json'));
                      if (jsonFiles.length > 0) {
                        const latestFile = jsonFiles.sort().pop();
                        
                        // Use the latest file
                        const latestPath = path.join(__dirname, "../reports", latestFile || '');
                        try {
                          const latestData = JSON.parse(readFileSync(latestPath, 'utf8'));
                          
                          if (latestData && latestData.categories) {
                            // Process each category
                            for (const category of params.categories) {
                              const categoryData = latestData.categories[category];
                              
                              if (categoryData) {
                                // Get all audits for this category
                                const audits = latestData.audits;
                                const categoryAudits = Object.keys(audits).filter(
                                  auditId => {
                                    const audit = audits[auditId];
                                    return audit.details && 
                                           categoryData.auditRefs.some((ref: any) => ref.id === auditId);
                                  }
                                );
                                
                                // Get score
                                let scoreDisplay = '';
                                
                                if (categoryData.score === null) {
                                  // When score cannot be calculated
                                  scoreDisplay = `⚪️ ${category.charAt(0).toUpperCase() + category.slice(1)}: Not measurable`;
                                } else {
                                  // When score can be calculated
                                  const score = Math.round(categoryData.score * 100);
                                  scoreDisplay = `${getScoreEmoji(score)} ${category.charAt(0).toUpperCase() + category.slice(1)}: ${score}/100`;
                                }
                                
                                // Add score to response
                                scoreText += scoreDisplay + '\n';
                                
                                // Collect improvement items
                                for (const auditId of categoryAudits) {
                                  const audit = audits[auditId];
                                  if ((audit.score || 0) < 0.9) {
                                    improvementItems.push({
                                      category,
                                      title: audit.title,
                                      description: audit.description,
                                    });
                                  }
                                }
                              }
                            }
                          }
                        } catch (err: any) {
                          throw new Error(`Failed to read latest JSON file: ${err.message}`);
                        }
                      } else {
                        throw new Error('Lighthouse report file not found.');
                      }
                    }
                  } catch (error) {
                    throw error; // Propagate to higher error handler
                  }
                }
    
                // Display improvement points (sorted by weight)
                if (improvementItems.length > 0) {
                  // Sort by category
                  improvementItems.sort((a, b) => {
                    if (a.category !== b.category) {
                      return a.category.localeCompare(b.category);
                    }
                    return a.title.localeCompare(b.title);
                  });
                  
                  // Group and display
                  let currentCategory = '';
                  for (const imp of improvementItems.slice(0, params.maxItems * params.categories.length)) {
                    if (currentCategory !== imp.category) {
                      currentCategory = imp.category;
                      // Display category name appropriately
                      const categoryDisplayName = {
                        'performance': 'Performance',
                        'accessibility': 'Accessibility',
                        'best-practices': 'Best Practices',
                        'seo': 'SEO',
                        'pwa': 'PWA'
                      }[imp.category] || imp.category;
                      
                      improvementText += `\n\n【${categoryDisplayName}】Improvement items:`;
                    }
                    improvementText += `\n・${imp.title}`;
                  }
                } else {
                  improvementText += "\n\nNo improvement items found.";
                }
    
                // Close browser automatically after analysis is complete
                await closeBrowser();
    
                // Return the results
                return {
                  content: [
                    {
                      type: "text" as const,
                      text: scoreText + improvementText,
                    },
                    {
                      type: "text" as const,
                      text: `report save path: ${reportPath}`,
                    },
                  ],
                };
              } catch (error: any) {
                // Close browser even when an error occurs
                await closeBrowser();
                
                throw error; // Propagate to higher error handler
              }
            } catch (error: any) {
              // Close browser even when an error occurs
              await closeBrowser();
              
              return {
                content: [
                  {
                    type: "text" as const,
                    text: `An error occurred during Lighthouse analysis: ${error instanceof Error ? error.message : String(error)}`,
                  },
                ],
                isError: true,
              };
            }
          };
    
          return await runAudit();
        } catch (error: any) {
          // Close browser even when an error occurs
          await closeBrowser();
          
          return {
            content: [
              {
                type: "text" as const,
                text: `An error occurred during Lighthouse analysis: ${error instanceof Error ? error.message : String(error)}`,
              },
            ],
            isError: true,
          };
        }
      } catch (error: any) {
        // Close browser even when an error occurs
        await closeBrowser();
    
        return {
          content: [
            {
              type: "text" as const,
              text: `An error occurred during Lighthouse analysis: ${error instanceof Error ? error.message : String(error)}`,
            },
          ],
          isError: true,
        };
      }
    }
  • Helper function to launch browser/page and navigate to the analysis URL, called first in handler.
    async function navigateToUrl(url: string) {
      try {
        const page = await getPage();
        await page.goto(url, { waitUntil: "load" });
        return page;
      } catch (error) {
        throw error;
      }
    }
  • Helper to launch Chromium browser with remote debugging port 9222 required for Lighthouse via playwright-lighthouse.
    async function launchBrowser() {
      if (!browser) {
        // Launch browser with remote debugging port
        browser = await chromium.launch({
          headless: true,
          args: [
            '--remote-debugging-port=9222',
            '--ignore-certificate-errors'
          ],
          timeout: 30000,
        });
      }
      return browser;
    }
Install Server

Other Tools

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/kbyk004-diy/playwright-lighthouse-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server