This is page 1 of 3. Use http://codebase.md/weotzi/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 ├── docs │ ├── mcp-docs.md │ └── mcp.md ├── LICENSE └── README.md ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | dist 3 | .port 4 | .DS_Store 5 | ``` -------------------------------------------------------------------------------- /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 | ## Roadmap 10 | 11 | Check out our project roadmap here: [Github Roadmap / Project Board](https://github.com/orgs/AgentDeskAI/projects/1/views/1) 12 | 13 | ## Updates 14 | 15 | v1.2.0 is out! Here's a quick breakdown of the update: 16 | - 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!) 17 | - Integrated a suite of SEO, performance, accessibility, and best practice analysis tools via Lighthouse 18 | - Implemented a NextJS specific prompt used to improve SEO for a NextJS application 19 | - Added Debugger Mode as a tool which executes all debugging tools in a particular sequence, along with a prompt to improve reasoning 20 | - Added Audit Mode as a tool to execute all auditing tools in a particular sequence 21 | - Resolved Windows connectivity issues 22 | - Improved networking between BrowserTools server, extension and MCP server with host/port auto-discovery, auto-reconnect, and graceful shutdown mechanisms 23 | - Added ability to more easily exit out of the Browser Tools server with Ctrl+C 24 | 25 | 26 | 27 | Please make sure to update the version in your IDE / MCP client as so: 28 | `npx @agentdeskai/[email protected]` 29 | 30 | Also make sure to download the latest version of the chrome extension here: 31 | [v1.2.0 BrowserToolsMCP Chrome Extension](https://github.com/AgentDeskAI/browser-tools-mcp/releases/download/v1.2.0/BrowserTools-1.2.0-extension.zip) 32 | 33 | From there you can run the local node server like so: 34 | `npx @agentdeskai/[email protected]` 35 | 36 | 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. 37 | 38 | And once you've opened your chrome dev tools, logs should be getting sent to your server 🦾 39 | 40 | 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) 41 | 42 | ## Full Update Notes: 43 | 44 | 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: 45 | 46 | - Evaluate pages for WCAG compliance 47 | - Identify performance bottlenecks 48 | - Flag on-page SEO issues 49 | - Check adherence to web development best practices 50 | - Review NextJS specific issues with SEO 51 | 52 | ...all without leaving your IDE 🎉 53 | 54 | --- 55 | 56 | ## 🔑 Key Additions 57 | 58 | | Audit Type | Description | 59 | | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | 60 | | **Accessibility** | WCAG-compliant checks for color contrast, missing alt text, keyboard navigation traps, ARIA attributes, and more. | 61 | | **Performance** | Lighthouse-driven analysis of render-blocking resources, excessive DOM size, unoptimized images, and other factors affecting page speed. | 62 | | **SEO** | Evaluates on-page SEO factors (like metadata, headings, and link structure) and suggests improvements for better search visibility. | 63 | | **Best Practices** | Checks for general best practices in web development. | 64 | | **NextJS Audit** | Injects a prompt used to perform a NextJS audit. | 65 | | **Audit Mode** | Runs all auditing tools in a sequence. | 66 | | **Debugger Mode** | Runs all debugging tools in a sequence. | 67 | 68 | --- 69 | 70 | ## 🛠️ Using Audit Tools 71 | 72 | ### ✅ **Before You Start** 73 | 74 | Ensure you have: 75 | 76 | - An **active tab** in your browser 77 | - The **BrowserTools extension enabled** 78 | 79 | ### ▶️ **Running Audits** 80 | 81 | **Headless Browser Automation**: 82 | 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. 83 | 84 | The headless browser instance remains active for **60 seconds** after the last audit call to efficiently handle consecutive audit requests. 85 | 86 | **Structured Results**: 87 | 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. 88 | 89 | The MCP server provides tools to run audits on the current page. Here are example queries you can use to trigger them: 90 | 91 | #### Accessibility Audit (`runAccessibilityAudit`) 92 | 93 | Ensures the page meets accessibility standards like WCAG. 94 | 95 | > **Example Queries:** 96 | > 97 | > - "Are there any accessibility issues on this page?" 98 | > - "Run an accessibility audit." 99 | > - "Check if this page meets WCAG standards." 100 | 101 | #### Performance Audit (`runPerformanceAudit`) 102 | 103 | Identifies performance bottlenecks and loading issues. 104 | 105 | > **Example Queries:** 106 | > 107 | > - "Why is this page loading so slowly?" 108 | > - "Check the performance of this page." 109 | > - "Run a performance audit." 110 | 111 | #### SEO Audit (`runSEOAudit`) 112 | 113 | Evaluates how well the page is optimized for search engines. 114 | 115 | > **Example Queries:** 116 | > 117 | > - "How can I improve SEO for this page?" 118 | > - "Run an SEO audit." 119 | > - "Check SEO on this page." 120 | 121 | #### Best Practices Audit (`runBestPracticesAudit`) 122 | 123 | Checks for general best practices in web development. 124 | 125 | > **Example Queries:** 126 | > 127 | > - "Run a best practices audit." 128 | > - "Check best practices on this page." 129 | > - "Are there any best practices issues on this page?" 130 | 131 | #### Audit Mode (`runAuditMode`) 132 | 133 | Runs all audits in a particular sequence. Will run a NextJS audit if the framework is detected. 134 | 135 | > **Example Queries:** 136 | > 137 | > - "Run audit mode." 138 | > - "Enter audit mode." 139 | 140 | #### NextJS Audits (`runNextJSAudit`) 141 | 142 | Checks for best practices and SEO improvements for NextJS applications 143 | 144 | > **Example Queries:** 145 | > 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 | > 156 | > - "Enter debugger mode." 157 | 158 | ## Architecture 159 | 160 | There are three core components all used to capture and analyze browser data: 161 | 162 | 1. **Chrome Extension**: A browser extension that captures screenshots, console logs, network activity and DOM elements. 163 | 2. **Node Server**: An intermediary server that facilitates communication between the Chrome extension and any instance of an MCP server. 164 | 3. **MCP Server**: A Model Context Protocol server that provides standardized tools for AI clients to interact with the browser. 165 | 166 | ``` 167 | ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ ┌─────────────┐ 168 | │ MCP Client │ ──► │ MCP Server │ ──► │ Node Server │ ──► │ Chrome │ 169 | │ (e.g. │ ◄── │ (Protocol │ ◄── │ (Middleware) │ ◄── │ Extension │ 170 | │ Cursor) │ │ Handler) │ │ │ │ │ 171 | └─────────────┘ └──────────────┘ └───────────────┘ └─────────────┘ 172 | ``` 173 | 174 | Model Context Protocol (MCP) is a capability supported by Anthropic AI models that 175 | allow you to create custom tools for any compatible client. MCP clients like Claude 176 | Desktop, Cursor, Cline or Zed can run an MCP server which "teaches" these clients 177 | about a new tool that they can use. 178 | 179 | 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. 180 | 181 | All consumers of the BrowserTools MCP Server interface with the same NodeJS API and Chrome extension. 182 | 183 | #### Chrome Extension 184 | 185 | - Monitors XHR requests/responses and console logs 186 | - Tracks selected DOM elements 187 | - Sends all logs and current element to the BrowserTools Connector 188 | - Connects to Websocket server to capture/send screenshots 189 | - Allows user to configure token/truncation limits + screenshot folder path 190 | 191 | #### Node Server 192 | 193 | - Acts as middleware between the Chrome extension and MCP server 194 | - Receives logs and currently selected element from Chrome extension 195 | - Processes requests from MCP server to capture logs, screenshot or current element 196 | - Sends Websocket command to the Chrome extension for capturing a screenshot 197 | - Intelligently truncates strings and # of duplicate objects in logs to avoid token limits 198 | - Removes cookies and sensitive headers to avoid sending to LLMs in MCP clients 199 | 200 | #### MCP Server 201 | 202 | - Implements the Model Context Protocol 203 | - Provides standardized tools for AI clients 204 | - Compatible with various MCP clients (Cursor, Cline, Zed, Claude Desktop, etc.) 205 | 206 | ## Installation 207 | 208 | Installation steps can be found in our documentation: 209 | 210 | - [BrowserTools MCP Docs](https://browsertools.agentdesk.ai/) 211 | 212 | ## Usage 213 | 214 | Once installed and configured, the system allows any compatible MCP client to: 215 | 216 | - Monitor browser console output 217 | - Capture network traffic 218 | - Take screenshots 219 | - Analyze selected elements 220 | - Wipe logs stored in our MCP server 221 | - Run accessibility, performance, SEO, and best practices audits 222 | 223 | ## Compatibility 224 | 225 | - Works with any MCP-compatible client 226 | - Primarily designed for Cursor IDE integration 227 | - Supports other AI editors and MCP clients 228 | ``` -------------------------------------------------------------------------------- /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 | } ``` -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /docs/mcp-docs.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Resources 2 | 3 | Expose data and content from your servers to LLMs 4 | 5 | Resources are a core primitive in the Model Context Protocol (MCP) that allow servers to expose data and content that can be read by clients and used as context for LLM interactions. 6 | 7 | Resources are designed to be application-controlled, meaning that the client application can decide how and when they should be used. Different MCP clients may handle resources differently. For example: 8 | 9 | Claude Desktop currently requires users to explicitly select resources before they can be used 10 | Other clients might automatically select resources based on heuristics 11 | Some implementations may even allow the AI model itself to determine which resources to use 12 | Server authors should be prepared to handle any of these interaction patterns when implementing resource support. In order to expose data to models automatically, server authors should use a model-controlled primitive such as Tools. 13 | 14 | 15 | Overview 16 | Resources represent any kind of data that an MCP server wants to make available to clients. This can include: 17 | 18 | File contents 19 | Database records 20 | API responses 21 | Live system data 22 | Screenshots and images 23 | Log files 24 | And more 25 | Each resource is identified by a unique URI and can contain either text or binary data. 26 | 27 | 28 | Resource URIs 29 | Resources are identified using URIs that follow this format: 30 | 31 | [protocol]://[host]/[path] 32 | For example: 33 | 34 | file:///home/user/documents/report.pdf 35 | postgres://database/customers/schema 36 | screen://localhost/display1 37 | The protocol and path structure is defined by the MCP server implementation. Servers can define their own custom URI schemes. 38 | 39 | 40 | Resource types 41 | Resources can contain two types of content: 42 | 43 | 44 | Text resources 45 | Text resources contain UTF-8 encoded text data. These are suitable for: 46 | 47 | Source code 48 | Configuration files 49 | Log files 50 | JSON/XML data 51 | Plain text 52 | 53 | Binary resources 54 | Binary resources contain raw binary data encoded in base64. These are suitable for: 55 | 56 | Images 57 | PDFs 58 | Audio files 59 | Video files 60 | Other non-text formats 61 | 62 | Resource discovery 63 | Clients can discover available resources through two main methods: 64 | 65 | 66 | Direct resources 67 | Servers expose a list of concrete resources via the resources/list endpoint. Each resource includes: 68 | 69 | { 70 | uri: string; // Unique identifier for the resource 71 | name: string; // Human-readable name 72 | description?: string; // Optional description 73 | mimeType?: string; // Optional MIME type 74 | } 75 | 76 | Resource templates 77 | For dynamic resources, servers can expose URI templates that clients can use to construct valid resource URIs: 78 | 79 | { 80 | uriTemplate: string; // URI template following RFC 6570 81 | name: string; // Human-readable name for this type 82 | description?: string; // Optional description 83 | mimeType?: string; // Optional MIME type for all matching resources 84 | } 85 | 86 | Reading resources 87 | To read a resource, clients make a resources/read request with the resource URI. 88 | 89 | The server responds with a list of resource contents: 90 | 91 | { 92 | contents: [ 93 | { 94 | uri: string; // The URI of the resource 95 | mimeType?: string; // Optional MIME type 96 | 97 | // One of: 98 | text?: string; // For text resources 99 | blob?: string; // For binary resources (base64 encoded) 100 | } 101 | 102 | ] 103 | } 104 | Servers may return multiple resources in response to one resources/read request. This could be used, for example, to return a list of files inside a directory when the directory is read. 105 | 106 | 107 | Resource updates 108 | MCP supports real-time updates for resources through two mechanisms: 109 | 110 | 111 | List changes 112 | Servers can notify clients when their list of available resources changes via the notifications/resources/list_changed notification. 113 | 114 | 115 | Content changes 116 | Clients can subscribe to updates for specific resources: 117 | 118 | Client sends resources/subscribe with resource URI 119 | Server sends notifications/resources/updated when the resource changes 120 | Client can fetch latest content with resources/read 121 | Client can unsubscribe with resources/unsubscribe 122 | 123 | Example implementation 124 | Here’s a simple example of implementing resource support in an MCP server: 125 | 126 | ## Prompts 127 | 128 | Create reusable prompt templates and workflows 129 | 130 | Prompts enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. They provide a powerful way to standardize and share common LLM interactions. 131 | 132 | Prompts are designed to be user-controlled, meaning they are exposed from servers to clients with the intention of the user being able to explicitly select them for use. 133 | 134 | 135 | Overview 136 | Prompts in MCP are predefined templates that can: 137 | 138 | Accept dynamic arguments 139 | Include context from resources 140 | Chain multiple interactions 141 | Guide specific workflows 142 | Surface as UI elements (like slash commands) 143 | 144 | Prompt structure 145 | Each prompt is defined with: 146 | 147 | { 148 | name: string; // Unique identifier for the prompt 149 | description?: string; // Human-readable description 150 | arguments?: [ // Optional list of arguments 151 | { 152 | name: string; // Argument identifier 153 | description?: string; // Argument description 154 | required?: boolean; // Whether argument is required 155 | } 156 | ] 157 | } 158 | 159 | Discovering prompts 160 | Clients can discover available prompts through the prompts/list endpoint: 161 | 162 | // Request 163 | { 164 | method: "prompts/list" 165 | } 166 | 167 | // Response 168 | { 169 | prompts: [ 170 | { 171 | name: "analyze-code", 172 | description: "Analyze code for potential improvements", 173 | arguments: [ 174 | { 175 | name: "language", 176 | description: "Programming language", 177 | required: true 178 | } 179 | ] 180 | } 181 | ] 182 | } 183 | 184 | Using prompts 185 | To use a prompt, clients make a prompts/get request: 186 | 187 | // Request 188 | { 189 | method: "prompts/get", 190 | params: { 191 | name: "analyze-code", 192 | arguments: { 193 | language: "python" 194 | } 195 | } 196 | } 197 | 198 | // Response 199 | { 200 | description: "Analyze Python code for potential improvements", 201 | messages: [ 202 | { 203 | role: "user", 204 | content: { 205 | type: "text", 206 | text: "Please analyze the following Python code for potential improvements:\n\n`python\ndef calculate_sum(numbers):\n total = 0\n for num in numbers:\n total = total + num\n return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n`" 207 | } 208 | } 209 | ] 210 | } 211 | 212 | Dynamic prompts 213 | Prompts can be dynamic and include: 214 | 215 | 216 | Embedded resource context 217 | 218 | { 219 | "name": "analyze-project", 220 | "description": "Analyze project logs and code", 221 | "arguments": [ 222 | { 223 | "name": "timeframe", 224 | "description": "Time period to analyze logs", 225 | "required": true 226 | }, 227 | { 228 | "name": "fileUri", 229 | "description": "URI of code file to review", 230 | "required": true 231 | } 232 | ] 233 | } 234 | When handling the prompts/get request: 235 | 236 | { 237 | "messages": [ 238 | { 239 | "role": "user", 240 | "content": { 241 | "type": "text", 242 | "text": "Analyze these system logs and the code file for any issues:" 243 | } 244 | }, 245 | { 246 | "role": "user", 247 | "content": { 248 | "type": "resource", 249 | "resource": { 250 | "uri": "logs://recent?timeframe=1h", 251 | "text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded", 252 | "mimeType": "text/plain" 253 | } 254 | } 255 | }, 256 | { 257 | "role": "user", 258 | "content": { 259 | "type": "resource", 260 | "resource": { 261 | "uri": "file:///path/to/code.py", 262 | "text": "def connect_to_service(timeout=30):\n retries = 3\n for attempt in range(retries):\n try:\n return establish_connection(timeout)\n except TimeoutError:\n if attempt == retries - 1:\n raise\n time.sleep(5)\n\ndef establish_connection(timeout):\n # Connection implementation\n pass", 263 | "mimeType": "text/x-python" 264 | } 265 | } 266 | } 267 | ] 268 | } 269 | 270 | Multi-step workflows 271 | 272 | const debugWorkflow = { 273 | name: "debug-error", 274 | async getMessages(error: string) { 275 | return [ 276 | { 277 | role: "user", 278 | content: { 279 | type: "text", 280 | text: `Here's an error I'm seeing: ${error}` 281 | } 282 | }, 283 | { 284 | role: "assistant", 285 | content: { 286 | type: "text", 287 | text: "I'll help analyze this error. What have you tried so far?" 288 | } 289 | }, 290 | { 291 | role: "user", 292 | content: { 293 | type: "text", 294 | text: "I've tried restarting the service, but the error persists." 295 | } 296 | } 297 | ]; 298 | } 299 | }; 300 | 301 | Example implementation 302 | Here’s a complete example of implementing prompts in an MCP server: 303 | 304 | TypeScript 305 | Python 306 | 307 | import { Server } from "@modelcontextprotocol/sdk/server"; 308 | import { 309 | ListPromptsRequestSchema, 310 | GetPromptRequestSchema 311 | } from "@modelcontextprotocol/sdk/types"; 312 | 313 | const PROMPTS = { 314 | "git-commit": { 315 | name: "git-commit", 316 | description: "Generate a Git commit message", 317 | arguments: [ 318 | { 319 | name: "changes", 320 | description: "Git diff or description of changes", 321 | required: true 322 | } 323 | ] 324 | }, 325 | "explain-code": { 326 | name: "explain-code", 327 | description: "Explain how code works", 328 | arguments: [ 329 | { 330 | name: "code", 331 | description: "Code to explain", 332 | required: true 333 | }, 334 | { 335 | name: "language", 336 | description: "Programming language", 337 | required: false 338 | } 339 | ] 340 | } 341 | }; 342 | 343 | const server = new Server({ 344 | name: "example-prompts-server", 345 | version: "1.0.0" 346 | }, { 347 | capabilities: { 348 | prompts: {} 349 | } 350 | }); 351 | 352 | // List available prompts 353 | server.setRequestHandler(ListPromptsRequestSchema, async () => { 354 | return { 355 | prompts: Object.values(PROMPTS) 356 | }; 357 | }); 358 | 359 | // Get specific prompt 360 | server.setRequestHandler(GetPromptRequestSchema, async (request) => { 361 | const prompt = PROMPTS[request.params.name]; 362 | if (!prompt) { 363 | throw new Error(`Prompt not found: ${request.params.name}`); 364 | } 365 | 366 | if (request.params.name === "git-commit") { 367 | return { 368 | messages: [ 369 | { 370 | role: "user", 371 | content: { 372 | type: "text", 373 | text: `Generate a concise but descriptive commit message for these changes:\n\n${request.params.arguments?.changes}` 374 | } 375 | } 376 | ] 377 | }; 378 | } 379 | 380 | if (request.params.name === "explain-code") { 381 | const language = request.params.arguments?.language || "Unknown"; 382 | return { 383 | messages: [ 384 | { 385 | role: "user", 386 | content: { 387 | type: "text", 388 | text: `Explain how this ${language} code works:\n\n${request.params.arguments?.code}` 389 | } 390 | } 391 | ] 392 | }; 393 | } 394 | 395 | throw new Error("Prompt implementation not found"); 396 | }); 397 | 398 | Best practices 399 | When implementing prompts: 400 | 401 | Use clear, descriptive prompt names 402 | Provide detailed descriptions for prompts and arguments 403 | Validate all required arguments 404 | Handle missing arguments gracefully 405 | Consider versioning for prompt templates 406 | Cache dynamic content when appropriate 407 | Implement error handling 408 | Document expected argument formats 409 | Consider prompt composability 410 | Test prompts with various inputs 411 | 412 | UI integration 413 | Prompts can be surfaced in client UIs as: 414 | 415 | Slash commands 416 | Quick actions 417 | Context menu items 418 | Command palette entries 419 | Guided workflows 420 | Interactive forms 421 | 422 | Updates and changes 423 | Servers can notify clients about prompt changes: 424 | 425 | Server capability: prompts.listChanged 426 | Notification: notifications/prompts/list_changed 427 | Client re-fetches prompt list 428 | 429 | Security considerations 430 | When implementing prompts: 431 | 432 | Validate all arguments 433 | Sanitize user input 434 | Consider rate limiting 435 | Implement access controls 436 | Audit prompt usage 437 | Handle sensitive data appropriately 438 | Validate generated content 439 | Implement timeouts 440 | Consider prompt injection risks 441 | Document security requirements 442 | 443 | ## Tools 444 | 445 | Tools 446 | Enable LLMs to perform actions through your server 447 | 448 | Tools are a powerful primitive in the Model Context Protocol (MCP) that enable servers to expose executable functionality to clients. Through tools, LLMs can interact with external systems, perform computations, and take actions in the real world. 449 | 450 | Tools are designed to be model-controlled, meaning that tools are exposed from servers to clients with the intention of the AI model being able to automatically invoke them (with a human in the loop to grant approval). 451 | 452 | 453 | Overview 454 | Tools in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. Key aspects of tools include: 455 | 456 | Discovery: Clients can list available tools through the tools/list endpoint 457 | Invocation: Tools are called using the tools/call endpoint, where servers perform the requested operation and return results 458 | Flexibility: Tools can range from simple calculations to complex API interactions 459 | Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems. 460 | 461 | 462 | Tool definition structure 463 | Each tool is defined with the following structure: 464 | 465 | { 466 | name: string; // Unique identifier for the tool 467 | description?: string; // Human-readable description 468 | inputSchema: { // JSON Schema for the tool's parameters 469 | type: "object", 470 | properties: { ... } // Tool-specific parameters 471 | } 472 | } 473 | 474 | Implementing tools 475 | Here’s an example of implementing a basic tool in an MCP server: 476 | 477 | TypeScript 478 | Python 479 | 480 | const server = new Server({ 481 | name: "example-server", 482 | version: "1.0.0" 483 | }, { 484 | capabilities: { 485 | tools: {} 486 | } 487 | }); 488 | 489 | // Define available tools 490 | server.setRequestHandler(ListToolsRequestSchema, async () => { 491 | return { 492 | tools: [{ 493 | name: "calculate_sum", 494 | description: "Add two numbers together", 495 | inputSchema: { 496 | type: "object", 497 | properties: { 498 | a: { type: "number" }, 499 | b: { type: "number" } 500 | }, 501 | required: ["a", "b"] 502 | } 503 | }] 504 | }; 505 | }); 506 | 507 | // Handle tool execution 508 | server.setRequestHandler(CallToolRequestSchema, async (request) => { 509 | if (request.params.name === "calculate_sum") { 510 | const { a, b } = request.params.arguments; 511 | return { 512 | content: [ 513 | { 514 | type: "text", 515 | text: String(a + b) 516 | } 517 | ] 518 | }; 519 | } 520 | throw new Error("Tool not found"); 521 | }); 522 | 523 | Example tool patterns 524 | Here are some examples of types of tools that a server could provide: 525 | 526 | 527 | System operations 528 | Tools that interact with the local system: 529 | 530 | { 531 | name: "execute_command", 532 | description: "Run a shell command", 533 | inputSchema: { 534 | type: "object", 535 | properties: { 536 | command: { type: "string" }, 537 | args: { type: "array", items: { type: "string" } } 538 | } 539 | } 540 | } 541 | 542 | API integrations 543 | Tools that wrap external APIs: 544 | 545 | { 546 | name: "github_create_issue", 547 | description: "Create a GitHub issue", 548 | inputSchema: { 549 | type: "object", 550 | properties: { 551 | title: { type: "string" }, 552 | body: { type: "string" }, 553 | labels: { type: "array", items: { type: "string" } } 554 | } 555 | } 556 | } 557 | 558 | Data processing 559 | Tools that transform or analyze data: 560 | 561 | { 562 | name: "analyze_csv", 563 | description: "Analyze a CSV file", 564 | inputSchema: { 565 | type: "object", 566 | properties: { 567 | filepath: { type: "string" }, 568 | operations: { 569 | type: "array", 570 | items: { 571 | enum: ["sum", "average", "count"] 572 | } 573 | } 574 | } 575 | } 576 | } 577 | 578 | Best practices 579 | When implementing tools: 580 | 581 | Provide clear, descriptive names and descriptions 582 | Use detailed JSON Schema definitions for parameters 583 | Include examples in tool descriptions to demonstrate how the model should use them 584 | Implement proper error handling and validation 585 | Use progress reporting for long operations 586 | Keep tool operations focused and atomic 587 | Document expected return value structures 588 | Implement proper timeouts 589 | Consider rate limiting for resource-intensive operations 590 | Log tool usage for debugging and monitoring 591 | 592 | Security considerations 593 | When exposing tools: 594 | 595 | 596 | Input validation 597 | Validate all parameters against the schema 598 | Sanitize file paths and system commands 599 | Validate URLs and external identifiers 600 | Check parameter sizes and ranges 601 | Prevent command injection 602 | 603 | Access control 604 | Implement authentication where needed 605 | Use appropriate authorization checks 606 | Audit tool usage 607 | Rate limit requests 608 | Monitor for abuse 609 | 610 | Error handling 611 | Don’t expose internal errors to clients 612 | Log security-relevant errors 613 | Handle timeouts appropriately 614 | Clean up resources after errors 615 | Validate return values 616 | 617 | Tool discovery and updates 618 | MCP supports dynamic tool discovery: 619 | 620 | Clients can list available tools at any time 621 | Servers can notify clients when tools change using notifications/tools/list_changed 622 | Tools can be added or removed during runtime 623 | Tool definitions can be updated (though this should be done carefully) 624 | 625 | Error handling 626 | Tool errors should be reported within the result object, not as MCP protocol-level errors. This allows the LLM to see and potentially handle the error. When a tool encounters an error: 627 | 628 | Set isError to true in the result 629 | Include error details in the content array 630 | Here’s an example of proper error handling for tools: 631 | 632 | TypeScript 633 | Python 634 | 635 | try { 636 | // Tool operation 637 | const result = performOperation(); 638 | return { 639 | content: [ 640 | { 641 | type: "text", 642 | text: `Operation successful: ${result}` 643 | } 644 | ] 645 | }; 646 | } catch (error) { 647 | return { 648 | isError: true, 649 | content: [ 650 | { 651 | type: "text", 652 | text: `Error: ${error.message}` 653 | } 654 | ] 655 | }; 656 | } 657 | This approach allows the LLM to see that an error occurred and potentially take corrective action or request human intervention. 658 | 659 | 660 | Testing tools 661 | A comprehensive testing strategy for MCP tools should cover: 662 | 663 | Functional testing: Verify tools execute correctly with valid inputs and handle invalid inputs appropriately 664 | Integration testing: Test tool interaction with external systems using both real and mocked dependencies 665 | Security testing: Validate authentication, authorization, input sanitization, and rate limiting 666 | Performance testing: Check behavior under load, timeout handling, and resource cleanup 667 | Error handling: Ensure tools properly report errors through the MCP protocol and clean up resources 668 | 669 | ## Sampling 670 | 671 | Sampling 672 | Let your servers request completions from LLMs 673 | 674 | Sampling is a powerful MCP feature that allows servers to request LLM completions through the client, enabling sophisticated agentic behaviors while maintaining security and privacy. 675 | 676 | This feature of MCP is not yet supported in the Claude Desktop client. 677 | 678 | 679 | How sampling works 680 | The sampling flow follows these steps: 681 | 682 | Server sends a sampling/createMessage request to the client 683 | Client reviews the request and can modify it 684 | Client samples from an LLM 685 | Client reviews the completion 686 | Client returns the result to the server 687 | This human-in-the-loop design ensures users maintain control over what the LLM sees and generates. 688 | 689 | 690 | Message format 691 | Sampling requests use a standardized message format: 692 | 693 | { 694 | messages: [ 695 | { 696 | role: "user" | "assistant", 697 | content: { 698 | type: "text" | "image", 699 | 700 | // For text: 701 | text?: string, 702 | 703 | // For images: 704 | data?: string, // base64 encoded 705 | mimeType?: string 706 | } 707 | } 708 | 709 | ], 710 | modelPreferences?: { 711 | hints?: [{ 712 | name?: string // Suggested model name/family 713 | }], 714 | costPriority?: number, // 0-1, importance of minimizing cost 715 | speedPriority?: number, // 0-1, importance of low latency 716 | intelligencePriority?: number // 0-1, importance of capabilities 717 | }, 718 | systemPrompt?: string, 719 | includeContext?: "none" | "thisServer" | "allServers", 720 | temperature?: number, 721 | maxTokens: number, 722 | stopSequences?: string[], 723 | metadata?: Record<string, unknown> 724 | } 725 | 726 | Request parameters 727 | 728 | Messages 729 | The messages array contains the conversation history to send to the LLM. Each message has: 730 | 731 | role: Either “user” or “assistant” 732 | content: The message content, which can be: 733 | Text content with a text field 734 | Image content with data (base64) and mimeType fields 735 | 736 | Model preferences 737 | The modelPreferences object allows servers to specify their model selection preferences: 738 | 739 | hints: Array of model name suggestions that clients can use to select an appropriate model: 740 | 741 | name: String that can match full or partial model names (e.g. “claude-3”, “sonnet”) 742 | Clients may map hints to equivalent models from different providers 743 | Multiple hints are evaluated in preference order 744 | Priority values (0-1 normalized): 745 | 746 | costPriority: Importance of minimizing costs 747 | speedPriority: Importance of low latency response 748 | intelligencePriority: Importance of advanced model capabilities 749 | Clients make the final model selection based on these preferences and their available models. 750 | 751 | 752 | System prompt 753 | An optional systemPrompt field allows servers to request a specific system prompt. The client may modify or ignore this. 754 | 755 | 756 | Context inclusion 757 | The includeContext parameter specifies what MCP context to include: 758 | 759 | "none": No additional context 760 | "thisServer": Include context from the requesting server 761 | "allServers": Include context from all connected MCP servers 762 | The client controls what context is actually included. 763 | 764 | 765 | Sampling parameters 766 | Fine-tune the LLM sampling with: 767 | 768 | temperature: Controls randomness (0.0 to 1.0) 769 | maxTokens: Maximum tokens to generate 770 | stopSequences: Array of sequences that stop generation 771 | metadata: Additional provider-specific parameters 772 | 773 | Response format 774 | The client returns a completion result: 775 | 776 | { 777 | model: string, // Name of the model used 778 | stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string, 779 | role: "user" | "assistant", 780 | content: { 781 | type: "text" | "image", 782 | text?: string, 783 | data?: string, 784 | mimeType?: string 785 | } 786 | } 787 | 788 | Example request 789 | Here’s an example of requesting sampling from a client: 790 | 791 | { 792 | "method": "sampling/createMessage", 793 | "params": { 794 | "messages": [ 795 | { 796 | "role": "user", 797 | "content": { 798 | "type": "text", 799 | "text": "What files are in the current directory?" 800 | } 801 | } 802 | ], 803 | "systemPrompt": "You are a helpful file system assistant.", 804 | "includeContext": "thisServer", 805 | "maxTokens": 100 806 | } 807 | } 808 | 809 | Best practices 810 | When implementing sampling: 811 | 812 | Always provide clear, well-structured prompts 813 | Handle both text and image content appropriately 814 | Set reasonable token limits 815 | Include relevant context through includeContext 816 | Validate responses before using them 817 | Handle errors gracefully 818 | Consider rate limiting sampling requests 819 | Document expected sampling behavior 820 | Test with various model parameters 821 | Monitor sampling costs 822 | 823 | Human in the loop controls 824 | Sampling is designed with human oversight in mind: 825 | 826 | 827 | For prompts 828 | Clients should show users the proposed prompt 829 | Users should be able to modify or reject prompts 830 | System prompts can be filtered or modified 831 | Context inclusion is controlled by the client 832 | 833 | For completions 834 | Clients should show users the completion 835 | Users should be able to modify or reject completions 836 | Clients can filter or modify completions 837 | Users control which model is used 838 | 839 | Security considerations 840 | When implementing sampling: 841 | 842 | Validate all message content 843 | Sanitize sensitive information 844 | Implement appropriate rate limits 845 | Monitor sampling usage 846 | Encrypt data in transit 847 | Handle user data privacy 848 | Audit sampling requests 849 | Control cost exposure 850 | Implement timeouts 851 | Handle model errors gracefully 852 | 853 | Common patterns 854 | 855 | Agentic workflows 856 | Sampling enables agentic patterns like: 857 | 858 | Reading and analyzing resources 859 | Making decisions based on context 860 | Generating structured data 861 | Handling multi-step tasks 862 | Providing interactive assistance 863 | 864 | Context management 865 | Best practices for context: 866 | 867 | Request minimal necessary context 868 | Structure context clearly 869 | Handle context size limits 870 | Update context as needed 871 | Clean up stale context 872 | 873 | Error handling 874 | Robust error handling should: 875 | 876 | Catch sampling failures 877 | Handle timeout errors 878 | Manage rate limits 879 | Validate responses 880 | Provide fallback behaviors 881 | Log errors appropriately 882 | 883 | Limitations 884 | Be aware of these limitations: 885 | 886 | Sampling depends on client capabilities 887 | Users control sampling behavior 888 | Context size has limits 889 | Rate limits may apply 890 | Costs should be considered 891 | Model availability varies 892 | Response times vary 893 | Not all content types supported 894 | 895 | ## Roots 896 | 897 | Roots 898 | Understanding roots in MCP 899 | 900 | Roots are a concept in MCP that define the boundaries where servers can operate. They provide a way for clients to inform servers about relevant resources and their locations. 901 | 902 | 903 | What are Roots? 904 | A root is a URI that a client suggests a server should focus on. When a client connects to a server, it declares which roots the server should work with. While primarily used for filesystem paths, roots can be any valid URI including HTTP URLs. 905 | 906 | For example, roots could be: 907 | 908 | file:///home/user/projects/myapp 909 | https://api.example.com/v1 910 | 911 | Why Use Roots? 912 | Roots serve several important purposes: 913 | 914 | Guidance: They inform servers about relevant resources and locations 915 | Clarity: Roots make it clear which resources are part of your workspace 916 | Organization: Multiple roots let you work with different resources simultaneously 917 | 918 | How Roots Work 919 | When a client supports roots, it: 920 | 921 | Declares the roots capability during connection 922 | Provides a list of suggested roots to the server 923 | Notifies the server when roots change (if supported) 924 | While roots are informational and not strictly enforcing, servers should: 925 | 926 | Respect the provided roots 927 | Use root URIs to locate and access resources 928 | Prioritize operations within root boundaries 929 | 930 | Common Use Cases 931 | Roots are commonly used to define: 932 | 933 | Project directories 934 | Repository locations 935 | API endpoints 936 | Configuration locations 937 | Resource boundaries 938 | 939 | Best Practices 940 | When working with roots: 941 | 942 | Only suggest necessary resources 943 | Use clear, descriptive names for roots 944 | Monitor root accessibility 945 | Handle root changes gracefully 946 | 947 | Example 948 | Here’s how a typical MCP client might expose roots: 949 | 950 | { 951 | "roots": [ 952 | { 953 | "uri": "file:///home/user/projects/frontend", 954 | "name": "Frontend Repository" 955 | }, 956 | { 957 | "uri": "https://api.example.com/v1", 958 | "name": "API Endpoint" 959 | } 960 | ] 961 | } 962 | This configuration suggests the server focus on both a local repository and an API endpoint while keeping them logically separated. 963 | 964 | ## Transports 965 | 966 | Transports 967 | Learn about MCP’s communication mechanisms 968 | 969 | Transports in the Model Context Protocol (MCP) provide the foundation for communication between clients and servers. A transport handles the underlying mechanics of how messages are sent and received. 970 | 971 | 972 | Message Format 973 | MCP uses JSON-RPC 2.0 as its wire format. The transport layer is responsible for converting MCP protocol messages into JSON-RPC format for transmission and converting received JSON-RPC messages back into MCP protocol messages. 974 | 975 | There are three types of JSON-RPC messages used: 976 | 977 | 978 | Requests 979 | 980 | { 981 | jsonrpc: "2.0", 982 | id: number | string, 983 | method: string, 984 | params?: object 985 | } 986 | 987 | Responses 988 | 989 | { 990 | jsonrpc: "2.0", 991 | id: number | string, 992 | result?: object, 993 | error?: { 994 | code: number, 995 | message: string, 996 | data?: unknown 997 | } 998 | } 999 | 1000 | Notifications 1001 | 1002 | { 1003 | jsonrpc: "2.0", 1004 | method: string, 1005 | params?: object 1006 | } 1007 | 1008 | Built-in Transport Types 1009 | MCP includes two standard transport implementations: 1010 | 1011 | 1012 | Standard Input/Output (stdio) 1013 | The stdio transport enables communication through standard input and output streams. This is particularly useful for local integrations and command-line tools. 1014 | 1015 | Use stdio when: 1016 | 1017 | Building command-line tools 1018 | Implementing local integrations 1019 | Needing simple process communication 1020 | Working with shell scripts 1021 | TypeScript (Server) 1022 | TypeScript (Client) 1023 | Python (Server) 1024 | Python (Client) 1025 | 1026 | const server = new Server({ 1027 | name: "example-server", 1028 | version: "1.0.0" 1029 | }, { 1030 | capabilities: {} 1031 | }); 1032 | 1033 | const transport = new StdioServerTransport(); 1034 | await server.connect(transport); 1035 | 1036 | Server-Sent Events (SSE) 1037 | SSE transport enables server-to-client streaming with HTTP POST requests for client-to-server communication. 1038 | 1039 | Use SSE when: 1040 | 1041 | Only server-to-client streaming is needed 1042 | Working with restricted networks 1043 | Implementing simple updates 1044 | TypeScript (Server) 1045 | TypeScript (Client) 1046 | Python (Server) 1047 | Python (Client) 1048 | 1049 | import express from "express"; 1050 | 1051 | const app = express(); 1052 | 1053 | const server = new Server({ 1054 | name: "example-server", 1055 | version: "1.0.0" 1056 | }, { 1057 | capabilities: {} 1058 | }); 1059 | 1060 | let transport: SSEServerTransport | null = null; 1061 | 1062 | app.get("/sse", (req, res) => { 1063 | transport = new SSEServerTransport("/messages", res); 1064 | server.connect(transport); 1065 | }); 1066 | 1067 | app.post("/messages", (req, res) => { 1068 | if (transport) { 1069 | transport.handlePostMessage(req, res); 1070 | } 1071 | }); 1072 | 1073 | app.listen(3000); 1074 | 1075 | Custom Transports 1076 | MCP makes it easy to implement custom transports for specific needs. Any transport implementation just needs to conform to the Transport interface: 1077 | 1078 | You can implement custom transports for: 1079 | 1080 | Custom network protocols 1081 | Specialized communication channels 1082 | Integration with existing systems 1083 | Performance optimization 1084 | TypeScript 1085 | Python 1086 | 1087 | interface Transport { 1088 | // Start processing messages 1089 | start(): Promise<void>; 1090 | 1091 | // Send a JSON-RPC message 1092 | send(message: JSONRPCMessage): Promise<void>; 1093 | 1094 | // Close the connection 1095 | close(): Promise<void>; 1096 | 1097 | // Callbacks 1098 | onclose?: () => void; 1099 | onerror?: (error: Error) => void; 1100 | onmessage?: (message: JSONRPCMessage) => void; 1101 | } 1102 | 1103 | Error Handling 1104 | Transport implementations should handle various error scenarios: 1105 | 1106 | Connection errors 1107 | Message parsing errors 1108 | Protocol errors 1109 | Network timeouts 1110 | Resource cleanup 1111 | Example error handling: 1112 | 1113 | TypeScript 1114 | Python 1115 | 1116 | class ExampleTransport implements Transport { 1117 | async start() { 1118 | try { 1119 | // Connection logic 1120 | } catch (error) { 1121 | this.onerror?.(new Error(`Failed to connect: ${error}`)); 1122 | throw error; 1123 | } 1124 | } 1125 | 1126 | async send(message: JSONRPCMessage) { 1127 | try { 1128 | // Sending logic 1129 | } catch (error) { 1130 | this.onerror?.(new Error(`Failed to send message: ${error}`)); 1131 | throw error; 1132 | } 1133 | } 1134 | } 1135 | 1136 | Best Practices 1137 | When implementing or using MCP transport: 1138 | 1139 | Handle connection lifecycle properly 1140 | Implement proper error handling 1141 | Clean up resources on connection close 1142 | Use appropriate timeouts 1143 | Validate messages before sending 1144 | Log transport events for debugging 1145 | Implement reconnection logic when appropriate 1146 | Handle backpressure in message queues 1147 | Monitor connection health 1148 | Implement proper security measures 1149 | 1150 | Security Considerations 1151 | When implementing transport: 1152 | 1153 | 1154 | Authentication and Authorization 1155 | Implement proper authentication mechanisms 1156 | Validate client credentials 1157 | Use secure token handling 1158 | Implement authorization checks 1159 | 1160 | Data Security 1161 | Use TLS for network transport 1162 | Encrypt sensitive data 1163 | Validate message integrity 1164 | Implement message size limits 1165 | Sanitize input data 1166 | 1167 | Network Security 1168 | Implement rate limiting 1169 | Use appropriate timeouts 1170 | Handle denial of service scenarios 1171 | Monitor for unusual patterns 1172 | Implement proper firewall rules 1173 | 1174 | Debugging Transport 1175 | Tips for debugging transport issues: 1176 | 1177 | Enable debug logging 1178 | Monitor message flow 1179 | Check connection states 1180 | Validate message formats 1181 | Test error scenarios 1182 | Use network analysis tools 1183 | Implement health checks 1184 | Monitor resource usage 1185 | Test edge cases 1186 | Use proper error tracking 1187 | ```