This is page 1 of 3. Use http://codebase.md/sugatraj/cursor-browser-tools-mcp?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .gitignore ├── browser-tools-mcp │ ├── mcp-server.ts │ ├── package-lock.json │ ├── package.json │ ├── README.md │ └── tsconfig.json ├── browser-tools-server │ ├── browser-connector.ts │ ├── lighthouse │ │ ├── accessibility.ts │ │ ├── best-practices.ts │ │ ├── index.ts │ │ ├── performance.ts │ │ ├── seo.ts │ │ └── types.ts │ ├── package-lock.json │ ├── package.json │ ├── puppeteer-service.ts │ ├── README.md │ └── tsconfig.json ├── chrome-extension │ ├── background.js │ ├── devtools.html │ ├── devtools.js │ ├── manifest.json │ ├── panel.html │ └── panel.js ├── debugcommands.mdc ├── docs │ ├── mcp-docs.md │ └── mcp.md ├── LICENSE ├── package.json ├── README.md ├── SETUP_INSTRUCTIONS.md ├── start-servers.bat └── start-servers.sh ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Node modules 2 | node_modules/ 3 | 4 | # Build directories 5 | browser-tools-mcp/dist/ 6 | browser-tools-server/dist/ 7 | browser-tools-server/dist-electron/ 8 | browser-tools-mcp/dist-electron/ 9 | 10 | # Environment files 11 | .env 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | # Log files 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | logs 22 | *.log 23 | 24 | # Editor directories and files 25 | .idea/ 26 | .vscode/ 27 | *.suo 28 | *.ntvs* 29 | *.njsproj 30 | *.sln 31 | *.sw? 32 | 33 | # OS files 34 | .DS_Store 35 | Thumbs.db 36 | ``` -------------------------------------------------------------------------------- /browser-tools-mcp/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Browser Tools MCP Server 2 | 3 | A Model Context Protocol (MCP) server that provides AI-powered browser tools integration. This server works in conjunction with the Browser Tools Server to provide AI capabilities for browser debugging and analysis. 4 | 5 | ## Features 6 | 7 | - MCP protocol implementation 8 | - Browser console log access 9 | - Network request analysis 10 | - Screenshot capture capabilities 11 | - Element selection and inspection 12 | - Real-time browser state monitoring 13 | - Accessibility, performance, SEO, and best practices audits 14 | 15 | ## Prerequisites 16 | 17 | - Node.js 14 or higher 18 | - Browser Tools Server running 19 | - Chrome or Chromium browser installed (required for audit functionality) 20 | 21 | ## Installation 22 | 23 | ```bash 24 | npx @agentdeskai/browser-tools-mcp 25 | ``` 26 | 27 | Or install globally: 28 | 29 | ```bash 30 | npm install -g @agentdeskai/browser-tools-mcp 31 | ``` 32 | 33 | ## Usage 34 | 35 | 1. First, make sure the Browser Tools Server is running: 36 | 37 | ```bash 38 | npx @agentdeskai/browser-tools-server 39 | ``` 40 | 41 | 2. Then start the MCP server: 42 | 43 | ```bash 44 | npx @agentdeskai/browser-tools-mcp 45 | ``` 46 | 47 | 3. The MCP server will connect to the Browser Tools Server and provide the following capabilities: 48 | 49 | - Console log retrieval 50 | - Network request monitoring 51 | - Screenshot capture 52 | - Element selection 53 | - Browser state analysis 54 | - Accessibility and performance audits 55 | 56 | ## MCP Functions 57 | 58 | The server provides the following MCP functions: 59 | 60 | - `mcp_getConsoleLogs` - Retrieve browser console logs 61 | - `mcp_getConsoleErrors` - Get browser console errors 62 | - `mcp_getNetworkErrors` - Get network error logs 63 | - `mcp_getNetworkSuccess` - Get successful network requests 64 | - `mcp_getNetworkLogs` - Get all network logs 65 | - `mcp_getSelectedElement` - Get the currently selected DOM element 66 | - `mcp_runAccessibilityAudit` - Run a WCAG-compliant accessibility audit 67 | - `mcp_runPerformanceAudit` - Run a performance audit 68 | - `mcp_runSEOAudit` - Run an SEO audit 69 | - `mcp_runBestPracticesAudit` - Run a best practices audit 70 | 71 | ## Integration 72 | 73 | This server is designed to work with AI tools and platforms that support the Model Context Protocol (MCP). It provides a standardized interface for AI models to interact with browser state and debugging information. 74 | 75 | ## License 76 | 77 | MIT 78 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # BrowserTools MCP 2 | 3 | > Make your AI tools 10x more aware and capable of interacting with your browser 4 | 5 | This application is a powerful browser monitoring and interaction tool that enables AI-powered applications via Anthropic's Model Context Protocol (MCP) to capture and analyze browser data through a Chrome extension. 6 | 7 | Read our [docs](https://browsertools.agentdesk.ai/) for the full installation, quickstart and contribution guides. 8 | 9 | <a href="https://glama.ai/mcp/servers/@Sugatraj/Cursor-Browser-Tools-MCP"> 10 | <img width="380" height="200" src="https://glama.ai/mcp/servers/@Sugatraj/Cursor-Browser-Tools-MCP/badge" alt="BrowserTools MCP server" /> 11 | </a> 12 | 13 | ## Roadmap 14 | 15 | Check out our project roadmap here: [Github Roadmap / Project Board](https://github.com/orgs/AgentDeskAI/projects/1/views/1) 16 | 17 | ## Updates 18 | 19 | v1.2.0 is out! Here's a quick breakdown of the update: 20 | - You can now enable "Allow Auto-Paste into Cursor" within the DevTools panel. Screenshots will be automatically pasted into Cursor (just make sure to focus/click into the Agent input field in Cursor, otherwise it won't work!) 21 | - Integrated a suite of SEO, performance, accessibility, and best practice analysis tools via Lighthouse 22 | - Implemented a NextJS specific prompt used to improve SEO for a NextJS application 23 | - Added Debugger Mode as a tool which executes all debugging tools in a particular sequence, along with a prompt to improve reasoning 24 | - Added Audit Mode as a tool to execute all auditing tools in a particular sequence 25 | - Resolved Windows connectivity issues 26 | - Improved networking between BrowserTools server, extension and MCP server with host/port auto-discovery, auto-reconnect, and graceful shutdown mechanisms 27 | - Added ability to more easily exit out of the Browser Tools server with Ctrl+C 28 | 29 | Please make sure to update the version in your IDE / MCP client as so: 30 | `npx @agentdeskai/[email protected]` 31 | 32 | Also make sure to download the latest version of the chrome extension here: 33 | [v1.2.0 BrowserToolsMCP Chrome Extension](https://github.com/AgentDeskAI/browser-tools-mcp/releases/download/v1.2.0/BrowserTools-1.2.0-extension.zip) 34 | 35 | From there you can run the local node server like so: 36 | `npx @agentdeskai/[email protected]` 37 | 38 | Make sure to specify version 1.2.0 since NPX caching may prevent you from getting the latest version! You should only have to do this once for every update. After you do it once, you should be on the latest version. 39 | 40 | And once you've opened your chrome dev tools, logs should be getting sent to your server 🦾 41 | 42 | If you have any questions or issues, feel free to open an issue ticket! And if you have any ideas to make this better, feel free to reach out or open an issue ticket with an enhancement tag or reach out to me at [@tedx_ai on x](https://x.com/tedx_ai) 43 | 44 | ## Full Update Notes: 45 | 46 | Coding agents like Cursor can run these audits against the current page seamlessly. By leveraging Puppeteer and the Lighthouse npm library, BrowserTools MCP can now: 47 | 48 | - Evaluate pages for WCAG compliance 49 | - Identify performance bottlenecks 50 | - Flag on-page SEO issues 51 | - Check adherence to web development best practices 52 | - Review NextJS specific issues with SEO 53 | 54 | ...all without leaving your IDE 🎉 55 | 56 | --- 57 | 58 | ## 🔑 Key Additions 59 | 60 | | Audit Type | Description | 61 | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | 62 | | **Accessibility** | WCAG-compliant checks for color contrast, missing alt text, keyboard navigation traps, ARIA attributes, and more. | 63 | | **Performance** | Lighthouse-driven analysis of render-blocking resources, excessive DOM size, unoptimized images, and other factors affecting page speed. | 64 | | **SEO** | Evaluates on-page SEO factors (like metadata, headings, and link structure) and suggests improvements for better search visibility. | 65 | | **Best Practices** | Checks for general best practices in web development. | 66 | | **NextJS Audit** | Injects a prompt used to perform a NextJS audit. | 67 | | **Audit Mode** | Runs all auditing tools in a sequence. | 68 | | **Debugger Mode** | Runs all debugging tools in a sequence. | 69 | 70 | --- 71 | 72 | ## 🛠️ Using Audit Tools 73 | 74 | ### ✅ **Before You Start** 75 | 76 | Ensure you have: 77 | 78 | - An **active tab** in your browser 79 | - The **BrowserTools extension enabled** 80 | 81 | ### ▶️ **Running Audits** 82 | 83 | **Headless Browser Automation**: 84 | Puppeteer automates a headless Chrome instance to load the page and collect audit data, ensuring accurate results even for SPAs or content loaded via JavaScript. 85 | 86 | The headless browser instance remains active for **60 seconds** after the last audit call to efficiently handle consecutive audit requests. 87 | 88 | **Structured Results**: 89 | Each audit returns results in a structured JSON format, including overall scores and detailed issue lists. This makes it easy for MCP-compatible clients to interpret the findings and present actionable insights. 90 | 91 | The MCP server provides tools to run audits on the current page. Here are example queries you can use to trigger them: 92 | 93 | #### Accessibility Audit (`runAccessibilityAudit`) 94 | 95 | Ensures the page meets accessibility standards like WCAG. 96 | 97 | > **Example Queries:** 98 | > 99 | > - "Are there any accessibility issues on this page?" 100 | > - "Run an accessibility audit." 101 | > - "Check if this page meets WCAG standards." 102 | 103 | #### Performance Audit (`runPerformanceAudit`) 104 | 105 | Identifies performance bottlenecks and loading issues. 106 | 107 | > **Example Queries:** 108 | > 109 | > - "Why is this page loading so slowly?" 110 | > - "Check the performance of this page." 111 | > - "Run a performance audit." 112 | 113 | #### SEO Audit (`runSEOAudit`) 114 | 115 | Evaluates how well the page is optimized for search engines. 116 | 117 | > **Example Queries:** 118 | > 119 | > - "How can I improve SEO for this page?" 120 | > - "Run an SEO audit." 121 | > - "Check SEO on this page." 122 | 123 | #### Best Practices Audit (`runBestPracticesAudit`) 124 | 125 | Checks for general best practices in web development. 126 | 127 | > **Example Queries:** 128 | > 129 | > - "Run a best practices audit." 130 | > - "Check best practices on this page." 131 | > - "Are there any best practices issues on this page?" 132 | 133 | #### Audit Mode (`runAuditMode`) 134 | 135 | Runs all audits in a particular sequence. Will run a NextJS audit if the framework is detected. 136 | 137 | > **Example Queries:** 138 | > - "Run audit mode." 139 | > - "Enter audit mode." 140 | 141 | #### NextJS Audits (`runNextJSAudit`) 142 | 143 | Checks for best practices and SEO improvements for NextJS applications. 144 | 145 | > **Example Queries:** 146 | > - "Run a NextJS audit." 147 | > - "Run a NextJS audit, I'm using app router." 148 | > - "Run a NextJS audit, I'm using page router." 149 | 150 | #### Debugger Mode (`runDebuggerMode`) 151 | 152 | Runs all debugging tools in a particular sequence. 153 | 154 | > **Example Queries:** 155 | > - "Enter debugger mode." 156 | 157 | ## Architecture 158 | 159 | There are three core components all used to capture and analyze browser data: 160 | 161 | 1. **Chrome Extension**: A browser extension that captures screenshots, console logs, network activity and DOM elements. 162 | 2. **Node Server**: An intermediary server that facilitates communication between the Chrome extension and any instance of an MCP server. 163 | 3. **MCP Server**: A Model Context Protocol server that provides standardized tools for AI clients to interact with the browser. 164 | 165 | ``` 166 | ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌─────────────┐ 167 | │ MCP Client │ ──► │ MCP Server │ ──► │ Node Server │ ──► │ Chrome │ 168 | │ (e.g. │ ◄── │ (Protocol │ ◄── │ (Middleware) │ ◄── │ Extension │ 169 | │ Cursor) │ │ Handler) │ │ │ │ │ 170 | └─────────────┘ └──────────────┘ └───────────────┘ └─────────────┘ 171 | ``` 172 | 173 | Model Context Protocol (MCP) is a capability supported by Anthropic AI models that 174 | allow you to create custom tools for any compatible client. MCP clients like Claude 175 | Desktop, Cursor, Cline or Zed can run an MCP server which "teaches" these clients 176 | about a new tool that they can use. 177 | 178 | These tools can call out to external APIs but in our case, **all logs are stored locally** on your machine and NEVER sent out to any third-party service or API. BrowserTools MCP runs a local instance of a NodeJS API server which communicates with the BrowserTools Chrome Extension. 179 | 180 | All consumers of the BrowserTools MCP Server interface with the same NodeJS API and Chrome extension. 181 | 182 | #### Chrome Extension 183 | 184 | - Monitors XHR requests/responses and console logs 185 | - Tracks selected DOM elements 186 | - Sends all logs and current element to the BrowserTools Connector 187 | - Connects to Websocket server to capture/send screenshots 188 | - Allows user toconfigure token/truncation limits + screenshot folder path 189 | 190 | #### Node Server 191 | 192 | - Acts as middleware between the Chrome extension and MCP server 193 | - Receives logs and currently selected element from Chrome extension 194 | - Processes requests from MCP server to capture logs, screenshot or current element 195 | - Sends Websocket command to the Chrome extension for capturing a screenshot 196 | - Intelligently truncates strings and # of duplicate objects in logs to avoid token limits 197 | - Removes cookies and sensitive headers to avoid sending to LLMs in MCP clients 198 | 199 | #### MCP Server 200 | 201 | - Implements the Model Context Protocol 202 | - Provides standardized tools for AI clients 203 | - Compatible with various MCP clients (Cursor, Cline, Zed, Claude Desktop, etc.) 204 | 205 | ## Installation 206 | 207 | Installation steps can be found in our documentation: 208 | 209 | - [BrowserTools MCP Docs](https://browsertools.agentdesk.ai/) 210 | 211 | ## Usage 212 | 213 | Once installed and configured, the system allows any compatible MCP client to: 214 | 215 | - Monitor browser console output 216 | - Capture network traffic 217 | - Take screenshots 218 | - Analyze selected elements 219 | - Wipe logs stored in our MCP server 220 | - Run accessibility, performance, SEO, and best practices audits 221 | 222 | ## Compatibility 223 | 224 | - Works with any MCP-compatible client 225 | - Primarily designed for Cursor IDE integration 226 | - Supports other AI editors and MCP clients ``` -------------------------------------------------------------------------------- /browser-tools-server/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Browser Tools Server 2 | 3 | A powerful browser tools server for capturing and managing browser events, logs, and screenshots. This server works in conjunction with the Browser Tools Chrome Extension to provide comprehensive browser debugging capabilities. 4 | 5 | ## Features 6 | 7 | - Console log capture 8 | - Network request monitoring 9 | - Screenshot capture 10 | - Element selection tracking 11 | - WebSocket real-time communication 12 | - Configurable log limits and settings 13 | - Lighthouse-powered accessibility, performance, SEO, and best practices audits 14 | 15 | ## Installation 16 | 17 | ```bash 18 | npx @agentdeskai/browser-tools-server 19 | ``` 20 | 21 | Or install globally: 22 | 23 | ```bash 24 | npm install -g @agentdeskai/browser-tools-server 25 | ``` 26 | 27 | ## Usage 28 | 29 | 1. Start the server: 30 | 31 | ```bash 32 | npx @agentdeskai/browser-tools-server 33 | ``` 34 | 35 | 2. The server will start on port 3025 by default 36 | 37 | 3. Install and enable the Browser Tools Chrome Extension 38 | 39 | 4. The server exposes the following endpoints: 40 | 41 | - `/console-logs` - Get console logs 42 | - `/console-errors` - Get console errors 43 | - `/network-errors` - Get network error logs 44 | - `/network-success` - Get successful network requests 45 | - `/all-xhr` - Get all network requests 46 | - `/screenshot` - Capture screenshots 47 | - `/selected-element` - Get currently selected DOM element 48 | - `/accessibility-audit` - Run accessibility audit on current page 49 | - `/performance-audit` - Run performance audit on current page 50 | - `/seo-audit` - Run SEO audit on current page 51 | 52 | ## API Documentation 53 | 54 | ### GET Endpoints 55 | 56 | - `GET /console-logs` - Returns recent console logs 57 | - `GET /console-errors` - Returns recent console errors 58 | - `GET /network-errors` - Returns recent network errors 59 | - `GET /network-success` - Returns recent successful network requests 60 | - `GET /all-xhr` - Returns all recent network requests 61 | - `GET /selected-element` - Returns the currently selected DOM element 62 | 63 | ### POST Endpoints 64 | 65 | - `POST /extension-log` - Receive logs from the extension 66 | - `POST /screenshot` - Capture and save screenshots 67 | - `POST /selected-element` - Update the selected element 68 | - `POST /wipelogs` - Clear all stored logs 69 | - `POST /accessibility-audit` - Run a WCAG-compliant accessibility audit on the current page 70 | - `POST /performance-audit` - Run a performance audit on the current page 71 | - `POST /seo-audit` - Run a SEO audit on the current page 72 | 73 | # Audit Functionality 74 | 75 | The server provides Lighthouse-powered audit capabilities through four AI-optimized endpoints. These audits have been specifically tailored for AI consumption, with structured data, clear categorization, and smart prioritization. 76 | 77 | ## Smart Limit Implementation 78 | 79 | All audit tools implement a "smart limit" approach to provide the most relevant information based on impact severity: 80 | 81 | - **Critical issues**: No limit (all issues are shown) 82 | - **Serious issues**: Up to 15 items per issue 83 | - **Moderate issues**: Up to 10 items per issue 84 | - **Minor issues**: Up to 3 items per issue 85 | 86 | This ensures that the most important issues are always included in the response, while less important ones are limited to maintain a manageable response size for AI processing. 87 | 88 | ## Common Audit Response Structure 89 | 90 | All audit responses follow a similar structure: 91 | 92 | ```json 93 | { 94 | "metadata": { 95 | "url": "https://example.com", 96 | "timestamp": "2025-03-06T16:28:30.930Z", 97 | "device": "desktop", 98 | "lighthouseVersion": "11.7.1" 99 | }, 100 | "report": { 101 | "score": 88, 102 | "audit_counts": { 103 | "failed": 2, 104 | "passed": 17, 105 | "manual": 10, 106 | "informative": 0, 107 | "not_applicable": 42 108 | } 109 | // Audit-specific content 110 | // ... 111 | } 112 | } 113 | ``` 114 | 115 | ## Accessibility Audit (`/accessibility-audit`) 116 | 117 | The accessibility audit evaluates web pages against WCAG standards, identifying issues that affect users with disabilities. 118 | 119 | ### Response Format 120 | 121 | ```json 122 | { 123 | "metadata": { 124 | "url": "https://example.com", 125 | "timestamp": "2025-03-06T16:28:30.930Z", 126 | "device": "desktop", 127 | "lighthouseVersion": "11.7.1" 128 | }, 129 | "report": { 130 | "score": 88, 131 | "audit_counts": { 132 | "failed": 2, 133 | "passed": 17, 134 | "manual": 10, 135 | "informative": 0, 136 | "not_applicable": 42 137 | }, 138 | "issues": [ 139 | { 140 | "id": "meta-viewport", 141 | "title": "`[user-scalable=\"no\"]` is used in the `<meta name=\"viewport\">` element or the `[maximum-scale]` attribute is less than 5.", 142 | "impact": "critical", 143 | "category": "a11y-best-practices", 144 | "elements": [ 145 | { 146 | "selector": "head > meta", 147 | "snippet": "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0\">", 148 | "label": "head > meta", 149 | "issue_description": "Fix any of the following: user-scalable on <meta> tag disables zooming on mobile devices" 150 | } 151 | ], 152 | "score": 0 153 | } 154 | ], 155 | "categories": { 156 | "a11y-navigation": { "score": 0, "issues_count": 0 }, 157 | "a11y-aria": { "score": 0, "issues_count": 1 }, 158 | "a11y-best-practices": { "score": 0, "issues_count": 1 } 159 | }, 160 | "critical_elements": [ 161 | { 162 | "selector": "head > meta", 163 | "snippet": "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0\">", 164 | "label": "head > meta", 165 | "issue_description": "Fix any of the following: user-scalable on <meta> tag disables zooming on mobile devices" 166 | } 167 | ], 168 | "prioritized_recommendations": [ 169 | "Fix ARIA attributes and roles", 170 | "Fix 1 issues in a11y-best-practices" 171 | ] 172 | } 173 | } 174 | ``` 175 | 176 | ### Key Features 177 | 178 | - **Issues Categorized by Impact**: Critical, serious, moderate, and minor 179 | - **Element-Specific Information**: Selectors, snippets, and labels for affected elements 180 | - **Issue Categories**: ARIA, navigation, color contrast, forms, keyboard access, etc. 181 | - **Critical Elements List**: Quick access to the most serious issues 182 | - **Prioritized Recommendations**: Actionable advice in order of importance 183 | 184 | ## Performance Audit (`/performance-audit`) 185 | 186 | The performance audit analyzes page load speed, Core Web Vitals, and optimization opportunities. 187 | 188 | ### Response Format 189 | 190 | ```json 191 | { 192 | "metadata": { 193 | "url": "https://example.com", 194 | "timestamp": "2025-03-06T16:27:44.900Z", 195 | "device": "desktop", 196 | "lighthouseVersion": "11.7.1" 197 | }, 198 | "report": { 199 | "score": 60, 200 | "audit_counts": { 201 | "failed": 11, 202 | "passed": 21, 203 | "manual": 0, 204 | "informative": 20, 205 | "not_applicable": 8 206 | }, 207 | "metrics": [ 208 | { 209 | "id": "lcp", 210 | "score": 0, 211 | "value_ms": 14149, 212 | "passes_core_web_vital": false, 213 | "element_selector": "div.heading > span", 214 | "element_type": "text", 215 | "element_content": "Welcome to Example" 216 | }, 217 | { 218 | "id": "fcp", 219 | "score": 0.53, 220 | "value_ms": 1542, 221 | "passes_core_web_vital": false 222 | }, 223 | { 224 | "id": "si", 225 | "score": 0, 226 | "value_ms": 6883 227 | }, 228 | { 229 | "id": "tti", 230 | "score": 0, 231 | "value_ms": 14746 232 | }, 233 | { 234 | "id": "cls", 235 | "score": 1, 236 | "value_ms": 0.001, 237 | "passes_core_web_vital": true 238 | }, 239 | { 240 | "id": "tbt", 241 | "score": 1, 242 | "value_ms": 43, 243 | "passes_core_web_vital": true 244 | } 245 | ], 246 | "opportunities": [ 247 | { 248 | "id": "render_blocking_resources", 249 | "savings_ms": 1270, 250 | "severity": "serious", 251 | "resources": [ 252 | { 253 | "url": "styles.css", 254 | "savings_ms": 781 255 | } 256 | ] 257 | } 258 | ], 259 | "page_stats": { 260 | "total_size_kb": 2190, 261 | "total_requests": 108, 262 | "resource_counts": { 263 | "js": 86, 264 | "css": 1, 265 | "img": 3, 266 | "font": 3, 267 | "other": 15 268 | }, 269 | "third_party_size_kb": 2110, 270 | "main_thread_blocking_time_ms": 693 271 | }, 272 | "prioritized_recommendations": ["Improve Largest Contentful Paint (LCP)"] 273 | } 274 | } 275 | ``` 276 | 277 | ### Key Features 278 | 279 | - **Core Web Vitals Analysis**: LCP, FCP, CLS, TBT with pass/fail status 280 | - **Element Information for LCP**: Identifies what's causing the largest contentful paint 281 | - **Optimization Opportunities**: Specific actions to improve performance with estimated time savings 282 | - **Resource Breakdown**: By type, size, and origin (first vs. third party) 283 | - **Main Thread Analysis**: Blocking time metrics to identify JavaScript performance issues 284 | - **Resource-Specific Recommendations**: For each optimization opportunity 285 | 286 | ## SEO Audit (`/seo-audit`) 287 | 288 | The SEO audit checks search engine optimization best practices and identifies issues that could affect search ranking. 289 | 290 | ### Response Format 291 | 292 | ```json 293 | { 294 | "metadata": { 295 | "url": "https://example.com", 296 | "timestamp": "2025-03-06T16:29:12.455Z", 297 | "device": "desktop", 298 | "lighthouseVersion": "11.7.1" 299 | }, 300 | "report": { 301 | "score": 91, 302 | "audit_counts": { 303 | "failed": 1, 304 | "passed": 10, 305 | "manual": 1, 306 | "informative": 0, 307 | "not_applicable": 3 308 | }, 309 | "issues": [ 310 | { 311 | "id": "is-crawlable", 312 | "title": "Page is blocked from indexing", 313 | "impact": "critical", 314 | "category": "crawlability", 315 | "score": 0 316 | } 317 | ], 318 | "categories": { 319 | "content": { "score": 0, "issues_count": 0 }, 320 | "mobile": { "score": 0, "issues_count": 0 }, 321 | "crawlability": { "score": 0, "issues_count": 1 }, 322 | "other": { "score": 0, "issues_count": 0 } 323 | }, 324 | "prioritized_recommendations": [ 325 | "Fix crawlability issues (1 issues): robots.txt, sitemaps, and redirects" 326 | ] 327 | } 328 | } 329 | ``` 330 | 331 | ### Key Features 332 | 333 | - **Issues Categorized by Impact**: Critical, serious, moderate, and minor 334 | - **SEO Categories**: Content, mobile friendliness, crawlability 335 | - **Issue Details**: Information about what's causing each SEO problem 336 | - **Prioritized Recommendations**: Actionable advice in order of importance 337 | 338 | ## Best Practices Audit (`/best-practices-audit`) 339 | 340 | The best practices audit evaluates adherence to web development best practices related to security, trust, user experience, and browser compatibility. 341 | 342 | ### Response Format 343 | 344 | ```json 345 | { 346 | "metadata": { 347 | "url": "https://example.com", 348 | "timestamp": "2025-03-06T17:01:38.029Z", 349 | "device": "desktop", 350 | "lighthouseVersion": "11.7.1" 351 | }, 352 | "report": { 353 | "score": 74, 354 | "audit_counts": { 355 | "failed": 4, 356 | "passed": 10, 357 | "manual": 0, 358 | "informative": 2, 359 | "not_applicable": 1 360 | }, 361 | "issues": [ 362 | { 363 | "id": "deprecations", 364 | "title": "Uses deprecated APIs", 365 | "impact": "critical", 366 | "category": "security", 367 | "score": 0, 368 | "details": [ 369 | { 370 | "value": "UnloadHandler" 371 | } 372 | ] 373 | }, 374 | { 375 | "id": "errors-in-console", 376 | "title": "Browser errors were logged to the console", 377 | "impact": "serious", 378 | "category": "user-experience", 379 | "score": 0, 380 | "details": [ 381 | { 382 | "source": "console.error", 383 | "description": "ReferenceError: variable is not defined" 384 | } 385 | ] 386 | } 387 | ], 388 | "categories": { 389 | "security": { "score": 75, "issues_count": 1 }, 390 | "trust": { "score": 100, "issues_count": 0 }, 391 | "user-experience": { "score": 50, "issues_count": 1 }, 392 | "browser-compat": { "score": 100, "issues_count": 0 }, 393 | "other": { "score": 75, "issues_count": 2 } 394 | }, 395 | "prioritized_recommendations": [ 396 | "Address 1 security issues: vulnerabilities, CSP, deprecations", 397 | "Improve 1 user experience issues: console errors, user interactions" 398 | ] 399 | } 400 | } 401 | ``` 402 | 403 | ### Key Features 404 | 405 | - **Issues Categorized by Impact**: Critical, serious, moderate, and minor 406 | - **Best Practice Categories**: Security, trust, user experience, browser compatibility 407 | - **Detailed Issue Information**: Specific problems affecting best practices compliance 408 | - **Security Focus**: Special attention to security vulnerabilities and deprecated APIs 409 | - **Prioritized Recommendations**: Actionable advice in order of importance 410 | 411 | ## License 412 | 413 | MIT 414 | 415 | # Puppeteer Service 416 | 417 | A comprehensive browser automation service built on Puppeteer to provide reliable cross-platform browser control capabilities. 418 | 419 | ## Features 420 | 421 | - **Cross-Platform Browser Support**: 422 | 423 | - Windows, macOS, and Linux support 424 | - Chrome, Edge, Brave, and Firefox detection 425 | - Fallback strategy for finding browser executables 426 | 427 | - **Smart Browser Management**: 428 | 429 | - Singleton browser instance with automatic cleanup 430 | - Connection retry mechanisms 431 | - Temporary user data directories with cleanup 432 | 433 | - **Rich Configuration Options**: 434 | - Custom browser paths 435 | - Network condition emulation 436 | - Device emulation (mobile, tablet, desktop) 437 | - Resource blocking 438 | - Cookies and headers customization 439 | - Locale and timezone emulation 440 | ``` -------------------------------------------------------------------------------- /chrome-extension/devtools.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <meta charset="utf-8" /> 5 | <title>BrowserTools MCP</title> 6 | </head> 7 | <body> 8 | <!-- DevTools extension script --> 9 | <script src="devtools.js"></script> 10 | </body> 11 | </html> 12 | ``` -------------------------------------------------------------------------------- /browser-tools-mcp/tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "outDir": "./dist", 8 | "rootDir": ".", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["*.ts"], 14 | "exclude": ["node_modules", "dist"] 15 | } ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "dependencies": { 3 | "@agentdeskai/browser-tools-mcp": "^1.2.0" 4 | }, 5 | "name": "cursor-mcp", 6 | "version": "1.0.0", 7 | "main": "index.js", 8 | "devDependencies": {}, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "node index.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "type": "commonjs", 17 | "description": "" 18 | } 19 | ``` -------------------------------------------------------------------------------- /browser-tools-server/tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "outDir": "./dist", 8 | "rootDir": ".", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["**/*.ts"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | ``` -------------------------------------------------------------------------------- /chrome-extension/manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "BrowserTools MCP", 3 | "version": "1.2.0", 4 | "description": "MCP tool for AI code editors to capture data from a browser such as console logs, network requests, screenshots and more", 5 | "manifest_version": 3, 6 | "devtools_page": "devtools.html", 7 | "permissions": [ 8 | "activeTab", 9 | "debugger", 10 | "storage", 11 | "tabs", 12 | "tabCapture", 13 | "windows" 14 | ], 15 | "host_permissions": [ 16 | "<all_urls>" 17 | ], 18 | "background": { 19 | "service_worker": "background.js" 20 | } 21 | } 22 | ``` -------------------------------------------------------------------------------- /start-servers.bat: -------------------------------------------------------------------------------- ``` 1 | @echo off 2 | echo Starting Browser Tools Server... 3 | start cmd /k "cd %~dp0browser-tools-server && node dist/browser-connector.js" 4 | 5 | echo. 6 | echo Waiting for Browser Tools Server to initialize... 7 | timeout /t 5 /nobreak > nul 8 | 9 | echo. 10 | echo Starting Browser Tools MCP Server... 11 | start cmd /k "cd %~dp0browser-tools-mcp && node dist/mcp-server.js" 12 | 13 | echo. 14 | echo Both servers started! You can now use the Browser Tools MCP in Cursor. 15 | echo. 16 | echo 1. Configure Chrome extension 17 | echo 2. In Cursor, press Cmd+Shift+P (or Ctrl+Shift+P on Windows) 18 | echo 3. Choose "Cursor Settings" and scroll to MCP section 19 | echo 4. Add a new MCP server with: 20 | echo Name: Browser Tools MCP 21 | echo Command: node C:/Users/sugat/Documents/Cursor MCP/browser-tools-mcp/dist/mcp-server.js 22 | echo. 23 | echo Press any key to close this window... 24 | pause > nul ``` -------------------------------------------------------------------------------- /start-servers.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | echo "Starting Browser Tools Server..." 4 | cd "$(dirname "$0")/browser-tools-server" && node dist/browser-connector.js & 5 | SERVER_PID=$! 6 | 7 | echo "" 8 | echo "Waiting for Browser Tools Server to initialize..." 9 | sleep 5 10 | 11 | echo "" 12 | echo "Starting Browser Tools MCP Server..." 13 | cd "$(dirname "$0")/browser-tools-mcp" && node dist/mcp-server.js & 14 | MCP_PID=$! 15 | 16 | echo "" 17 | echo "Both servers started! You can now use the Browser Tools MCP in Cursor." 18 | echo "" 19 | echo "1. Configure Chrome extension" 20 | echo "2. In Cursor, press Cmd+Shift+P (or Ctrl+Shift+P on Windows)" 21 | echo "3. Choose 'Cursor Settings' and scroll to MCP section" 22 | echo "4. Add a new MCP server with:" 23 | echo " Name: Browser Tools MCP" 24 | echo " Command: node $(pwd)/browser-tools-mcp/dist/mcp-server.js" 25 | echo "" 26 | echo "Press Ctrl+C to stop both servers." 27 | 28 | # Handle termination signal 29 | trap "kill $SERVER_PID $MCP_PID; exit" INT TERM 30 | 31 | # Wait for processes 32 | wait ``` -------------------------------------------------------------------------------- /browser-tools-server/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@agentdeskai/browser-tools-server", 3 | "version": "1.2.0", 4 | "description": "A browser tools server for capturing and managing browser events, logs, and screenshots", 5 | "type": "module", 6 | "main": "dist/browser-connector.js", 7 | "bin": { 8 | "browser-tools-server": "./dist/browser-connector.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc", 12 | "start": "tsc && node dist/browser-connector.js", 13 | "prepublishOnly": "npm run build" 14 | }, 15 | "keywords": [ 16 | "browser", 17 | "tools", 18 | "debugging", 19 | "logging", 20 | "screenshots", 21 | "chrome", 22 | "extension" 23 | ], 24 | "author": "AgentDesk AI", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@modelcontextprotocol/sdk": "^1.4.1", 28 | "body-parser": "^1.20.3", 29 | "cors": "^2.8.5", 30 | "express": "^4.21.2", 31 | "lighthouse": "^11.6.0", 32 | "llm-cost": "^1.0.5", 33 | "node-fetch": "^2.7.0", 34 | "puppeteer-core": "^22.4.1", 35 | "ws": "^8.18.0" 36 | }, 37 | "optionalDependencies": { 38 | "chrome-launcher": "^1.1.2" 39 | }, 40 | "devDependencies": { 41 | "@types/ws": "^8.5.14", 42 | "@types/body-parser": "^1.19.5", 43 | "@types/cors": "^2.8.17", 44 | "@types/express": "^5.0.0", 45 | "@types/node": "^22.13.1", 46 | "@types/node-fetch": "^2.6.11", 47 | "@types/puppeteer-core": "^7.0.4", 48 | "typescript": "^5.7.3" 49 | } 50 | } 51 | ``` -------------------------------------------------------------------------------- /browser-tools-mcp/package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@agentdeskai/browser-tools-mcp", 3 | "version": "1.2.0", 4 | "description": "MCP (Model Context Protocol) server for browser tools integration", 5 | "main": "dist/mcp-server.js", 6 | "bin": { 7 | "browser-tools-mcp": "dist/mcp-server.js" 8 | }, 9 | "scripts": { 10 | "inspect": "tsc && npx @modelcontextprotocol/inspector node -- dist/mcp-server.js", 11 | "inspect-live": "npx @modelcontextprotocol/inspector npx -- @agentdeskai/browser-tools-mcp", 12 | "build": "tsc", 13 | "start": "tsc && node dist/mcp-server.js", 14 | "prepublishOnly": "npm run build", 15 | "update": "npm run build && npm version patch && npm publish" 16 | }, 17 | "keywords": [ 18 | "mcp", 19 | "model-context-protocol", 20 | "browser", 21 | "tools", 22 | "debugging", 23 | "ai", 24 | "chrome", 25 | "extension" 26 | ], 27 | "author": "AgentDesk AI", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@modelcontextprotocol/sdk": "^1.4.1", 31 | "body-parser": "^1.20.3", 32 | "cors": "^2.8.5", 33 | "express": "^4.21.2", 34 | "llm-cost": "^1.0.5", 35 | "node-fetch": "^2.7.0", 36 | "ws": "^8.18.0" 37 | }, 38 | "devDependencies": { 39 | "@types/ws": "^8.5.14", 40 | "@types/body-parser": "^1.19.5", 41 | "@types/cors": "^2.8.17", 42 | "@types/express": "^5.0.0", 43 | "@types/node": "^22.13.1", 44 | "@types/node-fetch": "^2.6.11", 45 | "typescript": "^5.7.3" 46 | } 47 | } 48 | ``` -------------------------------------------------------------------------------- /browser-tools-server/lighthouse/types.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Audit categories available in Lighthouse 3 | */ 4 | export enum AuditCategory { 5 | ACCESSIBILITY = "accessibility", 6 | PERFORMANCE = "performance", 7 | SEO = "seo", 8 | BEST_PRACTICES = "best-practices", // Not yet implemented 9 | PWA = "pwa", // Not yet implemented 10 | } 11 | 12 | /** 13 | * Base interface for Lighthouse report metadata 14 | */ 15 | export interface LighthouseReport<T = any> { 16 | metadata: { 17 | url: string; 18 | timestamp: string; // ISO 8601, e.g., "2025-02-27T14:30:00Z" 19 | device: string; // e.g., "mobile", "desktop" 20 | lighthouseVersion: string; // e.g., "10.4.0" 21 | }; 22 | 23 | // For backward compatibility with existing report formats 24 | overallScore?: number; 25 | failedAuditsCount?: number; 26 | passedAuditsCount?: number; 27 | manualAuditsCount?: number; 28 | informativeAuditsCount?: number; 29 | notApplicableAuditsCount?: number; 30 | failedAudits?: any[]; 31 | 32 | // New format for specialized reports 33 | report?: T; // Generic report data that will be specialized by each audit type 34 | } 35 | 36 | /** 37 | * Configuration options for Lighthouse audits 38 | */ 39 | export interface LighthouseConfig { 40 | flags: { 41 | output: string[]; 42 | onlyCategories: string[]; 43 | formFactor: string; 44 | port: number | undefined; 45 | screenEmulation: { 46 | mobile: boolean; 47 | width: number; 48 | height: number; 49 | deviceScaleFactor: number; 50 | disabled: boolean; 51 | }; 52 | }; 53 | config: { 54 | extends: string; 55 | settings: { 56 | onlyCategories: string[]; 57 | emulatedFormFactor: string; 58 | throttling: { 59 | cpuSlowdownMultiplier: number; 60 | }; 61 | }; 62 | }; 63 | } 64 | ``` -------------------------------------------------------------------------------- /SETUP_INSTRUCTIONS.md: -------------------------------------------------------------------------------- ```markdown 1 | # Browser Tools MCP Setup Instructions 2 | 3 | This guide will help you set up Browser Tools MCP to use with Cursor, allowing you to debug web applications directly through your AI assistant. 4 | 5 | ## Step 1: Chrome Extension Setup 6 | 7 | 1. Open Chrome and go to `chrome://extensions/` 8 | 2. Enable "Developer mode" in the top-right corner 9 | 3. Click "Load unpacked" 10 | 4. Navigate to the `chrome-extension` folder in this repository and select it 11 | 5. The Browser Tools MCP extension should now appear in your extensions list 12 | 6. Click on the extension icon in your browser toolbar to verify it's connected 13 | 14 | ## Step 2: Starting the Servers 15 | 16 | ### Option 1: Using the batch file (Recommended for Windows) 17 | Simply double-click the `start-servers.bat` file in this repository. This will start both servers in separate command windows. 18 | 19 | ### Option 2: Manual startup 20 | 1. Start the Browser Tools Server: 21 | ``` 22 | cd browser-tools-server 23 | node dist/browser-connector.js 24 | ``` 25 | 26 | 2. In a separate terminal, start the MCP Server: 27 | ``` 28 | cd browser-tools-mcp 29 | node dist/mcp-server.js 30 | ``` 31 | 32 | ## Step 3: Configure Cursor 33 | 34 | 1. Open Cursor 35 | 2. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) 36 | 3. Select "Cursor Settings" 37 | 4. Scroll down to the MCP section 38 | 5. Click "Add a New MCP Server" 39 | 6. Enter the following details: 40 | - Name: `Browser Tools MCP` 41 | - Command: `node C:/Users/sugat/Documents/Cursor MCP/browser-tools-mcp/dist/mcp-server.js` 42 | 7. Click "Add" 43 | 8. Wait for the "Connected" status to appear 44 | 45 | ## Step 4: Using Browser Tools MCP 46 | 47 | You can now use the following commands in Cursor's AI assistant: 48 | 49 | - `gather console logs` - Get all console logs from the browser 50 | - `get network errors` - Get network error logs 51 | - `take a screenshot` - Capture the current browser view 52 | - `debug` - Run a complete debug sequence (if you've set up the Cursor rule) 53 | 54 | ### Setting up a Debug Command (Optional) 55 | 56 | 1. Create a folder named `cursor` in your project 57 | 2. Create a subfolder named `rules` 58 | 3. Create a file named `debugcommands.mdc` with the following content: 59 | ``` 60 | description: This tool performs debugging steps for applications 61 | match: "**/*.ts" 62 | --- 63 | When I say debug, gather console logs and take a screenshot of the app to help fix issues 64 | ``` 65 | 66 | Now you can simply type `/debug` in Cursor to run all debugging steps automatically. 67 | 68 | ## Troubleshooting 69 | 70 | - If the extension isn't connecting, make sure both servers are running 71 | - Check Chrome's console for any extension errors 72 | - Ensure you've entered the correct path in Cursor's MCP settings 73 | - If using WSL, run everything outside of Ubuntu, using Windows paths ``` -------------------------------------------------------------------------------- /browser-tools-server/lighthouse/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import lighthouse from "lighthouse"; 2 | import type { Result as LighthouseResult, Flags } from "lighthouse"; 3 | import { 4 | connectToHeadlessBrowser, 5 | scheduleBrowserCleanup, 6 | } from "../puppeteer-service.js"; 7 | import { LighthouseConfig, AuditCategory } from "./types.js"; 8 | 9 | /** 10 | * Creates a Lighthouse configuration object 11 | * @param categories Array of categories to audit 12 | * @returns Lighthouse configuration and flags 13 | */ 14 | export function createLighthouseConfig( 15 | categories: string[] = [AuditCategory.ACCESSIBILITY] 16 | ): LighthouseConfig { 17 | return { 18 | flags: { 19 | output: ["json"], 20 | onlyCategories: categories, 21 | formFactor: "desktop", 22 | port: undefined as number | undefined, 23 | screenEmulation: { 24 | mobile: false, 25 | width: 1350, 26 | height: 940, 27 | deviceScaleFactor: 1, 28 | disabled: false, 29 | }, 30 | }, 31 | config: { 32 | extends: "lighthouse:default", 33 | settings: { 34 | onlyCategories: categories, 35 | emulatedFormFactor: "desktop", 36 | throttling: { cpuSlowdownMultiplier: 1 }, 37 | }, 38 | }, 39 | }; 40 | } 41 | 42 | /** 43 | * Runs a Lighthouse audit on the specified URL via CDP 44 | * @param url The URL to audit 45 | * @param categories Array of categories to audit, defaults to ["accessibility"] 46 | * @returns Promise resolving to the Lighthouse result 47 | * @throws Error if the URL is invalid or if the audit fails 48 | */ 49 | export async function runLighthouseAudit( 50 | url: string, 51 | categories: string[] 52 | ): Promise<LighthouseResult> { 53 | console.log(`Starting Lighthouse ${categories.join(", ")} audit for: ${url}`); 54 | 55 | if (!url || url === "about:blank") { 56 | console.error("Invalid URL for Lighthouse audit"); 57 | throw new Error( 58 | "Cannot run audit on an empty page or about:blank. Please navigate to a valid URL first." 59 | ); 60 | } 61 | 62 | try { 63 | // Always use a dedicated headless browser for audits 64 | console.log("Using dedicated headless browser for audit"); 65 | 66 | // Determine if this is a performance audit - we need to load all resources for performance audits 67 | const isPerformanceAudit = categories.includes(AuditCategory.PERFORMANCE); 68 | 69 | // For performance audits, we want to load all resources 70 | // For accessibility or other audits, we can block non-essential resources 71 | try { 72 | const { port } = await connectToHeadlessBrowser(url, { 73 | blockResources: !isPerformanceAudit, 74 | }); 75 | 76 | console.log(`Connected to browser on port: ${port}`); 77 | 78 | // Create Lighthouse config 79 | const { flags, config } = createLighthouseConfig(categories); 80 | flags.port = port; 81 | 82 | console.log( 83 | `Running Lighthouse with categories: ${categories.join(", ")}` 84 | ); 85 | const runnerResult = await lighthouse(url, flags as Flags, config); 86 | console.log("Lighthouse scan completed"); 87 | 88 | if (!runnerResult?.lhr) { 89 | console.error("Lighthouse audit failed to produce results"); 90 | throw new Error("Lighthouse audit failed to produce results"); 91 | } 92 | 93 | // Schedule browser cleanup after a delay to allow for subsequent audits 94 | scheduleBrowserCleanup(); 95 | 96 | // Return the result 97 | const result = runnerResult.lhr; 98 | 99 | return result; 100 | } catch (browserError) { 101 | // Check if the error is related to Chrome/Edge not being available 102 | const errorMessage = 103 | browserError instanceof Error 104 | ? browserError.message 105 | : String(browserError); 106 | if ( 107 | errorMessage.includes("Chrome could not be found") || 108 | errorMessage.includes("Failed to launch browser") || 109 | errorMessage.includes("spawn ENOENT") 110 | ) { 111 | throw new Error( 112 | "Chrome or Edge browser could not be found. Please ensure that Chrome or Edge is installed on your system to run audits." 113 | ); 114 | } 115 | // Re-throw other errors 116 | throw browserError; 117 | } 118 | } catch (error) { 119 | console.error("Lighthouse audit failed:", error); 120 | // Schedule browser cleanup even if the audit fails 121 | scheduleBrowserCleanup(); 122 | throw new Error( 123 | `Lighthouse audit failed: ${ 124 | error instanceof Error ? error.message : String(error) 125 | }` 126 | ); 127 | } 128 | } 129 | 130 | // Export from specific audit modules 131 | export * from "./accessibility.js"; 132 | export * from "./performance.js"; 133 | export * from "./seo.js"; 134 | export * from "./types.js"; 135 | ``` -------------------------------------------------------------------------------- /chrome-extension/panel.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html> 3 | <head> 4 | <style> 5 | body { 6 | padding: 16px; 7 | font-family: system-ui, -apple-system, sans-serif; 8 | background-color: #282828; 9 | color: #fff; 10 | } 11 | .endpoint-list { 12 | margin: 16px 0; 13 | } 14 | .endpoint-item { 15 | display: flex; 16 | gap: 8px; 17 | margin-bottom: 8px; 18 | align-items: center; 19 | } 20 | .endpoint-form { 21 | display: flex; 22 | gap: 8px; 23 | margin-bottom: 16px; 24 | align-items: center; 25 | } 26 | button { 27 | padding: 4px 8px; 28 | } 29 | input { 30 | padding: 4px; 31 | } 32 | .status-indicator { 33 | width: 8px; 34 | height: 8px; 35 | border-radius: 50%; 36 | display: inline-block; 37 | } 38 | .status-connected { 39 | background: #4caf50; 40 | } 41 | .status-disconnected { 42 | background: #f44336; 43 | } 44 | .form-group { 45 | margin-bottom: 16px; 46 | } 47 | .form-group label { 48 | display: block; 49 | margin-bottom: 4px; 50 | } 51 | .checkbox-group { 52 | margin-bottom: 8px; 53 | } 54 | .checkbox-group-2 { 55 | margin-bottom: 6px; 56 | } 57 | input[type="number"], 58 | input[type="text"] { 59 | padding: 4px; 60 | width: 200px; 61 | } 62 | .settings-section { 63 | border: 1px solid #ccc; 64 | padding: 16px; 65 | margin-bottom: 16px; 66 | border-radius: 4px; 67 | } 68 | .settings-header { 69 | display: flex; 70 | align-items: center; 71 | justify-content: space-between; 72 | cursor: pointer; 73 | user-select: none; 74 | } 75 | .settings-header h3 { 76 | margin: 0; 77 | } 78 | .settings-content { 79 | display: none; 80 | margin-top: 16px; 81 | } 82 | .settings-content.visible { 83 | display: block; 84 | } 85 | .chevron { 86 | width: 20px; 87 | height: 20px; 88 | transition: transform 0.3s ease; 89 | } 90 | .chevron.open { 91 | transform: rotate(180deg); 92 | } 93 | .quick-actions { 94 | display: flex; 95 | gap: 8px; 96 | margin-bottom: 16px; 97 | } 98 | .action-button { 99 | background-color: #4a4a4a; 100 | color: white; 101 | border: none; 102 | padding: 8px 16px; 103 | border-radius: 4px; 104 | cursor: pointer; 105 | transition: background-color 0.2s; 106 | } 107 | .action-button:hover { 108 | background-color: #5a5a5a; 109 | } 110 | .action-button.danger { 111 | background-color: #f44336; 112 | } 113 | .action-button.danger:hover { 114 | background-color: #d32f2f; 115 | } 116 | </style> 117 | </head> 118 | <body> 119 | <div class="settings-section"> 120 | <h3>Quick Actions</h3> 121 | <div class="quick-actions"> 122 | <button id="capture-screenshot" class="action-button"> 123 | Capture Screenshot 124 | </button> 125 | <button id="wipe-logs" class="action-button danger"> 126 | Wipe All Logs 127 | </button> 128 | </div> 129 | <div class="checkbox-group-2" style="margin-top: 10px; display: flex; align-items: center;"> 130 | <label> 131 | <input type="checkbox" id="allow-auto-paste"> 132 | Allow Auto-paste to Cursor 133 | </label> 134 | </div> 135 | </div> 136 | 137 | <div class="settings-section"> 138 | <h3>Screenshot Settings</h3> 139 | <div class="form-group"> 140 | <label for="screenshot-path">Provide a directory to save screenshots to (by default screenshots will be saved to your downloads folder if no path is provided)</label> 141 | <input type="text" id="screenshot-path" placeholder="/path/to/screenshots"> 142 | </div> 143 | </div> 144 | 145 | <div class="settings-section"> 146 | <h3>Server Connection Settings</h3> 147 | <div class="form-group"> 148 | <label for="server-host">Server Host</label> 149 | <input type="text" id="server-host" placeholder="localhost or IP address"> 150 | </div> 151 | <div class="form-group"> 152 | <label for="server-port">Server Port</label> 153 | <input type="number" id="server-port" min="1" max="65535" value="3025"> 154 | </div> 155 | <div class="quick-actions"> 156 | <button id="discover-server" class="action-button"> 157 | Auto-Discover Server 158 | </button> 159 | <button id="test-connection" class="action-button"> 160 | Test Connection 161 | </button> 162 | </div> 163 | <div id="connection-status" style="margin-top: 8px; display: none;"> 164 | <span id="status-icon" class="status-indicator"></span> 165 | <span id="status-text"></span> 166 | </div> 167 | </div> 168 | 169 | <div class="settings-section"> 170 | <div class="settings-header" id="advanced-settings-header"> 171 | <h3>Advanced Settings</h3> 172 | <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 173 | <polyline points="6 9 12 15 18 9"></polyline> 174 | </svg> 175 | </div> 176 | 177 | <div class="settings-content" id="advanced-settings-content"> 178 | <div class="form-group"> 179 | <label for="log-limit">Log Limit (number of logs)</label> 180 | <input type="number" id="log-limit" min="1" value="50"> 181 | </div> 182 | 183 | <div class="form-group"> 184 | <label for="query-limit">Query Limit (characters)</label> 185 | <input type="number" id="query-limit" min="1" value="30000"> 186 | </div> 187 | 188 | <div class="form-group"> 189 | <label for="string-size-limit">String Size Limit (characters)</label> 190 | <input type="number" id="string-size-limit" min="1" value="500"> 191 | </div> 192 | 193 | <div class="form-group"> 194 | <label for="max-log-size">Max Log Size (characters)</label> 195 | <input type="number" id="max-log-size" min="1000" value="20000"> 196 | </div> 197 | 198 | <div class="checkbox-group"> 199 | <label> 200 | <input type="checkbox" id="show-request-headers"> 201 | Include Request Headers 202 | </label> 203 | </div> 204 | 205 | <div class="checkbox-group"> 206 | <label> 207 | <input type="checkbox" id="show-response-headers"> 208 | Include Response Headers 209 | </label> 210 | </div> 211 | </div> 212 | </div> 213 | 214 | <script src="panel.js"></script> 215 | </body> 216 | </html> ``` -------------------------------------------------------------------------------- /browser-tools-server/lighthouse/best-practices.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Result as LighthouseResult } from "lighthouse"; 2 | import { AuditCategory, LighthouseReport } from "./types.js"; 3 | import { runLighthouseAudit } from "./index.js"; 4 | 5 | // === Best Practices Report Types === 6 | 7 | /** 8 | * Best Practices-specific report content structure 9 | */ 10 | export interface BestPracticesReportContent { 11 | score: number; // Overall score (0-100) 12 | audit_counts: { 13 | // Counts of different audit types 14 | failed: number; 15 | passed: number; 16 | manual: number; 17 | informative: number; 18 | not_applicable: number; 19 | }; 20 | issues: AIBestPracticesIssue[]; 21 | categories: { 22 | [category: string]: { 23 | score: number; 24 | issues_count: number; 25 | }; 26 | }; 27 | prioritized_recommendations?: string[]; // Ordered list of recommendations 28 | } 29 | 30 | /** 31 | * Full Best Practices report implementing the base LighthouseReport interface 32 | */ 33 | export type AIOptimizedBestPracticesReport = 34 | LighthouseReport<BestPracticesReportContent>; 35 | 36 | /** 37 | * AI-optimized Best Practices issue 38 | */ 39 | interface AIBestPracticesIssue { 40 | id: string; // e.g., "js-libraries" 41 | title: string; // e.g., "Detected JavaScript libraries" 42 | impact: "critical" | "serious" | "moderate" | "minor"; 43 | category: string; // e.g., "security", "trust", "user-experience", "browser-compat" 44 | details?: { 45 | name?: string; // Name of the item (e.g., library name, vulnerability) 46 | version?: string; // Version information if applicable 47 | value?: string; // Current value or status 48 | issue?: string; // Description of the issue 49 | }[]; 50 | score: number | null; // 0-1 or null 51 | } 52 | 53 | // Original interfaces for backward compatibility 54 | interface BestPracticesAudit { 55 | id: string; 56 | title: string; 57 | description: string; 58 | score: number | null; 59 | scoreDisplayMode: string; 60 | details?: BestPracticesAuditDetails; 61 | } 62 | 63 | interface BestPracticesAuditDetails { 64 | items?: Array<Record<string, unknown>>; 65 | type?: string; // e.g., "table" 66 | } 67 | 68 | // This ensures we always include critical issues while limiting less important ones 69 | const DETAIL_LIMITS: Record<string, number> = { 70 | critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues 71 | serious: 15, // Up to 15 items for serious issues 72 | moderate: 10, // Up to 10 items for moderate issues 73 | minor: 3, // Up to 3 items for minor issues 74 | }; 75 | 76 | /** 77 | * Runs a Best Practices audit on the specified URL 78 | * @param url The URL to audit 79 | * @returns Promise resolving to AI-optimized Best Practices audit results 80 | */ 81 | export async function runBestPracticesAudit( 82 | url: string 83 | ): Promise<AIOptimizedBestPracticesReport> { 84 | try { 85 | const lhr = await runLighthouseAudit(url, [AuditCategory.BEST_PRACTICES]); 86 | return extractAIOptimizedData(lhr, url); 87 | } catch (error) { 88 | throw new Error( 89 | `Best Practices audit failed: ${ 90 | error instanceof Error ? error.message : String(error) 91 | }` 92 | ); 93 | } 94 | } 95 | 96 | /** 97 | * Extract AI-optimized Best Practices data from Lighthouse results 98 | */ 99 | const extractAIOptimizedData = ( 100 | lhr: LighthouseResult, 101 | url: string 102 | ): AIOptimizedBestPracticesReport => { 103 | const categoryData = lhr.categories[AuditCategory.BEST_PRACTICES]; 104 | const audits = lhr.audits || {}; 105 | 106 | // Add metadata 107 | const metadata = { 108 | url, 109 | timestamp: lhr.fetchTime || new Date().toISOString(), 110 | device: lhr.configSettings?.formFactor || "desktop", 111 | lighthouseVersion: lhr.lighthouseVersion || "unknown", 112 | }; 113 | 114 | // Process audit results 115 | const issues: AIBestPracticesIssue[] = []; 116 | const categories: { [key: string]: { score: number; issues_count: number } } = 117 | { 118 | security: { score: 0, issues_count: 0 }, 119 | trust: { score: 0, issues_count: 0 }, 120 | "user-experience": { score: 0, issues_count: 0 }, 121 | "browser-compat": { score: 0, issues_count: 0 }, 122 | other: { score: 0, issues_count: 0 }, 123 | }; 124 | 125 | // Counters for audit types 126 | let failedCount = 0; 127 | let passedCount = 0; 128 | let manualCount = 0; 129 | let informativeCount = 0; 130 | let notApplicableCount = 0; 131 | 132 | // Process failed audits (score < 1) 133 | const failedAudits = Object.entries(audits) 134 | .filter(([, audit]) => { 135 | const score = audit.score; 136 | return ( 137 | score !== null && 138 | score < 1 && 139 | audit.scoreDisplayMode !== "manual" && 140 | audit.scoreDisplayMode !== "notApplicable" 141 | ); 142 | }) 143 | .map(([auditId, audit]) => ({ auditId, ...audit })); 144 | 145 | // Update counters 146 | Object.values(audits).forEach((audit) => { 147 | const { score, scoreDisplayMode } = audit; 148 | 149 | if (scoreDisplayMode === "manual") { 150 | manualCount++; 151 | } else if (scoreDisplayMode === "informative") { 152 | informativeCount++; 153 | } else if (scoreDisplayMode === "notApplicable") { 154 | notApplicableCount++; 155 | } else if (score === 1) { 156 | passedCount++; 157 | } else if (score !== null && score < 1) { 158 | failedCount++; 159 | } 160 | }); 161 | 162 | // Process failed audits into AI-friendly format 163 | failedAudits.forEach((ref: any) => { 164 | // Determine impact level based on audit score and weight 165 | let impact: "critical" | "serious" | "moderate" | "minor" = "moderate"; 166 | const score = ref.score || 0; 167 | 168 | // Use a more reliable approach to determine impact 169 | if (score === 0) { 170 | impact = "critical"; 171 | } else if (score < 0.5) { 172 | impact = "serious"; 173 | } else if (score < 0.9) { 174 | impact = "moderate"; 175 | } else { 176 | impact = "minor"; 177 | } 178 | 179 | // Categorize the issue 180 | let category = "other"; 181 | 182 | // Security-related issues 183 | if ( 184 | ref.auditId.includes("csp") || 185 | ref.auditId.includes("security") || 186 | ref.auditId.includes("vulnerab") || 187 | ref.auditId.includes("password") || 188 | ref.auditId.includes("cert") || 189 | ref.auditId.includes("deprecat") 190 | ) { 191 | category = "security"; 192 | } 193 | // Trust and legitimacy issues 194 | else if ( 195 | ref.auditId.includes("doctype") || 196 | ref.auditId.includes("charset") || 197 | ref.auditId.includes("legit") || 198 | ref.auditId.includes("trust") 199 | ) { 200 | category = "trust"; 201 | } 202 | // User experience issues 203 | else if ( 204 | ref.auditId.includes("user") || 205 | ref.auditId.includes("experience") || 206 | ref.auditId.includes("console") || 207 | ref.auditId.includes("errors") || 208 | ref.auditId.includes("paste") 209 | ) { 210 | category = "user-experience"; 211 | } 212 | // Browser compatibility issues 213 | else if ( 214 | ref.auditId.includes("compat") || 215 | ref.auditId.includes("browser") || 216 | ref.auditId.includes("vendor") || 217 | ref.auditId.includes("js-lib") 218 | ) { 219 | category = "browser-compat"; 220 | } 221 | 222 | // Count issues by category 223 | categories[category].issues_count++; 224 | 225 | // Create issue object 226 | const issue: AIBestPracticesIssue = { 227 | id: ref.auditId, 228 | title: ref.title, 229 | impact, 230 | category, 231 | score: ref.score, 232 | details: [], 233 | }; 234 | 235 | // Extract details if available 236 | const refDetails = ref.details as BestPracticesAuditDetails | undefined; 237 | if (refDetails?.items && Array.isArray(refDetails.items)) { 238 | const itemLimit = DETAIL_LIMITS[impact]; 239 | const detailItems = refDetails.items.slice(0, itemLimit); 240 | 241 | detailItems.forEach((item: Record<string, unknown>) => { 242 | issue.details = issue.details || []; 243 | 244 | // Different audits have different detail structures 245 | const detail: Record<string, string> = {}; 246 | 247 | if (typeof item.name === "string") detail.name = item.name; 248 | if (typeof item.version === "string") detail.version = item.version; 249 | if (typeof item.issue === "string") detail.issue = item.issue; 250 | if (item.value !== undefined) detail.value = String(item.value); 251 | 252 | // For JS libraries, extract name and version 253 | if ( 254 | ref.auditId === "js-libraries" && 255 | typeof item.name === "string" && 256 | typeof item.version === "string" 257 | ) { 258 | detail.name = item.name; 259 | detail.version = item.version; 260 | } 261 | 262 | // Add other generic properties that might exist 263 | for (const [key, value] of Object.entries(item)) { 264 | if (!detail[key] && typeof value === "string") { 265 | detail[key] = value; 266 | } 267 | } 268 | 269 | issue.details.push(detail as any); 270 | }); 271 | } 272 | 273 | issues.push(issue); 274 | }); 275 | 276 | // Calculate category scores (0-100) 277 | Object.keys(categories).forEach((category) => { 278 | // Simplified scoring: if there are issues in this category, score is reduced proportionally 279 | const issueCount = categories[category].issues_count; 280 | if (issueCount > 0) { 281 | // More issues = lower score, max penalty of 25 points per issue 282 | const penalty = Math.min(100, issueCount * 25); 283 | categories[category].score = Math.max(0, 100 - penalty); 284 | } else { 285 | categories[category].score = 100; 286 | } 287 | }); 288 | 289 | // Generate prioritized recommendations 290 | const prioritized_recommendations: string[] = []; 291 | 292 | // Prioritize recommendations by category with most issues 293 | Object.entries(categories) 294 | .filter(([_, data]) => data.issues_count > 0) 295 | .sort(([_, a], [__, b]) => b.issues_count - a.issues_count) 296 | .forEach(([category, data]) => { 297 | let recommendation = ""; 298 | 299 | switch (category) { 300 | case "security": 301 | recommendation = `Address ${data.issues_count} security issues: vulnerabilities, CSP, deprecations`; 302 | break; 303 | case "trust": 304 | recommendation = `Fix ${data.issues_count} trust & legitimacy issues: doctype, charset`; 305 | break; 306 | case "user-experience": 307 | recommendation = `Improve ${data.issues_count} user experience issues: console errors, user interactions`; 308 | break; 309 | case "browser-compat": 310 | recommendation = `Resolve ${data.issues_count} browser compatibility issues: outdated libraries, vendor prefixes`; 311 | break; 312 | default: 313 | recommendation = `Fix ${data.issues_count} other best practice issues`; 314 | } 315 | 316 | prioritized_recommendations.push(recommendation); 317 | }); 318 | 319 | // Return the optimized report 320 | return { 321 | metadata, 322 | report: { 323 | score: categoryData?.score ? Math.round(categoryData.score * 100) : 0, 324 | audit_counts: { 325 | failed: failedCount, 326 | passed: passedCount, 327 | manual: manualCount, 328 | informative: informativeCount, 329 | not_applicable: notApplicableCount, 330 | }, 331 | issues, 332 | categories, 333 | prioritized_recommendations, 334 | }, 335 | }; 336 | }; 337 | ``` -------------------------------------------------------------------------------- /browser-tools-server/lighthouse/accessibility.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Result as LighthouseResult } from "lighthouse"; 2 | import { AuditCategory, LighthouseReport } from "./types.js"; 3 | import { runLighthouseAudit } from "./index.js"; 4 | 5 | // === Accessibility Report Types === 6 | 7 | /** 8 | * Accessibility-specific report content structure 9 | */ 10 | export interface AccessibilityReportContent { 11 | score: number; // Overall score (0-100) 12 | audit_counts: { 13 | // Counts of different audit types 14 | failed: number; 15 | passed: number; 16 | manual: number; 17 | informative: number; 18 | not_applicable: number; 19 | }; 20 | issues: AIAccessibilityIssue[]; 21 | categories: { 22 | [category: string]: { 23 | score: number; 24 | issues_count: number; 25 | }; 26 | }; 27 | critical_elements: AIAccessibilityElement[]; 28 | prioritized_recommendations?: string[]; // Ordered list of recommendations 29 | } 30 | 31 | /** 32 | * Full accessibility report implementing the base LighthouseReport interface 33 | */ 34 | export type AIOptimizedAccessibilityReport = 35 | LighthouseReport<AccessibilityReportContent>; 36 | 37 | /** 38 | * AI-optimized accessibility issue 39 | */ 40 | interface AIAccessibilityIssue { 41 | id: string; // e.g., "color-contrast" 42 | title: string; // e.g., "Color contrast is sufficient" 43 | impact: "critical" | "serious" | "moderate" | "minor"; 44 | category: string; // e.g., "contrast", "aria", "forms", "keyboard" 45 | elements?: AIAccessibilityElement[]; // Elements with issues 46 | score: number | null; // 0-1 or null 47 | } 48 | 49 | /** 50 | * Accessibility element with issues 51 | */ 52 | interface AIAccessibilityElement { 53 | selector: string; // CSS selector 54 | snippet?: string; // HTML snippet 55 | label?: string; // Element label 56 | issue_description?: string; // Description of the issue 57 | value?: string | number; // Current value (e.g., contrast ratio) 58 | } 59 | 60 | // Original interfaces for backward compatibility 61 | interface AccessibilityAudit { 62 | id: string; // e.g., "color-contrast" 63 | title: string; // e.g., "Color contrast is sufficient" 64 | description: string; // e.g., "Ensures text is readable..." 65 | score: number | null; // 0-1 (normalized), null for manual/informative 66 | scoreDisplayMode: string; // e.g., "binary", "numeric", "manual" 67 | details?: AuditDetails; // Optional, structured details 68 | weight?: number; // Optional, audit weight for impact calculation 69 | } 70 | 71 | type AuditDetails = { 72 | items?: Array<{ 73 | node?: { 74 | selector: string; // e.g., ".my-class" 75 | snippet?: string; // HTML snippet 76 | nodeLabel?: string; // e.g., "Modify logging size limits / truncation" 77 | explanation?: string; // Explanation of why the node fails the audit 78 | }; 79 | value?: string | number; // Specific value (e.g., contrast ratio) 80 | explanation?: string; // Explanation at the item level 81 | }>; 82 | debugData?: string; // Optional, debug information 83 | [key: string]: any; // Flexible for other detail types (tables, etc.) 84 | }; 85 | 86 | // Original limits were optimized for human consumption 87 | // This ensures we always include critical issues while limiting less important ones 88 | const DETAIL_LIMITS = { 89 | critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues 90 | serious: 15, // Up to 15 items for serious issues 91 | moderate: 10, // Up to 10 items for moderate issues 92 | minor: 3, // Up to 3 items for minor issues 93 | }; 94 | 95 | /** 96 | * Runs an accessibility audit on the specified URL 97 | * @param url The URL to audit 98 | * @returns Promise resolving to AI-optimized accessibility audit results 99 | */ 100 | export async function runAccessibilityAudit( 101 | url: string 102 | ): Promise<AIOptimizedAccessibilityReport> { 103 | try { 104 | const lhr = await runLighthouseAudit(url, [AuditCategory.ACCESSIBILITY]); 105 | return extractAIOptimizedData(lhr, url); 106 | } catch (error) { 107 | throw new Error( 108 | `Accessibility audit failed: ${ 109 | error instanceof Error ? error.message : String(error) 110 | }` 111 | ); 112 | } 113 | } 114 | 115 | /** 116 | * Extract AI-optimized accessibility data from Lighthouse results 117 | */ 118 | const extractAIOptimizedData = ( 119 | lhr: LighthouseResult, 120 | url: string 121 | ): AIOptimizedAccessibilityReport => { 122 | const categoryData = lhr.categories[AuditCategory.ACCESSIBILITY]; 123 | const audits = lhr.audits || {}; 124 | 125 | // Add metadata 126 | const metadata = { 127 | url, 128 | timestamp: lhr.fetchTime || new Date().toISOString(), 129 | device: "desktop", // This could be made configurable 130 | lighthouseVersion: lhr.lighthouseVersion, 131 | }; 132 | 133 | // Initialize variables 134 | const issues: AIAccessibilityIssue[] = []; 135 | const criticalElements: AIAccessibilityElement[] = []; 136 | const categories: { 137 | [category: string]: { score: number; issues_count: number }; 138 | } = {}; 139 | 140 | // Count audits by type 141 | let failedCount = 0; 142 | let passedCount = 0; 143 | let manualCount = 0; 144 | let informativeCount = 0; 145 | let notApplicableCount = 0; 146 | 147 | // Process audit refs 148 | const auditRefs = categoryData?.auditRefs || []; 149 | 150 | // First pass: count audits by type and initialize categories 151 | auditRefs.forEach((ref) => { 152 | const audit = audits[ref.id]; 153 | if (!audit) return; 154 | 155 | // Count by scoreDisplayMode 156 | if (audit.scoreDisplayMode === "manual") { 157 | manualCount++; 158 | } else if (audit.scoreDisplayMode === "informative") { 159 | informativeCount++; 160 | } else if (audit.scoreDisplayMode === "notApplicable") { 161 | notApplicableCount++; 162 | } else if (audit.score !== null) { 163 | // Binary pass/fail 164 | if (audit.score >= 0.9) { 165 | passedCount++; 166 | } else { 167 | failedCount++; 168 | } 169 | } 170 | 171 | // Process categories 172 | if (ref.group) { 173 | // Initialize category if not exists 174 | if (!categories[ref.group]) { 175 | categories[ref.group] = { score: 0, issues_count: 0 }; 176 | } 177 | 178 | // Update category score and issues count 179 | if (audit.score !== null && audit.score < 0.9) { 180 | categories[ref.group].issues_count++; 181 | } 182 | } 183 | }); 184 | 185 | // Second pass: process failed audits into AI-friendly format 186 | auditRefs 187 | .filter((ref) => { 188 | const audit = audits[ref.id]; 189 | return audit && audit.score !== null && audit.score < 0.9; 190 | }) 191 | .sort((a, b) => (b.weight || 0) - (a.weight || 0)) 192 | // No limit on number of failed audits - we'll show them all 193 | .forEach((ref) => { 194 | const audit = audits[ref.id]; 195 | 196 | // Determine impact level based on score and weight 197 | let impact: "critical" | "serious" | "moderate" | "minor" = "moderate"; 198 | if (audit.score === 0) { 199 | impact = "critical"; 200 | } else if (audit.score !== null && audit.score <= 0.5) { 201 | impact = "serious"; 202 | } else if (audit.score !== null && audit.score > 0.7) { 203 | impact = "minor"; 204 | } 205 | 206 | // Create elements array 207 | const elements: AIAccessibilityElement[] = []; 208 | 209 | if (audit.details) { 210 | const details = audit.details as any; 211 | if (details.items && Array.isArray(details.items)) { 212 | const items = details.items; 213 | // Apply limits based on impact level 214 | const itemLimit = DETAIL_LIMITS[impact]; 215 | items.slice(0, itemLimit).forEach((item: any) => { 216 | if (item.node) { 217 | const element: AIAccessibilityElement = { 218 | selector: item.node.selector, 219 | snippet: item.node.snippet, 220 | label: item.node.nodeLabel, 221 | issue_description: item.node.explanation || item.explanation, 222 | }; 223 | 224 | if (item.value !== undefined) { 225 | element.value = item.value; 226 | } 227 | 228 | elements.push(element); 229 | 230 | // Add to critical elements if impact is critical or serious 231 | if (impact === "critical" || impact === "serious") { 232 | criticalElements.push(element); 233 | } 234 | } 235 | }); 236 | } 237 | } 238 | 239 | // Create the issue 240 | const issue: AIAccessibilityIssue = { 241 | id: ref.id, 242 | title: audit.title, 243 | impact, 244 | category: ref.group || "other", 245 | elements: elements.length > 0 ? elements : undefined, 246 | score: audit.score, 247 | }; 248 | 249 | issues.push(issue); 250 | }); 251 | 252 | // Calculate overall score 253 | const score = Math.round((categoryData?.score || 0) * 100); 254 | 255 | // Generate prioritized recommendations 256 | const prioritized_recommendations: string[] = []; 257 | 258 | // Add category-specific recommendations 259 | Object.entries(categories) 260 | .filter(([_, data]) => data.issues_count > 0) 261 | .sort(([_, a], [__, b]) => b.issues_count - a.issues_count) 262 | .forEach(([category, data]) => { 263 | let recommendation = ""; 264 | 265 | switch (category) { 266 | case "a11y-color-contrast": 267 | recommendation = "Improve color contrast for better readability"; 268 | break; 269 | case "a11y-names-labels": 270 | recommendation = "Add proper labels to all interactive elements"; 271 | break; 272 | case "a11y-aria": 273 | recommendation = "Fix ARIA attributes and roles"; 274 | break; 275 | case "a11y-navigation": 276 | recommendation = "Improve keyboard navigation and focus management"; 277 | break; 278 | case "a11y-language": 279 | recommendation = "Add proper language attributes to HTML"; 280 | break; 281 | case "a11y-tables-lists": 282 | recommendation = "Fix table and list structures for screen readers"; 283 | break; 284 | default: 285 | recommendation = `Fix ${data.issues_count} issues in ${category}`; 286 | } 287 | 288 | prioritized_recommendations.push(recommendation); 289 | }); 290 | 291 | // Add specific high-impact recommendations 292 | if (issues.some((issue) => issue.id === "color-contrast")) { 293 | prioritized_recommendations.push( 294 | "Fix low contrast text for better readability" 295 | ); 296 | } 297 | 298 | if (issues.some((issue) => issue.id === "document-title")) { 299 | prioritized_recommendations.push("Add a descriptive page title"); 300 | } 301 | 302 | if (issues.some((issue) => issue.id === "image-alt")) { 303 | prioritized_recommendations.push("Add alt text to all images"); 304 | } 305 | 306 | // Create the report content 307 | const reportContent: AccessibilityReportContent = { 308 | score, 309 | audit_counts: { 310 | failed: failedCount, 311 | passed: passedCount, 312 | manual: manualCount, 313 | informative: informativeCount, 314 | not_applicable: notApplicableCount, 315 | }, 316 | issues, 317 | categories, 318 | critical_elements: criticalElements, 319 | prioritized_recommendations: 320 | prioritized_recommendations.length > 0 321 | ? prioritized_recommendations 322 | : undefined, 323 | }; 324 | 325 | // Return the full report following the LighthouseReport interface 326 | return { 327 | metadata, 328 | report: reportContent, 329 | }; 330 | }; 331 | ``` -------------------------------------------------------------------------------- /browser-tools-server/lighthouse/seo.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Result as LighthouseResult } from "lighthouse"; 2 | import { AuditCategory, LighthouseReport } from "./types.js"; 3 | import { runLighthouseAudit } from "./index.js"; 4 | 5 | // === SEO Report Types === 6 | 7 | /** 8 | * SEO-specific report content structure 9 | */ 10 | export interface SEOReportContent { 11 | score: number; // Overall score (0-100) 12 | audit_counts: { 13 | // Counts of different audit types 14 | failed: number; 15 | passed: number; 16 | manual: number; 17 | informative: number; 18 | not_applicable: number; 19 | }; 20 | issues: AISEOIssue[]; 21 | categories: { 22 | [category: string]: { 23 | score: number; 24 | issues_count: number; 25 | }; 26 | }; 27 | prioritized_recommendations?: string[]; // Ordered list of recommendations 28 | } 29 | 30 | /** 31 | * Full SEO report implementing the base LighthouseReport interface 32 | */ 33 | export type AIOptimizedSEOReport = LighthouseReport<SEOReportContent>; 34 | 35 | /** 36 | * AI-optimized SEO issue 37 | */ 38 | interface AISEOIssue { 39 | id: string; // e.g., "meta-description" 40 | title: string; // e.g., "Document has a meta description" 41 | impact: "critical" | "serious" | "moderate" | "minor"; 42 | category: string; // e.g., "content", "mobile", "crawlability" 43 | details?: { 44 | selector?: string; // CSS selector if applicable 45 | value?: string; // Current value 46 | issue?: string; // Description of the issue 47 | }[]; 48 | score: number | null; // 0-1 or null 49 | } 50 | 51 | // Original interfaces for backward compatibility 52 | interface SEOAudit { 53 | id: string; // e.g., "meta-description" 54 | title: string; // e.g., "Document has a meta description" 55 | description: string; // e.g., "Meta descriptions improve SEO..." 56 | score: number | null; // 0-1 or null 57 | scoreDisplayMode: string; // e.g., "binary" 58 | details?: SEOAuditDetails; // Optional, structured details 59 | weight?: number; // For prioritization 60 | } 61 | 62 | interface SEOAuditDetails { 63 | items?: Array<{ 64 | selector?: string; // e.g., "meta[name='description']" 65 | issue?: string; // e.g., "Meta description is missing" 66 | value?: string; // e.g., Current meta description text 67 | }>; 68 | type?: string; // e.g., "table" 69 | } 70 | 71 | // This ensures we always include critical issues while limiting less important ones 72 | const DETAIL_LIMITS = { 73 | critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues 74 | serious: 15, // Up to 15 items for serious issues 75 | moderate: 10, // Up to 10 items for moderate issues 76 | minor: 3, // Up to 3 items for minor issues 77 | }; 78 | 79 | /** 80 | * Runs an SEO audit on the specified URL 81 | * @param url The URL to audit 82 | * @returns Promise resolving to AI-optimized SEO audit results 83 | */ 84 | export async function runSEOAudit(url: string): Promise<AIOptimizedSEOReport> { 85 | try { 86 | const lhr = await runLighthouseAudit(url, [AuditCategory.SEO]); 87 | return extractAIOptimizedData(lhr, url); 88 | } catch (error) { 89 | throw new Error( 90 | `SEO audit failed: ${ 91 | error instanceof Error ? error.message : String(error) 92 | }` 93 | ); 94 | } 95 | } 96 | 97 | /** 98 | * Extract AI-optimized SEO data from Lighthouse results 99 | */ 100 | const extractAIOptimizedData = ( 101 | lhr: LighthouseResult, 102 | url: string 103 | ): AIOptimizedSEOReport => { 104 | const categoryData = lhr.categories[AuditCategory.SEO]; 105 | const audits = lhr.audits || {}; 106 | 107 | // Add metadata 108 | const metadata = { 109 | url, 110 | timestamp: lhr.fetchTime || new Date().toISOString(), 111 | device: "desktop", // This could be made configurable 112 | lighthouseVersion: lhr.lighthouseVersion, 113 | }; 114 | 115 | // Initialize variables 116 | const issues: AISEOIssue[] = []; 117 | const categories: { 118 | [category: string]: { score: number; issues_count: number }; 119 | } = { 120 | content: { score: 0, issues_count: 0 }, 121 | mobile: { score: 0, issues_count: 0 }, 122 | crawlability: { score: 0, issues_count: 0 }, 123 | other: { score: 0, issues_count: 0 }, 124 | }; 125 | 126 | // Count audits by type 127 | let failedCount = 0; 128 | let passedCount = 0; 129 | let manualCount = 0; 130 | let informativeCount = 0; 131 | let notApplicableCount = 0; 132 | 133 | // Process audit refs 134 | const auditRefs = categoryData?.auditRefs || []; 135 | 136 | // First pass: count audits by type and initialize categories 137 | auditRefs.forEach((ref) => { 138 | const audit = audits[ref.id]; 139 | if (!audit) return; 140 | 141 | // Count by scoreDisplayMode 142 | if (audit.scoreDisplayMode === "manual") { 143 | manualCount++; 144 | } else if (audit.scoreDisplayMode === "informative") { 145 | informativeCount++; 146 | } else if (audit.scoreDisplayMode === "notApplicable") { 147 | notApplicableCount++; 148 | } else if (audit.score !== null) { 149 | // Binary pass/fail 150 | if (audit.score >= 0.9) { 151 | passedCount++; 152 | } else { 153 | failedCount++; 154 | } 155 | } 156 | 157 | // Categorize the issue 158 | let category = "other"; 159 | if ( 160 | ref.id.includes("crawl") || 161 | ref.id.includes("http") || 162 | ref.id.includes("redirect") || 163 | ref.id.includes("robots") 164 | ) { 165 | category = "crawlability"; 166 | } else if ( 167 | ref.id.includes("viewport") || 168 | ref.id.includes("font-size") || 169 | ref.id.includes("tap-targets") 170 | ) { 171 | category = "mobile"; 172 | } else if ( 173 | ref.id.includes("document") || 174 | ref.id.includes("meta") || 175 | ref.id.includes("description") || 176 | ref.id.includes("canonical") || 177 | ref.id.includes("title") || 178 | ref.id.includes("link") 179 | ) { 180 | category = "content"; 181 | } 182 | 183 | // Update category score and issues count 184 | if (audit.score !== null && audit.score < 0.9) { 185 | categories[category].issues_count++; 186 | } 187 | }); 188 | 189 | // Second pass: process failed audits into AI-friendly format 190 | auditRefs 191 | .filter((ref) => { 192 | const audit = audits[ref.id]; 193 | return audit && audit.score !== null && audit.score < 0.9; 194 | }) 195 | .sort((a, b) => (b.weight || 0) - (a.weight || 0)) 196 | // No limit on failed audits - we'll filter dynamically based on impact 197 | .forEach((ref) => { 198 | const audit = audits[ref.id]; 199 | 200 | // Determine impact level based on score and weight 201 | let impact: "critical" | "serious" | "moderate" | "minor" = "moderate"; 202 | if (audit.score === 0) { 203 | impact = "critical"; 204 | } else if (audit.score !== null && audit.score <= 0.5) { 205 | impact = "serious"; 206 | } else if (audit.score !== null && audit.score > 0.7) { 207 | impact = "minor"; 208 | } 209 | 210 | // Categorize the issue 211 | let category = "other"; 212 | if ( 213 | ref.id.includes("crawl") || 214 | ref.id.includes("http") || 215 | ref.id.includes("redirect") || 216 | ref.id.includes("robots") 217 | ) { 218 | category = "crawlability"; 219 | } else if ( 220 | ref.id.includes("viewport") || 221 | ref.id.includes("font-size") || 222 | ref.id.includes("tap-targets") 223 | ) { 224 | category = "mobile"; 225 | } else if ( 226 | ref.id.includes("document") || 227 | ref.id.includes("meta") || 228 | ref.id.includes("description") || 229 | ref.id.includes("canonical") || 230 | ref.id.includes("title") || 231 | ref.id.includes("link") 232 | ) { 233 | category = "content"; 234 | } 235 | 236 | // Extract details 237 | const details: { selector?: string; value?: string; issue?: string }[] = 238 | []; 239 | 240 | if (audit.details) { 241 | const auditDetails = audit.details as any; 242 | if (auditDetails.items && Array.isArray(auditDetails.items)) { 243 | // Determine item limit based on impact 244 | const itemLimit = DETAIL_LIMITS[impact]; 245 | 246 | auditDetails.items.slice(0, itemLimit).forEach((item: any) => { 247 | const detail: { 248 | selector?: string; 249 | value?: string; 250 | issue?: string; 251 | } = {}; 252 | 253 | if (item.selector) { 254 | detail.selector = item.selector; 255 | } 256 | 257 | if (item.value !== undefined) { 258 | detail.value = item.value; 259 | } 260 | 261 | if (item.issue) { 262 | detail.issue = item.issue; 263 | } 264 | 265 | if (Object.keys(detail).length > 0) { 266 | details.push(detail); 267 | } 268 | }); 269 | } 270 | } 271 | 272 | // Create the issue 273 | const issue: AISEOIssue = { 274 | id: ref.id, 275 | title: audit.title, 276 | impact, 277 | category, 278 | details: details.length > 0 ? details : undefined, 279 | score: audit.score, 280 | }; 281 | 282 | issues.push(issue); 283 | }); 284 | 285 | // Calculate overall score 286 | const score = Math.round((categoryData?.score || 0) * 100); 287 | 288 | // Generate prioritized recommendations 289 | const prioritized_recommendations: string[] = []; 290 | 291 | // Add category-specific recommendations 292 | Object.entries(categories) 293 | .filter(([_, data]) => data.issues_count > 0) 294 | .sort(([_, a], [__, b]) => b.issues_count - a.issues_count) 295 | .forEach(([category, data]) => { 296 | if (data.issues_count === 0) return; 297 | 298 | let recommendation = ""; 299 | 300 | switch (category) { 301 | case "content": 302 | recommendation = `Improve SEO content (${data.issues_count} issues): titles, descriptions, and headers`; 303 | break; 304 | case "mobile": 305 | recommendation = `Optimize for mobile devices (${data.issues_count} issues)`; 306 | break; 307 | case "crawlability": 308 | recommendation = `Fix crawlability issues (${data.issues_count} issues): robots.txt, sitemaps, and redirects`; 309 | break; 310 | default: 311 | recommendation = `Fix ${data.issues_count} SEO issues in category: ${category}`; 312 | } 313 | 314 | prioritized_recommendations.push(recommendation); 315 | }); 316 | 317 | // Add specific high-impact recommendations 318 | if (issues.some((issue) => issue.id === "meta-description")) { 319 | prioritized_recommendations.push( 320 | "Add a meta description to improve click-through rate" 321 | ); 322 | } 323 | 324 | if (issues.some((issue) => issue.id === "document-title")) { 325 | prioritized_recommendations.push( 326 | "Add a descriptive page title with keywords" 327 | ); 328 | } 329 | 330 | if (issues.some((issue) => issue.id === "hreflang")) { 331 | prioritized_recommendations.push( 332 | "Fix hreflang implementation for international SEO" 333 | ); 334 | } 335 | 336 | if (issues.some((issue) => issue.id === "canonical")) { 337 | prioritized_recommendations.push("Implement proper canonical tags"); 338 | } 339 | 340 | // Create the report content 341 | const reportContent: SEOReportContent = { 342 | score, 343 | audit_counts: { 344 | failed: failedCount, 345 | passed: passedCount, 346 | manual: manualCount, 347 | informative: informativeCount, 348 | not_applicable: notApplicableCount, 349 | }, 350 | issues, 351 | categories, 352 | prioritized_recommendations: 353 | prioritized_recommendations.length > 0 354 | ? prioritized_recommendations 355 | : undefined, 356 | }; 357 | 358 | // Return the full report following the LighthouseReport interface 359 | return { 360 | metadata, 361 | report: reportContent, 362 | }; 363 | }; 364 | ``` -------------------------------------------------------------------------------- /docs/mcp.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP TypeScript SDK   2 | 3 | ## Table of Contents 4 | 5 | - [Overview](mdc:#overview) 6 | - [Installation](mdc:#installation) 7 | - [Quickstart](mdc:#quickstart) 8 | - [What is MCP?](mdc:#what-is-mcp) 9 | - [Core Concepts](mdc:#core-concepts) 10 | - [Server](mdc:#server) 11 | - [Resources](mdc:#resources) 12 | - [Tools](mdc:#tools) 13 | - [Prompts](mdc:#prompts) 14 | - [Running Your Server](mdc:#running-your-server) 15 | - [stdio](mdc:#stdio) 16 | - [HTTP with SSE](mdc:#http-with-sse) 17 | - [Testing and Debugging](mdc:#testing-and-debugging) 18 | - [Examples](mdc:#examples) 19 | - [Echo Server](mdc:#echo-server) 20 | - [SQLite Explorer](mdc:#sqlite-explorer) 21 | - [Advanced Usage](mdc:#advanced-usage) 22 | - [Low-Level Server](mdc:#low-level-server) 23 | - [Writing MCP Clients](mdc:#writing-mcp-clients) 24 | - [Server Capabilities](mdc:#server-capabilities) 25 | 26 | ## Overview 27 | 28 | The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements the full MCP specification, making it easy to: 29 | 30 | - Build MCP clients that can connect to any MCP server 31 | - Create MCP servers that expose resources, prompts and tools 32 | - Use standard transports like stdio and SSE 33 | - Handle all MCP protocol messages and lifecycle events 34 | 35 | ## Installation 36 | 37 | ```bash 38 | npm install @modelcontextprotocol/sdk 39 | ``` 40 | 41 | ## Quick Start 42 | 43 | Let's create a simple MCP server that exposes a calculator tool and some data: 44 | 45 | ```typescript 46 | import { 47 | McpServer, 48 | ResourceTemplate, 49 | } from "@modelcontextprotocol/sdk/server/mcp.js"; 50 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 51 | import { z } from "zod"; 52 | 53 | // Create an MCP server 54 | const server = new McpServer({ 55 | name: "Demo", 56 | version: "1.0.0", 57 | }); 58 | 59 | // Add an addition tool 60 | server.tool("add", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ 61 | content: [{ type: "text", text: String(a + b) }], 62 | })); 63 | 64 | // Add a dynamic greeting resource 65 | server.resource( 66 | "greeting", 67 | new ResourceTemplate("greeting://{name}", { list: undefined }), 68 | async (uri, { name }) => ({ 69 | contents: [ 70 | { 71 | uri: uri.href, 72 | text: `Hello, ${name}!`, 73 | }, 74 | ], 75 | }) 76 | ); 77 | 78 | // Start receiving messages on stdin and sending messages on stdout 79 | const transport = new StdioServerTransport(); 80 | await server.connect(transport); 81 | ``` 82 | 83 | ## What is MCP? 84 | 85 | The [Model Context Protocol (MCP)](mdc:https:/modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: 86 | 87 | - Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) 88 | - Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) 89 | - Define interaction patterns through **Prompts** (reusable templates for LLM interactions) 90 | - And more! 91 | 92 | ## Core Concepts 93 | 94 | ### Server 95 | 96 | The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: 97 | 98 | ```typescript 99 | const server = new McpServer({ 100 | name: "My App", 101 | version: "1.0.0", 102 | }); 103 | ``` 104 | 105 | ### Resources 106 | 107 | Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: 108 | 109 | ```typescript 110 | // Static resource 111 | server.resource("config", "config://app", async (uri) => ({ 112 | contents: [ 113 | { 114 | uri: uri.href, 115 | text: "App configuration here", 116 | }, 117 | ], 118 | })); 119 | 120 | // Dynamic resource with parameters 121 | server.resource( 122 | "user-profile", 123 | new ResourceTemplate("users://{userId}/profile", { list: undefined }), 124 | async (uri, { userId }) => ({ 125 | contents: [ 126 | { 127 | uri: uri.href, 128 | text: `Profile data for user ${userId}`, 129 | }, 130 | ], 131 | }) 132 | ); 133 | ``` 134 | 135 | ### Tools 136 | 137 | Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: 138 | 139 | ```typescript 140 | // Simple tool with parameters 141 | server.tool( 142 | "calculate-bmi", 143 | { 144 | weightKg: z.number(), 145 | heightM: z.number(), 146 | }, 147 | async ({ weightKg, heightM }) => ({ 148 | content: [ 149 | { 150 | type: "text", 151 | text: String(weightKg / (heightM * heightM)), 152 | }, 153 | ], 154 | }) 155 | ); 156 | 157 | // Async tool with external API call 158 | server.tool("fetch-weather", { city: z.string() }, async ({ city }) => { 159 | const response = await fetch(`https://api.weather.com/${city}`); 160 | const data = await response.text(); 161 | return { 162 | content: [{ type: "text", text: data }], 163 | }; 164 | }); 165 | ``` 166 | 167 | ### Prompts 168 | 169 | Prompts are reusable templates that help LLMs interact with your server effectively: 170 | 171 | ```typescript 172 | server.prompt("review-code", { code: z.string() }, ({ code }) => ({ 173 | messages: [ 174 | { 175 | role: "user", 176 | content: { 177 | type: "text", 178 | text: `Please review this code:\n\n${code}`, 179 | }, 180 | }, 181 | ], 182 | })); 183 | ``` 184 | 185 | ## Running Your Server 186 | 187 | MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport: 188 | 189 | ### stdio 190 | 191 | For command-line tools and direct integrations: 192 | 193 | ```typescript 194 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 195 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 196 | 197 | const server = new McpServer({ 198 | name: "example-server", 199 | version: "1.0.0", 200 | }); 201 | 202 | // ... set up server resources, tools, and prompts ... 203 | 204 | const transport = new StdioServerTransport(); 205 | await server.connect(transport); 206 | ``` 207 | 208 | ### HTTP with SSE 209 | 210 | For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to: 211 | 212 | ```typescript 213 | import express from "express"; 214 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 215 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 216 | 217 | const server = new McpServer({ 218 | name: "example-server", 219 | version: "1.0.0", 220 | }); 221 | 222 | // ... set up server resources, tools, and prompts ... 223 | 224 | const app = express(); 225 | 226 | app.get("/sse", async (req, res) => { 227 | const transport = new SSEServerTransport("/messages", res); 228 | await server.connect(transport); 229 | }); 230 | 231 | app.post("/messages", async (req, res) => { 232 | // Note: to support multiple simultaneous connections, these messages will 233 | // need to be routed to a specific matching transport. (This logic isn't 234 | // implemented here, for simplicity.) 235 | await transport.handlePostMessage(req, res); 236 | }); 237 | 238 | app.listen(3001); 239 | ``` 240 | 241 | ### Testing and Debugging 242 | 243 | To test your server, you can use the [MCP Inspector](mdc:https:/github.com/modelcontextprotocol/inspector). See its README for more information. 244 | 245 | ## Examples 246 | 247 | ### Echo Server 248 | 249 | A simple server demonstrating resources, tools, and prompts: 250 | 251 | ```typescript 252 | import { 253 | McpServer, 254 | ResourceTemplate, 255 | } from "@modelcontextprotocol/sdk/server/mcp.js"; 256 | import { z } from "zod"; 257 | 258 | const server = new McpServer({ 259 | name: "Echo", 260 | version: "1.0.0", 261 | }); 262 | 263 | server.resource( 264 | "echo", 265 | new ResourceTemplate("echo://{message}", { list: undefined }), 266 | async (uri, { message }) => ({ 267 | contents: [ 268 | { 269 | uri: uri.href, 270 | text: `Resource echo: ${message}`, 271 | }, 272 | ], 273 | }) 274 | ); 275 | 276 | server.tool("echo", { message: z.string() }, async ({ message }) => ({ 277 | content: [{ type: "text", text: `Tool echo: ${message}` }], 278 | })); 279 | 280 | server.prompt("echo", { message: z.string() }, ({ message }) => ({ 281 | messages: [ 282 | { 283 | role: "user", 284 | content: { 285 | type: "text", 286 | text: `Please process this message: ${message}`, 287 | }, 288 | }, 289 | ], 290 | })); 291 | ``` 292 | 293 | ### SQLite Explorer 294 | 295 | A more complex example showing database integration: 296 | 297 | ```typescript 298 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 299 | import sqlite3 from "sqlite3"; 300 | import { promisify } from "util"; 301 | import { z } from "zod"; 302 | 303 | const server = new McpServer({ 304 | name: "SQLite Explorer", 305 | version: "1.0.0", 306 | }); 307 | 308 | // Helper to create DB connection 309 | const getDb = () => { 310 | const db = new sqlite3.Database("database.db"); 311 | return { 312 | all: promisify<string, any[]>(db.all.bind(db)), 313 | close: promisify(db.close.bind(db)), 314 | }; 315 | }; 316 | 317 | server.resource("schema", "schema://main", async (uri) => { 318 | const db = getDb(); 319 | try { 320 | const tables = await db.all( 321 | "SELECT sql FROM sqlite_master WHERE type='table'" 322 | ); 323 | return { 324 | contents: [ 325 | { 326 | uri: uri.href, 327 | text: tables.map((t: { sql: string }) => t.sql).join("\n"), 328 | }, 329 | ], 330 | }; 331 | } finally { 332 | await db.close(); 333 | } 334 | }); 335 | 336 | server.tool("query", { sql: z.string() }, async ({ sql }) => { 337 | const db = getDb(); 338 | try { 339 | const results = await db.all(sql); 340 | return { 341 | content: [ 342 | { 343 | type: "text", 344 | text: JSON.stringify(results, null, 2), 345 | }, 346 | ], 347 | }; 348 | } catch (err: unknown) { 349 | const error = err as Error; 350 | return { 351 | content: [ 352 | { 353 | type: "text", 354 | text: `Error: ${error.message}`, 355 | }, 356 | ], 357 | isError: true, 358 | }; 359 | } finally { 360 | await db.close(); 361 | } 362 | }); 363 | ``` 364 | 365 | ## Advanced Usage 366 | 367 | ### Low-Level Server 368 | 369 | For more control, you can use the low-level Server class directly: 370 | 371 | ```typescript 372 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 373 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 374 | import { 375 | ListPromptsRequestSchema, 376 | GetPromptRequestSchema, 377 | } from "@modelcontextprotocol/sdk/types.js"; 378 | 379 | const server = new Server( 380 | { 381 | name: "example-server", 382 | version: "1.0.0", 383 | }, 384 | { 385 | capabilities: { 386 | prompts: {}, 387 | }, 388 | } 389 | ); 390 | 391 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 392 | return { 393 | prompts: [ 394 | { 395 | name: "example-prompt", 396 | description: "An example prompt template", 397 | arguments: [ 398 | { 399 | name: "arg1", 400 | description: "Example argument", 401 | required: true, 402 | }, 403 | ], 404 | }, 405 | ], 406 | }; 407 | }); 408 | 409 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 410 | if (request.params.name !== "example-prompt") { 411 | throw new Error("Unknown prompt"); 412 | } 413 | return { 414 | description: "Example prompt", 415 | messages: [ 416 | { 417 | role: "user", 418 | content: { 419 | type: "text", 420 | text: "Example prompt text", 421 | }, 422 | }, 423 | ], 424 | }; 425 | }); 426 | 427 | const transport = new StdioServerTransport(); 428 | await server.connect(transport); 429 | ``` 430 | 431 | ### Writing MCP Clients 432 | 433 | The SDK provides a high-level client interface: 434 | 435 | ```typescript 436 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 437 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 438 | 439 | const transport = new StdioClientTransport({ 440 | command: "node", 441 | args: ["server.js"], 442 | }); 443 | 444 | const client = new Client( 445 | { 446 | name: "example-client", 447 | version: "1.0.0", 448 | }, 449 | { 450 | capabilities: { 451 | prompts: {}, 452 | resources: {}, 453 | tools: {}, 454 | }, 455 | } 456 | ); 457 | 458 | await client.connect(transport); 459 | 460 | // List prompts 461 | const prompts = await client.listPrompts(); 462 | 463 | // Get a prompt 464 | const prompt = await client.getPrompt("example-prompt", { 465 | arg1: "value", 466 | }); 467 | 468 | // List resources 469 | const resources = await client.listResources(); 470 | 471 | // Read a resource 472 | const resource = await client.readResource("file:///example.txt"); 473 | 474 | // Call a tool 475 | const result = await client.callTool({ 476 | name: "example-tool", 477 | arguments: { 478 | arg1: "value", 479 | }, 480 | }); 481 | ``` 482 | 483 | ## Documentation 484 | 485 | - [Model Context Protocol documentation](mdc:https:/modelcontextprotocol.io) 486 | - [MCP Specification](mdc:https:/spec.modelcontextprotocol.io) 487 | - [Example Servers](mdc:https:/github.com/modelcontextprotocol/servers) 488 | 489 | ## Contributing 490 | 491 | Issues and pull requests are welcome on GitHub at https://github.com/modelcontextprotocol/typescript-sdk. 492 | 493 | ## License 494 | 495 | This project is licensed under the MIT License—see the [LICENSE](mdc:LICENSE) file for details. 496 | ``` -------------------------------------------------------------------------------- /chrome-extension/background.js: -------------------------------------------------------------------------------- ```javascript 1 | // Listen for messages from the devtools panel 2 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 3 | if (message.type === "GET_CURRENT_URL" && message.tabId) { 4 | getCurrentTabUrl(message.tabId) 5 | .then((url) => { 6 | sendResponse({ success: true, url: url }); 7 | }) 8 | .catch((error) => { 9 | sendResponse({ success: false, error: error.message }); 10 | }); 11 | return true; // Required to use sendResponse asynchronously 12 | } 13 | 14 | // Handle explicit request to update the server with the URL 15 | if (message.type === "UPDATE_SERVER_URL" && message.tabId && message.url) { 16 | console.log( 17 | `Background: Received request to update server with URL for tab ${message.tabId}: ${message.url}` 18 | ); 19 | updateServerWithUrl( 20 | message.tabId, 21 | message.url, 22 | message.source || "explicit_update" 23 | ) 24 | .then(() => { 25 | if (sendResponse) sendResponse({ success: true }); 26 | }) 27 | .catch((error) => { 28 | console.error("Background: Error updating server with URL:", error); 29 | if (sendResponse) 30 | sendResponse({ success: false, error: error.message }); 31 | }); 32 | return true; // Required to use sendResponse asynchronously 33 | } 34 | 35 | if (message.type === "CAPTURE_SCREENSHOT" && message.tabId) { 36 | // First get the server settings 37 | chrome.storage.local.get(["browserConnectorSettings"], (result) => { 38 | const settings = result.browserConnectorSettings || { 39 | serverHost: "localhost", 40 | serverPort: 3025, 41 | }; 42 | 43 | // Validate server identity first 44 | validateServerIdentity(settings.serverHost, settings.serverPort) 45 | .then((isValid) => { 46 | if (!isValid) { 47 | console.error( 48 | "Cannot capture screenshot: Not connected to a valid browser tools server" 49 | ); 50 | sendResponse({ 51 | success: false, 52 | error: 53 | "Not connected to a valid browser tools server. Please check your connection settings.", 54 | }); 55 | return; 56 | } 57 | 58 | // Continue with screenshot capture 59 | captureAndSendScreenshot(message, settings, sendResponse); 60 | }) 61 | .catch((error) => { 62 | console.error("Error validating server:", error); 63 | sendResponse({ 64 | success: false, 65 | error: "Failed to validate server identity: " + error.message, 66 | }); 67 | }); 68 | }); 69 | return true; // Required to use sendResponse asynchronously 70 | } 71 | }); 72 | 73 | // Validate server identity 74 | async function validateServerIdentity(host, port) { 75 | try { 76 | const response = await fetch(`http://${host}:${port}/.identity`, { 77 | signal: AbortSignal.timeout(3000), // 3 second timeout 78 | }); 79 | 80 | if (!response.ok) { 81 | console.error(`Invalid server response: ${response.status}`); 82 | return false; 83 | } 84 | 85 | const identity = await response.json(); 86 | 87 | // Validate the server signature 88 | if (identity.signature !== "mcp-browser-connector-24x7") { 89 | console.error("Invalid server signature - not the browser tools server"); 90 | return false; 91 | } 92 | 93 | return true; 94 | } catch (error) { 95 | console.error("Error validating server identity:", error); 96 | return false; 97 | } 98 | } 99 | 100 | // Helper function to process the tab and run the audit 101 | function processTabForAudit(tab, tabId) { 102 | const url = tab.url; 103 | 104 | if (!url) { 105 | console.error(`No URL available for tab ${tabId}`); 106 | return; 107 | } 108 | 109 | // Update our cache and the server with this URL 110 | tabUrls.set(tabId, url); 111 | updateServerWithUrl(tabId, url); 112 | } 113 | 114 | // Track URLs for each tab 115 | const tabUrls = new Map(); 116 | 117 | // Function to get the current URL for a tab 118 | async function getCurrentTabUrl(tabId) { 119 | try { 120 | console.log("Background: Getting URL for tab", tabId); 121 | 122 | // First check if we have it cached 123 | if (tabUrls.has(tabId)) { 124 | const cachedUrl = tabUrls.get(tabId); 125 | console.log("Background: Found cached URL:", cachedUrl); 126 | return cachedUrl; 127 | } 128 | 129 | // Otherwise get it from the tab 130 | try { 131 | const tab = await chrome.tabs.get(tabId); 132 | if (tab && tab.url) { 133 | // Cache the URL 134 | tabUrls.set(tabId, tab.url); 135 | console.log("Background: Got URL from tab:", tab.url); 136 | return tab.url; 137 | } else { 138 | console.log("Background: Tab exists but no URL found"); 139 | } 140 | } catch (tabError) { 141 | console.error("Background: Error getting tab:", tabError); 142 | } 143 | 144 | // If we can't get the tab directly, try querying for active tabs 145 | try { 146 | const tabs = await chrome.tabs.query({ 147 | active: true, 148 | currentWindow: true, 149 | }); 150 | if (tabs && tabs.length > 0 && tabs[0].url) { 151 | const activeUrl = tabs[0].url; 152 | console.log("Background: Got URL from active tab:", activeUrl); 153 | // Cache this URL as well 154 | tabUrls.set(tabId, activeUrl); 155 | return activeUrl; 156 | } 157 | } catch (queryError) { 158 | console.error("Background: Error querying tabs:", queryError); 159 | } 160 | 161 | console.log("Background: Could not find URL for tab", tabId); 162 | return null; 163 | } catch (error) { 164 | console.error("Background: Error getting tab URL:", error); 165 | return null; 166 | } 167 | } 168 | 169 | // Listen for tab updates to detect page refreshes and URL changes 170 | chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { 171 | // Track URL changes 172 | if (changeInfo.url) { 173 | console.log(`URL changed in tab ${tabId} to ${changeInfo.url}`); 174 | tabUrls.set(tabId, changeInfo.url); 175 | 176 | // Send URL update to server if possible 177 | updateServerWithUrl(tabId, changeInfo.url, "tab_url_change"); 178 | } 179 | 180 | // Check if this is a page refresh (status becoming "complete") 181 | if (changeInfo.status === "complete") { 182 | // Update URL in our cache 183 | if (tab.url) { 184 | tabUrls.set(tabId, tab.url); 185 | // Send URL update to server if possible 186 | updateServerWithUrl(tabId, tab.url, "page_complete"); 187 | } 188 | 189 | retestConnectionOnRefresh(tabId); 190 | } 191 | }); 192 | 193 | // Listen for tab activation (switching between tabs) 194 | chrome.tabs.onActivated.addListener((activeInfo) => { 195 | const tabId = activeInfo.tabId; 196 | console.log(`Tab activated: ${tabId}`); 197 | 198 | // Get the URL of the newly activated tab 199 | chrome.tabs.get(tabId, (tab) => { 200 | if (chrome.runtime.lastError) { 201 | console.error("Error getting tab info:", chrome.runtime.lastError); 202 | return; 203 | } 204 | 205 | if (tab && tab.url) { 206 | console.log(`Active tab changed to ${tab.url}`); 207 | 208 | // Update our cache 209 | tabUrls.set(tabId, tab.url); 210 | 211 | // Send URL update to server 212 | updateServerWithUrl(tabId, tab.url, "tab_activated"); 213 | } 214 | }); 215 | }); 216 | 217 | // Function to update the server with the current URL 218 | async function updateServerWithUrl(tabId, url, source = "background_update") { 219 | if (!url) { 220 | console.error("Cannot update server with empty URL"); 221 | return; 222 | } 223 | 224 | console.log(`Updating server with URL for tab ${tabId}: ${url}`); 225 | 226 | // Get the saved settings 227 | chrome.storage.local.get(["browserConnectorSettings"], async (result) => { 228 | const settings = result.browserConnectorSettings || { 229 | serverHost: "localhost", 230 | serverPort: 3025, 231 | }; 232 | 233 | // Maximum number of retry attempts 234 | const maxRetries = 3; 235 | let retryCount = 0; 236 | let success = false; 237 | 238 | while (retryCount < maxRetries && !success) { 239 | try { 240 | // Send the URL to the server 241 | const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/current-url`; 242 | console.log( 243 | `Attempt ${ 244 | retryCount + 1 245 | }/${maxRetries} to update server with URL: ${url}` 246 | ); 247 | 248 | const response = await fetch(serverUrl, { 249 | method: "POST", 250 | headers: { 251 | "Content-Type": "application/json", 252 | }, 253 | body: JSON.stringify({ 254 | url: url, 255 | tabId: tabId, 256 | timestamp: Date.now(), 257 | source: source, 258 | }), 259 | // Add a timeout to prevent hanging requests 260 | signal: AbortSignal.timeout(5000), 261 | }); 262 | 263 | if (response.ok) { 264 | const responseData = await response.json(); 265 | console.log( 266 | `Successfully updated server with URL: ${url}`, 267 | responseData 268 | ); 269 | success = true; 270 | } else { 271 | console.error( 272 | `Server returned error: ${response.status} ${response.statusText}` 273 | ); 274 | retryCount++; 275 | // Wait before retrying 276 | await new Promise((resolve) => setTimeout(resolve, 500)); 277 | } 278 | } catch (error) { 279 | console.error(`Error updating server with URL: ${error.message}`); 280 | retryCount++; 281 | // Wait before retrying 282 | await new Promise((resolve) => setTimeout(resolve, 500)); 283 | } 284 | } 285 | 286 | if (!success) { 287 | console.error( 288 | `Failed to update server with URL after ${maxRetries} attempts` 289 | ); 290 | } 291 | }); 292 | } 293 | 294 | // Clean up when tabs are closed 295 | chrome.tabs.onRemoved.addListener((tabId) => { 296 | tabUrls.delete(tabId); 297 | }); 298 | 299 | // Function to retest connection when a page is refreshed 300 | async function retestConnectionOnRefresh(tabId) { 301 | console.log(`Page refreshed in tab ${tabId}, retesting connection...`); 302 | 303 | // Get the saved settings 304 | chrome.storage.local.get(["browserConnectorSettings"], async (result) => { 305 | const settings = result.browserConnectorSettings || { 306 | serverHost: "localhost", 307 | serverPort: 3025, 308 | }; 309 | 310 | // Test the connection with the last known host and port 311 | const isConnected = await validateServerIdentity( 312 | settings.serverHost, 313 | settings.serverPort 314 | ); 315 | 316 | // Notify all devtools instances about the connection status 317 | chrome.runtime.sendMessage({ 318 | type: "CONNECTION_STATUS_UPDATE", 319 | isConnected: isConnected, 320 | tabId: tabId, 321 | }); 322 | 323 | // Always notify for page refresh, whether connected or not 324 | // This ensures any ongoing discovery is cancelled and restarted 325 | chrome.runtime.sendMessage({ 326 | type: "INITIATE_AUTO_DISCOVERY", 327 | reason: "page_refresh", 328 | tabId: tabId, 329 | forceRestart: true, // Add a flag to indicate this should force restart any ongoing processes 330 | }); 331 | 332 | if (!isConnected) { 333 | console.log( 334 | "Connection test failed after page refresh, initiating auto-discovery..." 335 | ); 336 | } else { 337 | console.log("Connection test successful after page refresh"); 338 | } 339 | }); 340 | } 341 | 342 | // Function to capture and send screenshot 343 | function captureAndSendScreenshot(message, settings, sendResponse) { 344 | // Get the inspected window's tab 345 | chrome.tabs.get(message.tabId, (tab) => { 346 | if (chrome.runtime.lastError) { 347 | console.error("Error getting tab:", chrome.runtime.lastError); 348 | sendResponse({ 349 | success: false, 350 | error: chrome.runtime.lastError.message, 351 | }); 352 | return; 353 | } 354 | 355 | // Get all windows to find the one containing our tab 356 | chrome.windows.getAll({ populate: true }, (windows) => { 357 | const targetWindow = windows.find((w) => 358 | w.tabs.some((t) => t.id === message.tabId) 359 | ); 360 | 361 | if (!targetWindow) { 362 | console.error("Could not find window containing the inspected tab"); 363 | sendResponse({ 364 | success: false, 365 | error: "Could not find window containing the inspected tab", 366 | }); 367 | return; 368 | } 369 | 370 | // Capture screenshot of the window containing our tab 371 | chrome.tabs.captureVisibleTab( 372 | targetWindow.id, 373 | { format: "png" }, 374 | (dataUrl) => { 375 | // Ignore DevTools panel capture error if it occurs 376 | if ( 377 | chrome.runtime.lastError && 378 | !chrome.runtime.lastError.message.includes("devtools://") 379 | ) { 380 | console.error( 381 | "Error capturing screenshot:", 382 | chrome.runtime.lastError 383 | ); 384 | sendResponse({ 385 | success: false, 386 | error: chrome.runtime.lastError.message, 387 | }); 388 | return; 389 | } 390 | 391 | // Send screenshot data to browser connector using configured settings 392 | const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/screenshot`; 393 | console.log(`Sending screenshot to ${serverUrl}`); 394 | 395 | fetch(serverUrl, { 396 | method: "POST", 397 | headers: { 398 | "Content-Type": "application/json", 399 | }, 400 | body: JSON.stringify({ 401 | data: dataUrl, 402 | path: message.screenshotPath, 403 | }), 404 | }) 405 | .then((response) => response.json()) 406 | .then((result) => { 407 | if (result.error) { 408 | console.error("Error from server:", result.error); 409 | sendResponse({ success: false, error: result.error }); 410 | } else { 411 | console.log("Screenshot saved successfully:", result.path); 412 | // Send success response even if DevTools capture failed 413 | sendResponse({ 414 | success: true, 415 | path: result.path, 416 | title: tab.title || "Current Tab", 417 | }); 418 | } 419 | }) 420 | .catch((error) => { 421 | console.error("Error sending screenshot data:", error); 422 | sendResponse({ 423 | success: false, 424 | error: error.message || "Failed to save screenshot", 425 | }); 426 | }); 427 | } 428 | ); 429 | }); 430 | }); 431 | } 432 | ``` -------------------------------------------------------------------------------- /browser-tools-server/lighthouse/performance.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { Result as LighthouseResult } from "lighthouse"; 2 | import { AuditCategory, LighthouseReport } from "./types.js"; 3 | import { runLighthouseAudit } from "./index.js"; 4 | 5 | // === Performance Report Types === 6 | 7 | /** 8 | * Performance-specific report content structure 9 | */ 10 | export interface PerformanceReportContent { 11 | score: number; // Overall score (0-100) 12 | audit_counts: { 13 | // Counts of different audit types 14 | failed: number; 15 | passed: number; 16 | manual: number; 17 | informative: number; 18 | not_applicable: number; 19 | }; 20 | metrics: AIOptimizedMetric[]; 21 | opportunities: AIOptimizedOpportunity[]; 22 | page_stats?: AIPageStats; // Optional page statistics 23 | prioritized_recommendations?: string[]; // Ordered list of recommendations 24 | } 25 | 26 | /** 27 | * Full performance report implementing the base LighthouseReport interface 28 | */ 29 | export type AIOptimizedPerformanceReport = 30 | LighthouseReport<PerformanceReportContent>; 31 | 32 | // AI-optimized performance metric format 33 | interface AIOptimizedMetric { 34 | id: string; // Short ID like "lcp", "fcp" 35 | score: number | null; // 0-1 score 36 | value_ms: number; // Value in milliseconds 37 | element_type?: string; // For LCP: "image", "text", etc. 38 | element_selector?: string; // DOM selector for the element 39 | element_url?: string; // For images/videos 40 | element_content?: string; // For text content (truncated) 41 | passes_core_web_vital?: boolean; // Whether this metric passes as a Core Web Vital 42 | } 43 | 44 | // AI-optimized opportunity format 45 | interface AIOptimizedOpportunity { 46 | id: string; // Like "render_blocking", "http2" 47 | savings_ms: number; // Time savings in ms 48 | severity?: "critical" | "serious" | "moderate" | "minor"; // Severity classification 49 | resources: Array<{ 50 | url: string; // Resource URL 51 | savings_ms?: number; // Individual resource savings 52 | size_kb?: number; // Size in KB 53 | type?: string; // Resource type (js, css, img, etc.) 54 | is_third_party?: boolean; // Whether this is a third-party resource 55 | }>; 56 | } 57 | 58 | // Page stats for AI analysis 59 | interface AIPageStats { 60 | total_size_kb: number; // Total page weight in KB 61 | total_requests: number; // Total number of requests 62 | resource_counts: { 63 | // Count by resource type 64 | js: number; 65 | css: number; 66 | img: number; 67 | font: number; 68 | other: number; 69 | }; 70 | third_party_size_kb: number; // Size of third-party resources 71 | main_thread_blocking_time_ms: number; // Time spent blocking the main thread 72 | } 73 | 74 | // This ensures we always include critical issues while limiting less important ones 75 | const DETAIL_LIMITS = { 76 | critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues 77 | serious: 15, // Up to 15 items for serious issues 78 | moderate: 10, // Up to 10 items for moderate issues 79 | minor: 3, // Up to 3 items for minor issues 80 | }; 81 | 82 | /** 83 | * Performance audit adapted for AI consumption 84 | * This format is optimized for AI agents with: 85 | * - Concise, relevant information without redundant descriptions 86 | * - Key metrics and opportunities clearly structured 87 | * - Only actionable data that an AI can use for recommendations 88 | */ 89 | export async function runPerformanceAudit( 90 | url: string 91 | ): Promise<AIOptimizedPerformanceReport> { 92 | try { 93 | const lhr = await runLighthouseAudit(url, [AuditCategory.PERFORMANCE]); 94 | return extractAIOptimizedData(lhr, url); 95 | } catch (error) { 96 | throw new Error( 97 | `Performance audit failed: ${ 98 | error instanceof Error ? error.message : String(error) 99 | }` 100 | ); 101 | } 102 | } 103 | 104 | /** 105 | * Extract AI-optimized performance data from Lighthouse results 106 | */ 107 | const extractAIOptimizedData = ( 108 | lhr: LighthouseResult, 109 | url: string 110 | ): AIOptimizedPerformanceReport => { 111 | const audits = lhr.audits || {}; 112 | const categoryData = lhr.categories[AuditCategory.PERFORMANCE]; 113 | const score = Math.round((categoryData?.score || 0) * 100); 114 | 115 | // Add metadata 116 | const metadata = { 117 | url, 118 | timestamp: lhr.fetchTime || new Date().toISOString(), 119 | device: "desktop", // This could be made configurable 120 | lighthouseVersion: lhr.lighthouseVersion, 121 | }; 122 | 123 | // Count audits by type 124 | const auditRefs = categoryData?.auditRefs || []; 125 | let failedCount = 0; 126 | let passedCount = 0; 127 | let manualCount = 0; 128 | let informativeCount = 0; 129 | let notApplicableCount = 0; 130 | 131 | auditRefs.forEach((ref) => { 132 | const audit = audits[ref.id]; 133 | if (!audit) return; 134 | 135 | if (audit.scoreDisplayMode === "manual") { 136 | manualCount++; 137 | } else if (audit.scoreDisplayMode === "informative") { 138 | informativeCount++; 139 | } else if (audit.scoreDisplayMode === "notApplicable") { 140 | notApplicableCount++; 141 | } else if (audit.score !== null) { 142 | if (audit.score >= 0.9) { 143 | passedCount++; 144 | } else { 145 | failedCount++; 146 | } 147 | } 148 | }); 149 | 150 | const audit_counts = { 151 | failed: failedCount, 152 | passed: passedCount, 153 | manual: manualCount, 154 | informative: informativeCount, 155 | not_applicable: notApplicableCount, 156 | }; 157 | 158 | const metrics: AIOptimizedMetric[] = []; 159 | const opportunities: AIOptimizedOpportunity[] = []; 160 | 161 | // Extract core metrics 162 | if (audits["largest-contentful-paint"]) { 163 | const lcp = audits["largest-contentful-paint"]; 164 | const lcpElement = audits["largest-contentful-paint-element"]; 165 | 166 | const metric: AIOptimizedMetric = { 167 | id: "lcp", 168 | score: lcp.score, 169 | value_ms: Math.round(lcp.numericValue || 0), 170 | passes_core_web_vital: lcp.score !== null && lcp.score >= 0.9, 171 | }; 172 | 173 | // Enhanced LCP element detection 174 | 175 | // 1. Try from largest-contentful-paint-element audit 176 | if (lcpElement && lcpElement.details) { 177 | const lcpDetails = lcpElement.details as any; 178 | 179 | // First attempt - try to get directly from items 180 | if ( 181 | lcpDetails.items && 182 | Array.isArray(lcpDetails.items) && 183 | lcpDetails.items.length > 0 184 | ) { 185 | const item = lcpDetails.items[0]; 186 | 187 | // For text elements in tables format 188 | if (item.type === "table" && item.items && item.items.length > 0) { 189 | const firstTableItem = item.items[0]; 190 | 191 | if (firstTableItem.node) { 192 | if (firstTableItem.node.selector) { 193 | metric.element_selector = firstTableItem.node.selector; 194 | } 195 | 196 | // Determine element type based on path or selector 197 | const path = firstTableItem.node.path; 198 | const selector = firstTableItem.node.selector || ""; 199 | 200 | if (path) { 201 | if ( 202 | selector.includes(" > img") || 203 | selector.includes(" img") || 204 | selector.endsWith("img") || 205 | path.includes(",IMG") 206 | ) { 207 | metric.element_type = "image"; 208 | 209 | // Try to extract image name from selector 210 | const imgMatch = selector.match(/img[.][^> ]+/); 211 | if (imgMatch && !metric.element_url) { 212 | metric.element_url = imgMatch[0]; 213 | } 214 | } else if ( 215 | path.includes(",SPAN") || 216 | path.includes(",P") || 217 | path.includes(",H") 218 | ) { 219 | metric.element_type = "text"; 220 | } 221 | } 222 | 223 | // Try to extract text content if available 224 | if (firstTableItem.node.nodeLabel) { 225 | metric.element_content = firstTableItem.node.nodeLabel.substring( 226 | 0, 227 | 100 228 | ); 229 | } 230 | } 231 | } 232 | // Original handling for direct items 233 | else if (item.node?.nodeLabel) { 234 | // Determine element type from node label 235 | if (item.node.nodeLabel.startsWith("<img")) { 236 | metric.element_type = "image"; 237 | // Try to extract image URL from the node snippet 238 | const match = item.node.snippet?.match(/src="([^"]+)"/); 239 | if (match && match[1]) { 240 | metric.element_url = match[1]; 241 | } 242 | } else if (item.node.nodeLabel.startsWith("<video")) { 243 | metric.element_type = "video"; 244 | } else if (item.node.nodeLabel.startsWith("<h")) { 245 | metric.element_type = "heading"; 246 | } else { 247 | metric.element_type = "text"; 248 | } 249 | 250 | if (item.node?.selector) { 251 | metric.element_selector = item.node.selector; 252 | } 253 | } 254 | } 255 | } 256 | 257 | // 2. Try from lcp-lazy-loaded audit 258 | const lcpImageAudit = audits["lcp-lazy-loaded"]; 259 | if (lcpImageAudit && lcpImageAudit.details) { 260 | const lcpImageDetails = lcpImageAudit.details as any; 261 | 262 | if ( 263 | lcpImageDetails.items && 264 | Array.isArray(lcpImageDetails.items) && 265 | lcpImageDetails.items.length > 0 266 | ) { 267 | const item = lcpImageDetails.items[0]; 268 | 269 | if (item.url) { 270 | metric.element_type = "image"; 271 | metric.element_url = item.url; 272 | } 273 | } 274 | } 275 | 276 | // 3. Try directly from the LCP audit details 277 | if (!metric.element_url && lcp.details) { 278 | const lcpDirectDetails = lcp.details as any; 279 | 280 | if (lcpDirectDetails.items && Array.isArray(lcpDirectDetails.items)) { 281 | for (const item of lcpDirectDetails.items) { 282 | if (item.url || (item.node && item.node.path)) { 283 | if (item.url) { 284 | metric.element_url = item.url; 285 | metric.element_type = item.url.match( 286 | /\.(jpg|jpeg|png|gif|webp|svg)$/i 287 | ) 288 | ? "image" 289 | : "resource"; 290 | } 291 | if (item.node && item.node.selector) { 292 | metric.element_selector = item.node.selector; 293 | } 294 | break; 295 | } 296 | } 297 | } 298 | } 299 | 300 | // 4. Check for specific audit that might contain image info 301 | const largestImageAudit = audits["largest-image-paint"]; 302 | if (largestImageAudit && largestImageAudit.details) { 303 | const imageDetails = largestImageAudit.details as any; 304 | 305 | if ( 306 | imageDetails.items && 307 | Array.isArray(imageDetails.items) && 308 | imageDetails.items.length > 0 309 | ) { 310 | const item = imageDetails.items[0]; 311 | 312 | if (item.url) { 313 | // If we have a large image that's close in time to LCP, it's likely the LCP element 314 | metric.element_type = "image"; 315 | metric.element_url = item.url; 316 | } 317 | } 318 | } 319 | 320 | // 5. Check for network requests audit to find image resources 321 | if (!metric.element_url) { 322 | const networkRequests = audits["network-requests"]; 323 | 324 | if (networkRequests && networkRequests.details) { 325 | const networkDetails = networkRequests.details as any; 326 | 327 | if (networkDetails.items && Array.isArray(networkDetails.items)) { 328 | // Get all image resources loaded close to the LCP time 329 | const lcpTime = lcp.numericValue || 0; 330 | const imageResources = networkDetails.items 331 | .filter( 332 | (item: any) => 333 | item.url && 334 | item.mimeType && 335 | item.mimeType.startsWith("image/") && 336 | item.endTime && 337 | Math.abs(item.endTime - lcpTime) < 500 // Within 500ms of LCP 338 | ) 339 | .sort( 340 | (a: any, b: any) => 341 | Math.abs(a.endTime - lcpTime) - Math.abs(b.endTime - lcpTime) 342 | ); 343 | 344 | if (imageResources.length > 0) { 345 | const closestImage = imageResources[0]; 346 | 347 | if (!metric.element_type) { 348 | metric.element_type = "image"; 349 | metric.element_url = closestImage.url; 350 | } 351 | } 352 | } 353 | } 354 | } 355 | 356 | metrics.push(metric); 357 | } 358 | 359 | if (audits["first-contentful-paint"]) { 360 | const fcp = audits["first-contentful-paint"]; 361 | metrics.push({ 362 | id: "fcp", 363 | score: fcp.score, 364 | value_ms: Math.round(fcp.numericValue || 0), 365 | passes_core_web_vital: fcp.score !== null && fcp.score >= 0.9, 366 | }); 367 | } 368 | 369 | if (audits["speed-index"]) { 370 | const si = audits["speed-index"]; 371 | metrics.push({ 372 | id: "si", 373 | score: si.score, 374 | value_ms: Math.round(si.numericValue || 0), 375 | }); 376 | } 377 | 378 | if (audits["interactive"]) { 379 | const tti = audits["interactive"]; 380 | metrics.push({ 381 | id: "tti", 382 | score: tti.score, 383 | value_ms: Math.round(tti.numericValue || 0), 384 | }); 385 | } 386 | 387 | // Add CLS (Cumulative Layout Shift) 388 | if (audits["cumulative-layout-shift"]) { 389 | const cls = audits["cumulative-layout-shift"]; 390 | metrics.push({ 391 | id: "cls", 392 | score: cls.score, 393 | // CLS is not in ms, but a unitless value 394 | value_ms: Math.round((cls.numericValue || 0) * 1000) / 1000, // Convert to 3 decimal places 395 | passes_core_web_vital: cls.score !== null && cls.score >= 0.9, 396 | }); 397 | } 398 | 399 | // Add TBT (Total Blocking Time) 400 | if (audits["total-blocking-time"]) { 401 | const tbt = audits["total-blocking-time"]; 402 | metrics.push({ 403 | id: "tbt", 404 | score: tbt.score, 405 | value_ms: Math.round(tbt.numericValue || 0), 406 | passes_core_web_vital: tbt.score !== null && tbt.score >= 0.9, 407 | }); 408 | } 409 | 410 | // Extract opportunities 411 | if (audits["render-blocking-resources"]) { 412 | const rbrAudit = audits["render-blocking-resources"]; 413 | 414 | // Determine impact level based on potential savings 415 | let impact: "critical" | "serious" | "moderate" | "minor" = "moderate"; 416 | const savings = Math.round(rbrAudit.numericValue || 0); 417 | 418 | if (savings > 2000) { 419 | impact = "critical"; 420 | } else if (savings > 1000) { 421 | impact = "serious"; 422 | } else if (savings < 300) { 423 | impact = "minor"; 424 | } 425 | 426 | const opportunity: AIOptimizedOpportunity = { 427 | id: "render_blocking_resources", 428 | savings_ms: savings, 429 | severity: impact, 430 | resources: [], 431 | }; 432 | 433 | const rbrDetails = rbrAudit.details as any; 434 | if (rbrDetails && rbrDetails.items && Array.isArray(rbrDetails.items)) { 435 | // Determine how many items to include based on impact 436 | const itemLimit = DETAIL_LIMITS[impact]; 437 | 438 | rbrDetails.items 439 | .slice(0, itemLimit) 440 | .forEach((item: { url?: string; wastedMs?: number }) => { 441 | if (item.url) { 442 | // Extract file name from full URL 443 | const fileName = item.url.split("/").pop() || item.url; 444 | opportunity.resources.push({ 445 | url: fileName, 446 | savings_ms: Math.round(item.wastedMs || 0), 447 | }); 448 | } 449 | }); 450 | } 451 | 452 | if (opportunity.resources.length > 0) { 453 | opportunities.push(opportunity); 454 | } 455 | } 456 | 457 | if (audits["uses-http2"]) { 458 | const http2Audit = audits["uses-http2"]; 459 | 460 | // Determine impact level based on potential savings 461 | let impact: "critical" | "serious" | "moderate" | "minor" = "moderate"; 462 | const savings = Math.round(http2Audit.numericValue || 0); 463 | 464 | if (savings > 2000) { 465 | impact = "critical"; 466 | } else if (savings > 1000) { 467 | impact = "serious"; 468 | } else if (savings < 300) { 469 | impact = "minor"; 470 | } 471 | 472 | const opportunity: AIOptimizedOpportunity = { 473 | id: "http2", 474 | savings_ms: savings, 475 | severity: impact, 476 | resources: [], 477 | }; 478 | 479 | const http2Details = http2Audit.details as any; 480 | if ( 481 | http2Details && 482 | http2Details.items && 483 | Array.isArray(http2Details.items) 484 | ) { 485 | // Determine how many items to include based on impact 486 | const itemLimit = DETAIL_LIMITS[impact]; 487 | 488 | http2Details.items 489 | .slice(0, itemLimit) 490 | .forEach((item: { url?: string }) => { 491 | if (item.url) { 492 | // Extract file name from full URL 493 | const fileName = item.url.split("/").pop() || item.url; 494 | opportunity.resources.push({ url: fileName }); 495 | } 496 | }); 497 | } 498 | 499 | if (opportunity.resources.length > 0) { 500 | opportunities.push(opportunity); 501 | } 502 | } 503 | 504 | // After extracting all metrics and opportunities, collect page stats 505 | // Extract page stats 506 | let page_stats: AIPageStats | undefined; 507 | 508 | // Total page stats 509 | const totalByteWeight = audits["total-byte-weight"]; 510 | const networkRequests = audits["network-requests"]; 511 | const thirdPartyAudit = audits["third-party-summary"]; 512 | const mainThreadWork = audits["mainthread-work-breakdown"]; 513 | 514 | if (networkRequests && networkRequests.details) { 515 | const resourceDetails = networkRequests.details as any; 516 | 517 | if (resourceDetails.items && Array.isArray(resourceDetails.items)) { 518 | const resources = resourceDetails.items; 519 | const totalRequests = resources.length; 520 | 521 | // Calculate total size and counts by type 522 | let totalSizeKb = 0; 523 | let jsCount = 0, 524 | cssCount = 0, 525 | imgCount = 0, 526 | fontCount = 0, 527 | otherCount = 0; 528 | 529 | resources.forEach((resource: any) => { 530 | const sizeKb = resource.transferSize 531 | ? Math.round(resource.transferSize / 1024) 532 | : 0; 533 | totalSizeKb += sizeKb; 534 | 535 | // Count by mime type 536 | const mimeType = resource.mimeType || ""; 537 | if (mimeType.includes("javascript") || resource.url.endsWith(".js")) { 538 | jsCount++; 539 | } else if (mimeType.includes("css") || resource.url.endsWith(".css")) { 540 | cssCount++; 541 | } else if ( 542 | mimeType.includes("image") || 543 | /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(resource.url) 544 | ) { 545 | imgCount++; 546 | } else if ( 547 | mimeType.includes("font") || 548 | /\.(woff|woff2|ttf|otf|eot)$/i.test(resource.url) 549 | ) { 550 | fontCount++; 551 | } else { 552 | otherCount++; 553 | } 554 | }); 555 | 556 | // Calculate third-party size 557 | let thirdPartySizeKb = 0; 558 | if (thirdPartyAudit && thirdPartyAudit.details) { 559 | const thirdPartyDetails = thirdPartyAudit.details as any; 560 | if (thirdPartyDetails.items && Array.isArray(thirdPartyDetails.items)) { 561 | thirdPartyDetails.items.forEach((item: any) => { 562 | if (item.transferSize) { 563 | thirdPartySizeKb += Math.round(item.transferSize / 1024); 564 | } 565 | }); 566 | } 567 | } 568 | 569 | // Get main thread blocking time 570 | let mainThreadBlockingTimeMs = 0; 571 | if (mainThreadWork && mainThreadWork.numericValue) { 572 | mainThreadBlockingTimeMs = Math.round(mainThreadWork.numericValue); 573 | } 574 | 575 | // Create page stats object 576 | page_stats = { 577 | total_size_kb: totalSizeKb, 578 | total_requests: totalRequests, 579 | resource_counts: { 580 | js: jsCount, 581 | css: cssCount, 582 | img: imgCount, 583 | font: fontCount, 584 | other: otherCount, 585 | }, 586 | third_party_size_kb: thirdPartySizeKb, 587 | main_thread_blocking_time_ms: mainThreadBlockingTimeMs, 588 | }; 589 | } 590 | } 591 | 592 | // Generate prioritized recommendations 593 | const prioritized_recommendations: string[] = []; 594 | 595 | // Add key recommendations based on failed audits with high impact 596 | if ( 597 | audits["render-blocking-resources"] && 598 | audits["render-blocking-resources"].score !== null && 599 | audits["render-blocking-resources"].score === 0 600 | ) { 601 | prioritized_recommendations.push("Eliminate render-blocking resources"); 602 | } 603 | 604 | if ( 605 | audits["uses-responsive-images"] && 606 | audits["uses-responsive-images"].score !== null && 607 | audits["uses-responsive-images"].score === 0 608 | ) { 609 | prioritized_recommendations.push("Properly size images"); 610 | } 611 | 612 | if ( 613 | audits["uses-optimized-images"] && 614 | audits["uses-optimized-images"].score !== null && 615 | audits["uses-optimized-images"].score === 0 616 | ) { 617 | prioritized_recommendations.push("Efficiently encode images"); 618 | } 619 | 620 | if ( 621 | audits["uses-text-compression"] && 622 | audits["uses-text-compression"].score !== null && 623 | audits["uses-text-compression"].score === 0 624 | ) { 625 | prioritized_recommendations.push("Enable text compression"); 626 | } 627 | 628 | if ( 629 | audits["uses-http2"] && 630 | audits["uses-http2"].score !== null && 631 | audits["uses-http2"].score === 0 632 | ) { 633 | prioritized_recommendations.push("Use HTTP/2"); 634 | } 635 | 636 | // Add more specific recommendations based on Core Web Vitals 637 | if ( 638 | audits["largest-contentful-paint"] && 639 | audits["largest-contentful-paint"].score !== null && 640 | audits["largest-contentful-paint"].score < 0.5 641 | ) { 642 | prioritized_recommendations.push("Improve Largest Contentful Paint (LCP)"); 643 | } 644 | 645 | if ( 646 | audits["cumulative-layout-shift"] && 647 | audits["cumulative-layout-shift"].score !== null && 648 | audits["cumulative-layout-shift"].score < 0.5 649 | ) { 650 | prioritized_recommendations.push("Reduce layout shifts (CLS)"); 651 | } 652 | 653 | if ( 654 | audits["total-blocking-time"] && 655 | audits["total-blocking-time"].score !== null && 656 | audits["total-blocking-time"].score < 0.5 657 | ) { 658 | prioritized_recommendations.push("Reduce JavaScript execution time"); 659 | } 660 | 661 | // Create the performance report content 662 | const reportContent: PerformanceReportContent = { 663 | score, 664 | audit_counts, 665 | metrics, 666 | opportunities, 667 | page_stats, 668 | prioritized_recommendations: 669 | prioritized_recommendations.length > 0 670 | ? prioritized_recommendations 671 | : undefined, 672 | }; 673 | 674 | // Return the full report following the LighthouseReport interface 675 | return { 676 | metadata, 677 | report: reportContent, 678 | }; 679 | }; 680 | ```