[
  {
    "path": ".gitignore",
    "content": "node_modules\ndist\n.port\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 AgentDesk LLC\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "THIS PROJECT IS NO LONGER ACTIVE PLEASE USE A DIFFERENT SOLUTION FOR THIS. \n\n# BrowserTools MCP\n\n\n> Make your AI tools 10x more aware and capable of interacting with your browser\n\nThis 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.\n\nRead our [docs](https://browsertools.agentdesk.ai/) for the full installation, quickstart and contribution guides.\n\n## Roadmap\n\nCheck out our project roadmap here: [Github Roadmap / Project Board](https://github.com/orgs/AgentDeskAI/projects/1/views/1)\n\n## Updates\n\nv1.2.0 is out! Here's a quick breakdown of the update:\n- 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!)\n- Integrated a suite of SEO, performance, accessibility, and best practice analysis tools via Lighthouse\n- Implemented a NextJS specific prompt used to improve SEO for a NextJS application\n- Added Debugger Mode as a tool which executes all debugging tools in a particular sequence, along with a prompt to improve reasoning\n- Added Audit Mode as a tool to execute all auditing tools in a particular sequence\n- Resolved Windows connectivity issues\n- Improved networking between BrowserTools server, extension and MCP server with host/port auto-discovery, auto-reconnect, and graceful shutdown mechanisms\n- Added ability to more easily exit out of the Browser Tools server with Ctrl+C\n\n## Quickstart Guide\n\nThere are three components to run this MCP tool:\n\n1. Install our chrome extension from here: [v1.2.0 BrowserToolsMCP Chrome Extension](https://github.com/AgentDeskAI/browser-tools-mcp/releases/download/v1.2.0/BrowserTools-1.2.0-extension.zip)\n2. Install the MCP server from this command within your IDE: `npx @agentdeskai/browser-tools-mcp@latest`\n3. Open a new terminal and run this command: `npx @agentdeskai/browser-tools-server@latest`\n\n* Different IDEs have different configs but this command is generally a good starting point; please reference your IDEs docs for the proper config setup\n\nIMPORTANT TIP - there are two servers you need to install. There's...\n- browser-tools-server (local nodejs server that's a middleware for gathering logs)\nand\n- browser-tools-mcp (MCP server that you install into your IDE that communicates w/ the extension + browser-tools-server)\n\n`npx @agentdeskai/browser-tools-mcp@latest` is what you put into your IDE\n`npx @agentdeskai/browser-tools-server@latest` is what you run in a new terminal window\n\nAfter those three steps, open up your chrome dev tools and then the BrowserToolsMCP panel.\n\nIf you're still having issues try these steps:\n- Quit / close down your browser. Not just the window but all of Chrome itself. \n- Restart the local node server (browser-tools-server)\n- Make sure you only have ONE instance of chrome dev tools panel open\n\nAfter that, it should work but if it doesn't let me know and I can share some more steps to gather logs/info about the issue!\n\nIf 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)\n\n## Full Update Notes:\n\nCoding agents like Cursor can run these audits against the current page seamlessly. By leveraging Puppeteer and the Lighthouse npm library, BrowserTools MCP can now:\n\n- Evaluate pages for WCAG compliance\n- Identify performance bottlenecks\n- Flag on-page SEO issues\n- Check adherence to web development best practices\n- Review NextJS specific issues with SEO\n\n...all without leaving your IDE 🎉\n\n---\n\n## 🔑 Key Additions\n\n| Audit Type         | Description                                                                                                                              |\n| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |\n| **Accessibility**  | WCAG-compliant checks for color contrast, missing alt text, keyboard navigation traps, ARIA attributes, and more.                        |\n| **Performance**    | Lighthouse-driven analysis of render-blocking resources, excessive DOM size, unoptimized images, and other factors affecting page speed. |\n| **SEO**            | Evaluates on-page SEO factors (like metadata, headings, and link structure) and suggests improvements for better search visibility.      |\n| **Best Practices** | Checks for general best practices in web development.                                                                                    |\n| **NextJS Audit**   | Injects a prompt used to perform a NextJS audit.                                                                                         |\n| **Audit Mode**     | Runs all auditing tools in a sequence.                                                                                                   |\n| **Debugger Mode**  | Runs all debugging tools in a sequence.                                                                                                  |\n\n---\n\n## 🛠️ Using Audit Tools\n\n### ✅ **Before You Start**\n\nEnsure you have:\n\n- An **active tab** in your browser\n- The **BrowserTools extension enabled**\n\n### ▶️ **Running Audits**\n\n**Headless Browser Automation**:  \n 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.\n\nThe headless browser instance remains active for **60 seconds** after the last audit call to efficiently handle consecutive audit requests.\n\n**Structured Results**:  \n 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.\n\nThe MCP server provides tools to run audits on the current page. Here are example queries you can use to trigger them:\n\n#### Accessibility Audit (`runAccessibilityAudit`)\n\nEnsures the page meets accessibility standards like WCAG.\n\n> **Example Queries:**\n>\n> - \"Are there any accessibility issues on this page?\"\n> - \"Run an accessibility audit.\"\n> - \"Check if this page meets WCAG standards.\"\n\n#### Performance Audit (`runPerformanceAudit`)\n\nIdentifies performance bottlenecks and loading issues.\n\n> **Example Queries:**\n>\n> - \"Why is this page loading so slowly?\"\n> - \"Check the performance of this page.\"\n> - \"Run a performance audit.\"\n\n#### SEO Audit (`runSEOAudit`)\n\nEvaluates how well the page is optimized for search engines.\n\n> **Example Queries:**\n>\n> - \"How can I improve SEO for this page?\"\n> - \"Run an SEO audit.\"\n> - \"Check SEO on this page.\"\n\n#### Best Practices Audit (`runBestPracticesAudit`)\n\nChecks for general best practices in web development.\n\n> **Example Queries:**\n>\n> - \"Run a best practices audit.\"\n> - \"Check best practices on this page.\"\n> - \"Are there any best practices issues on this page?\"\n\n#### Audit Mode (`runAuditMode`)\n\nRuns all audits in a particular sequence. Will run a NextJS audit if the framework is detected.\n\n> **Example Queries:**\n>\n> - \"Run audit mode.\"\n> - \"Enter audit mode.\"\n\n#### NextJS Audits (`runNextJSAudit`)\n\nChecks for best practices and SEO improvements for NextJS applications\n\n> **Example Queries:**\n>\n> - \"Run a NextJS audit.\"\n> - \"Run a NextJS audit, I'm using app router.\"\n> - \"Run a NextJS audit, I'm using page router.\"\n\n#### Debugger Mode (`runDebuggerMode`)\n\nRuns all debugging tools in a particular sequence\n\n> **Example Queries:**\n>\n> - \"Enter debugger mode.\"\n\n## Architecture\n\nThere are three core components all used to capture and analyze browser data:\n\n1. **Chrome Extension**: A browser extension that captures screenshots, console logs, network activity and DOM elements.\n2. **Node Server**: An intermediary server that facilitates communication between the Chrome extension and any instance of an MCP server.\n3. **MCP Server**: A Model Context Protocol server that provides standardized tools for AI clients to interact with the browser.\n\n```\n┌─────────────┐     ┌──────────────┐     ┌───────────────┐     ┌─────────────┐\n│  MCP Client │ ──► │  MCP Server  │ ──► │  Node Server  │ ──► │   Chrome    │\n│  (e.g.      │ ◄── │  (Protocol   │ ◄── │ (Middleware)  │ ◄── │  Extension  │\n│   Cursor)   │     │   Handler)   │     │               │     │             │\n└─────────────┘     └──────────────┘     └───────────────┘     └─────────────┘\n```\n\nModel Context Protocol (MCP) is a capability supported by Anthropic AI models that\nallow you to create custom tools for any compatible client. MCP clients like Claude\nDesktop, Cursor, Cline or Zed can run an MCP server which \"teaches\" these clients\nabout a new tool that they can use.\n\nThese 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.\n\nAll consumers of the BrowserTools MCP Server interface with the same NodeJS API and Chrome extension.\n\n#### Chrome Extension\n\n- Monitors XHR requests/responses and console logs\n- Tracks selected DOM elements\n- Sends all logs and current element to the BrowserTools Connector\n- Connects to Websocket server to capture/send screenshots\n- Allows user to configure token/truncation limits + screenshot folder path\n\n#### Node Server\n\n- Acts as middleware between the Chrome extension and MCP server\n- Receives logs and currently selected element from Chrome extension\n- Processes requests from MCP server to capture logs, screenshot or current element\n- Sends Websocket command to the Chrome extension for capturing a screenshot\n- Intelligently truncates strings and # of duplicate objects in logs to avoid token limits\n- Removes cookies and sensitive headers to avoid sending to LLMs in MCP clients\n\n#### MCP Server\n\n- Implements the Model Context Protocol\n- Provides standardized tools for AI clients\n- Compatible with various MCP clients (Cursor, Cline, Zed, Claude Desktop, etc.)\n\n## Installation\n\nInstallation steps can be found in our documentation:\n\n- [BrowserTools MCP Docs](https://browsertools.agentdesk.ai/)\n\n## Usage\n\nOnce installed and configured, the system allows any compatible MCP client to:\n\n- Monitor browser console output\n- Capture network traffic\n- Take screenshots\n- Analyze selected elements\n- Wipe logs stored in our MCP server\n- Run accessibility, performance, SEO, and best practices audits\n\n## Compatibility\n\n- Works with any MCP-compatible client\n- Primarily designed for Cursor IDE integration\n- Supports other AI editors and MCP clients\n"
  },
  {
    "path": "browser-tools-mcp/README.md",
    "content": "# Browser Tools MCP Server\n\nA 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.\n\n## Features\n\n- MCP protocol implementation\n- Browser console log access\n- Network request analysis\n- Screenshot capture capabilities\n- Element selection and inspection\n- Real-time browser state monitoring\n- Accessibility, performance, SEO, and best practices audits\n\n## Prerequisites\n\n- Node.js 14 or higher\n- Browser Tools Server running\n- Chrome or Chromium browser installed (required for audit functionality)\n\n## Installation\n\n```bash\nnpx @agentdeskai/browser-tools-mcp\n```\n\nOr install globally:\n\n```bash\nnpm install -g @agentdeskai/browser-tools-mcp\n```\n\n## Usage\n\n1. First, make sure the Browser Tools Server is running:\n\n```bash\nnpx @agentdeskai/browser-tools-server\n```\n\n2. Then start the MCP server:\n\n```bash\nnpx @agentdeskai/browser-tools-mcp\n```\n\n3. The MCP server will connect to the Browser Tools Server and provide the following capabilities:\n\n- Console log retrieval\n- Network request monitoring\n- Screenshot capture\n- Element selection\n- Browser state analysis\n- Accessibility and performance audits\n\n## MCP Functions\n\nThe server provides the following MCP functions:\n\n- `mcp_getConsoleLogs` - Retrieve browser console logs\n- `mcp_getConsoleErrors` - Get browser console errors\n- `mcp_getNetworkErrors` - Get network error logs\n- `mcp_getNetworkSuccess` - Get successful network requests\n- `mcp_getNetworkLogs` - Get all network logs\n- `mcp_getSelectedElement` - Get the currently selected DOM element\n- `mcp_runAccessibilityAudit` - Run a WCAG-compliant accessibility audit\n- `mcp_runPerformanceAudit` - Run a performance audit\n- `mcp_runSEOAudit` - Run an SEO audit\n- `mcp_runBestPracticesAudit` - Run a best practices audit\n\n## Integration\n\nThis 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.\n\n## License\n\nMIT\n"
  },
  {
    "path": "browser-tools-mcp/mcp-server.ts",
    "content": "#!/usr/bin/env node\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport path from \"path\";\nimport fs from \"fs\";\n\n// Create the MCP server\nconst server = new McpServer({\n  name: \"Browser Tools MCP\",\n  version: \"1.2.0\",\n});\n\n// Track the discovered server connection\nlet discoveredHost = \"127.0.0.1\";\nlet discoveredPort = 3025;\nlet serverDiscovered = false;\n\n// Function to get the default port from environment variable or default\nfunction getDefaultServerPort(): number {\n  // Check environment variable first\n  if (process.env.BROWSER_TOOLS_PORT) {\n    const envPort = parseInt(process.env.BROWSER_TOOLS_PORT, 10);\n    if (!isNaN(envPort) && envPort > 0) {\n      return envPort;\n    }\n  }\n\n  // Try to read from .port file\n  try {\n    const portFilePath = path.join(__dirname, \".port\");\n    if (fs.existsSync(portFilePath)) {\n      const port = parseInt(fs.readFileSync(portFilePath, \"utf8\").trim(), 10);\n      if (!isNaN(port) && port > 0) {\n        return port;\n      }\n    }\n  } catch (err) {\n    console.error(\"Error reading port file:\", err);\n  }\n\n  // Default port if no configuration found\n  return 3025;\n}\n\n// Function to get default server host from environment variable or default\nfunction getDefaultServerHost(): string {\n  // Check environment variable first\n  if (process.env.BROWSER_TOOLS_HOST) {\n    return process.env.BROWSER_TOOLS_HOST;\n  }\n\n  // Default to localhost\n  return \"127.0.0.1\";\n}\n\n// Server discovery function - similar to what you have in the Chrome extension\nasync function discoverServer(): Promise<boolean> {\n  console.log(\"Starting server discovery process\");\n\n  // Common hosts to try\n  const hosts = [getDefaultServerHost(), \"127.0.0.1\", \"localhost\"];\n\n  // Ports to try (start with default, then try others)\n  const defaultPort = getDefaultServerPort();\n  const ports = [defaultPort];\n\n  // Add additional ports (fallback range)\n  for (let p = 3025; p <= 3035; p++) {\n    if (p !== defaultPort) {\n      ports.push(p);\n    }\n  }\n\n  console.log(`Will try hosts: ${hosts.join(\", \")}`);\n  console.log(`Will try ports: ${ports.join(\", \")}`);\n\n  // Try to find the server\n  for (const host of hosts) {\n    for (const port of ports) {\n      try {\n        console.log(`Checking ${host}:${port}...`);\n\n        // Use the identity endpoint for validation\n        const response = await fetch(`http://${host}:${port}/.identity`, {\n          signal: AbortSignal.timeout(1000), // 1 second timeout\n        });\n\n        if (response.ok) {\n          const identity = await response.json();\n\n          // Verify this is actually our server by checking the signature\n          if (identity.signature === \"mcp-browser-connector-24x7\") {\n            console.log(`Successfully found server at ${host}:${port}`);\n\n            // Save the discovered connection\n            discoveredHost = host;\n            discoveredPort = port;\n            serverDiscovered = true;\n\n            return true;\n          }\n        }\n      } catch (error: any) {\n        // Ignore connection errors during discovery\n        console.error(`Error checking ${host}:${port}: ${error.message}`);\n      }\n    }\n  }\n\n  console.error(\"No server found during discovery\");\n  return false;\n}\n\n// Wrapper function to ensure server connection before making requests\nasync function withServerConnection<T>(\n  apiCall: () => Promise<T>\n): Promise<T | any> {\n  // Attempt to discover server if not already discovered\n  if (!serverDiscovered) {\n    const discovered = await discoverServer();\n    if (!discovered) {\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: \"Failed to discover browser connector server. Please ensure it's running.\",\n          },\n        ],\n        isError: true,\n      };\n    }\n  }\n\n  // Now make the actual API call with discovered host/port\n  try {\n    return await apiCall();\n  } catch (error: any) {\n    // If the request fails, try rediscovering the server once\n    console.error(\n      `API call failed: ${error.message}. Attempting rediscovery...`\n    );\n    serverDiscovered = false;\n\n    if (await discoverServer()) {\n      console.error(\"Rediscovery successful. Retrying API call...\");\n      try {\n        // Retry the API call with the newly discovered connection\n        return await apiCall();\n      } catch (retryError: any) {\n        console.error(`Retry failed: ${retryError.message}`);\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `Error after reconnection attempt: ${retryError.message}`,\n            },\n          ],\n          isError: true,\n        };\n      }\n    } else {\n      console.error(\"Rediscovery failed. Could not reconnect to server.\");\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: `Failed to reconnect to server: ${error.message}`,\n          },\n        ],\n        isError: true,\n      };\n    }\n  }\n}\n\n// We'll define our tools that retrieve data from the browser connector\nserver.tool(\"getConsoleLogs\", \"Check our browser logs\", async () => {\n  return await withServerConnection(async () => {\n    const response = await fetch(\n      `http://${discoveredHost}:${discoveredPort}/console-logs`\n    );\n    const json = await response.json();\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: JSON.stringify(json, null, 2),\n        },\n      ],\n    };\n  });\n});\n\nserver.tool(\n  \"getConsoleErrors\",\n  \"Check our browsers console errors\",\n  async () => {\n    return await withServerConnection(async () => {\n      const response = await fetch(\n        `http://${discoveredHost}:${discoveredPort}/console-errors`\n      );\n      const json = await response.json();\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: JSON.stringify(json, null, 2),\n          },\n        ],\n      };\n    });\n  }\n);\n\nserver.tool(\"getNetworkErrors\", \"Check our network ERROR logs\", async () => {\n  return await withServerConnection(async () => {\n    const response = await fetch(\n      `http://${discoveredHost}:${discoveredPort}/network-errors`\n    );\n    const json = await response.json();\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: JSON.stringify(json, null, 2),\n        },\n      ],\n      isError: true,\n    };\n  });\n});\n\nserver.tool(\"getNetworkLogs\", \"Check ALL our network logs\", async () => {\n  return await withServerConnection(async () => {\n    const response = await fetch(\n      `http://${discoveredHost}:${discoveredPort}/network-success`\n    );\n    const json = await response.json();\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: JSON.stringify(json, null, 2),\n        },\n      ],\n    };\n  });\n});\n\nserver.tool(\n  \"takeScreenshot\",\n  \"Take a screenshot of the current browser tab\",\n  async () => {\n    return await withServerConnection(async () => {\n      try {\n        const response = await fetch(\n          `http://${discoveredHost}:${discoveredPort}/capture-screenshot`,\n          {\n            method: \"POST\",\n          }\n        );\n\n        const result = await response.json();\n\n        if (response.ok) {\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: \"Successfully saved screenshot\",\n              },\n            ],\n          };\n        } else {\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: `Error taking screenshot: ${result.error}`,\n              },\n            ],\n          };\n        }\n      } catch (error: any) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `Failed to take screenshot: ${errorMessage}`,\n            },\n          ],\n        };\n      }\n    });\n  }\n);\n\nserver.tool(\n  \"getSelectedElement\",\n  \"Get the selected element from the browser\",\n  async () => {\n    return await withServerConnection(async () => {\n      const response = await fetch(\n        `http://${discoveredHost}:${discoveredPort}/selected-element`\n      );\n      const json = await response.json();\n      return {\n        content: [\n          {\n            type: \"text\",\n            text: JSON.stringify(json, null, 2),\n          },\n        ],\n      };\n    });\n  }\n);\n\nserver.tool(\"wipeLogs\", \"Wipe all browser logs from memory\", async () => {\n  return await withServerConnection(async () => {\n    const response = await fetch(\n      `http://${discoveredHost}:${discoveredPort}/wipelogs`,\n      {\n        method: \"POST\",\n      }\n    );\n    const json = await response.json();\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: json.message,\n        },\n      ],\n    };\n  });\n});\n\n// Define audit categories as enum to match the server's AuditCategory enum\nenum AuditCategory {\n  ACCESSIBILITY = \"accessibility\",\n  PERFORMANCE = \"performance\",\n  SEO = \"seo\",\n  BEST_PRACTICES = \"best-practices\",\n  PWA = \"pwa\",\n}\n\n// Add tool for accessibility audits, launches a headless browser instance\nserver.tool(\n  \"runAccessibilityAudit\",\n  \"Run an accessibility audit on the current page\",\n  {},\n  async () => {\n    return await withServerConnection(async () => {\n      try {\n        // Simplified approach - let the browser connector handle the current tab and URL\n        console.log(\n          `Sending POST request to http://${discoveredHost}:${discoveredPort}/accessibility-audit`\n        );\n        const response = await fetch(\n          `http://${discoveredHost}:${discoveredPort}/accessibility-audit`,\n          {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n              Accept: \"application/json\",\n            },\n            body: JSON.stringify({\n              category: AuditCategory.ACCESSIBILITY,\n              source: \"mcp_tool\",\n              timestamp: Date.now(),\n            }),\n          }\n        );\n\n        // Log the response status\n        console.log(`Accessibility audit response status: ${response.status}`);\n\n        if (!response.ok) {\n          const errorText = await response.text();\n          console.error(`Accessibility audit error: ${errorText}`);\n          throw new Error(`Server returned ${response.status}: ${errorText}`);\n        }\n\n        const json = await response.json();\n\n        // flatten it by merging metadata with the report contents\n        if (json.report) {\n          const { metadata, report } = json;\n          const flattened = {\n            ...metadata,\n            ...report,\n          };\n\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: JSON.stringify(flattened, null, 2),\n              },\n            ],\n          };\n        } else {\n          // Return as-is if it's not in the new format\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: JSON.stringify(json, null, 2),\n              },\n            ],\n          };\n        }\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        console.error(\"Error in accessibility audit:\", errorMessage);\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `Failed to run accessibility audit: ${errorMessage}`,\n            },\n          ],\n        };\n      }\n    });\n  }\n);\n\n// Add tool for performance audits, launches a headless browser instance\nserver.tool(\n  \"runPerformanceAudit\",\n  \"Run a performance audit on the current page\",\n  {},\n  async () => {\n    return await withServerConnection(async () => {\n      try {\n        // Simplified approach - let the browser connector handle the current tab and URL\n        console.log(\n          `Sending POST request to http://${discoveredHost}:${discoveredPort}/performance-audit`\n        );\n        const response = await fetch(\n          `http://${discoveredHost}:${discoveredPort}/performance-audit`,\n          {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n              Accept: \"application/json\",\n            },\n            body: JSON.stringify({\n              category: AuditCategory.PERFORMANCE,\n              source: \"mcp_tool\",\n              timestamp: Date.now(),\n            }),\n          }\n        );\n\n        // Log the response status\n        console.log(`Performance audit response status: ${response.status}`);\n\n        if (!response.ok) {\n          const errorText = await response.text();\n          console.error(`Performance audit error: ${errorText}`);\n          throw new Error(`Server returned ${response.status}: ${errorText}`);\n        }\n\n        const json = await response.json();\n\n        // flatten it by merging metadata with the report contents\n        if (json.report) {\n          const { metadata, report } = json;\n          const flattened = {\n            ...metadata,\n            ...report,\n          };\n\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: JSON.stringify(flattened, null, 2),\n              },\n            ],\n          };\n        } else {\n          // Return as-is if it's not in the new format\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: JSON.stringify(json, null, 2),\n              },\n            ],\n          };\n        }\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        console.error(\"Error in performance audit:\", errorMessage);\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `Failed to run performance audit: ${errorMessage}`,\n            },\n          ],\n        };\n      }\n    });\n  }\n);\n\n// Add tool for SEO audits, launches a headless browser instance\nserver.tool(\n  \"runSEOAudit\",\n  \"Run an SEO audit on the current page\",\n  {},\n  async () => {\n    return await withServerConnection(async () => {\n      try {\n        console.log(\n          `Sending POST request to http://${discoveredHost}:${discoveredPort}/seo-audit`\n        );\n        const response = await fetch(\n          `http://${discoveredHost}:${discoveredPort}/seo-audit`,\n          {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n              Accept: \"application/json\",\n            },\n            body: JSON.stringify({\n              category: AuditCategory.SEO,\n              source: \"mcp_tool\",\n              timestamp: Date.now(),\n            }),\n          }\n        );\n\n        // Log the response status\n        console.log(`SEO audit response status: ${response.status}`);\n\n        if (!response.ok) {\n          const errorText = await response.text();\n          console.error(`SEO audit error: ${errorText}`);\n          throw new Error(`Server returned ${response.status}: ${errorText}`);\n        }\n\n        const json = await response.json();\n\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: JSON.stringify(json, null, 2),\n            },\n          ],\n        };\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        console.error(\"Error in SEO audit:\", errorMessage);\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `Failed to run SEO audit: ${errorMessage}`,\n            },\n          ],\n        };\n      }\n    });\n  }\n);\n\nserver.tool(\"runNextJSAudit\", {}, async () => ({\n  content: [\n    {\n      type: \"text\",\n      text: `\n      You are an expert in SEO and web development with NextJS. Given the following procedures for analyzing my codebase, please perform a comprehensive - page by page analysis of our NextJS application to identify any issues or areas of improvement for SEO.\n\n      After each iteration of changes, reinvoke this tool to re-fetch our SEO audit procedures and then scan our codebase again to identify additional areas of improvement. \n\n      When no more areas of improvement are found, return \"No more areas of improvement found, your NextJS application is optimized for SEO!\".\n\n      Start by analyzing each of the following aspects of our codebase:\n      1. Meta tags - provides information about your website to search engines and social media platforms.\n\n        Pages should provide the following standard meta tags:\n\n        title\n        description\n        keywords\n        robots\n        viewport\n        charSet\n        Open Graph meta tags:\n\n        og:site_name\n        og:locale\n        og:title\n        og:description\n        og:type\n        og:url\n        og:image\n        og:image:alt\n        og:image:type\n        og:image:width\n        og:image:height\n        Article meta tags (actually it's also OpenGraph):\n\n        article:published_time\n        article:modified_time\n        article:author\n        Twitter meta tags:\n\n        twitter:card\n        twitter:site\n        twitter:creator\n        twitter:title\n        twitter:description\n        twitter:image\n\n        For applications using the pages router, set up metatags like this in pages/[slug].tsx:\n          import Head from \"next/head\";\n\n          export default function Page() {\n            return (\n              <Head>\n                <title>\n                  Next.js SEO: The Complete Checklist to Boost Your Site Ranking\n                </title>\n                <meta\n                  name=\"description\"\n                  content=\"Learn how to optimize your Next.js website for SEO by following this complete checklist.\"\n                />\n                <meta\n                  name=\"keywords\"\n                  content=\"nextjs seo complete checklist, nextjs seo tutorial\"\n                />\n                <meta name=\"robots\" content=\"index, follow\" />\n                <meta name=\"googlebot\" content=\"index, follow\" />\n                <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n                <meta charSet=\"utf-8\" />\n                <meta property=\"og:site_name\" content=\"Blog | Minh Vu\" />\n                <meta property=\"og:locale\" content=\"en_US\" />\n                <meta\n                  property=\"og:title\"\n                  content=\"Next.js SEO: The Complete Checklist to Boost Your Site Ranking\"\n                />\n                <meta\n                  property=\"og:description\"\n                  content=\"Learn how to optimize your Next.js website for SEO by following this complete checklist.\"\n                />\n                <meta property=\"og:type\" content=\"website\" />\n                <meta property=\"og:url\" content=\"https://dminhvu.com/nextjs-seo\" />\n                <meta\n                  property=\"og:image\"\n                  content=\"https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png\"\n                />\n                <meta property=\"og:image:alt\" content=\"Next.js SEO\" />\n                <meta property=\"og:image:type\" content=\"image/png\" />\n                <meta property=\"og:image:width\" content=\"1200\" />\n                <meta property=\"og:image:height\" content=\"630\" />\n                <meta\n                  property=\"article:published_time\"\n                  content=\"2024-01-11T11:35:00+07:00\"\n                />\n                <meta\n                  property=\"article:modified_time\"\n                  content=\"2024-01-11T11:35:00+07:00\"\n                />\n                <meta\n                  property=\"article:author\"\n                  content=\"https://www.linkedin.com/in/dminhvu02\"\n                />\n                <meta name=\"twitter:card\" content=\"summary_large_image\" />\n                <meta name=\"twitter:site\" content=\"@dminhvu02\" />\n                <meta name=\"twitter:creator\" content=\"@dminhvu02\" />\n                <meta\n                  name=\"twitter:title\"\n                  content=\"Next.js SEO: The Complete Checklist to Boost Your Site Ranking\"\n                />\n                <meta\n                  name=\"twitter:description\"\n                  content=\"Learn how to optimize your Next.js website for SEO by following this complete checklist.\"\n                />\n                <meta\n                  name=\"twitter:image\"\n                  content=\"https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png\"\n                />\n              </Head>\n            );\n          }\n\n        For applications using the app router, set up metatags like this in layout.tsx:\n          import type { Viewport, Metadata } from \"next\";\n\n          export const viewport: Viewport = {\n            width: \"device-width\",\n            initialScale: 1,\n            themeColor: \"#ffffff\"\n          };\n          \n          export const metadata: Metadata = {\n            metadataBase: new URL(\"https://dminhvu.com\"),\n            openGraph: {\n              siteName: \"Blog | Minh Vu\",\n              type: \"website\",\n              locale: \"en_US\"\n            },\n            robots: {\n              index: true,\n              follow: true,\n              \"max-image-preview\": \"large\",\n              \"max-snippet\": -1,\n              \"max-video-preview\": -1,\n              googleBot: \"index, follow\"\n            },\n            alternates: {\n              types: {\n                \"application/rss+xml\": \"https://dminhvu.com/rss.xml\"\n              }\n            },\n            applicationName: \"Blog | Minh Vu\",\n            appleWebApp: {\n              title: \"Blog | Minh Vu\",\n              statusBarStyle: \"default\",\n              capable: true\n            },\n            verification: {\n              google: \"YOUR_DATA\",\n              yandex: [\"YOUR_DATA\"],\n              other: {\n                \"msvalidate.01\": [\"YOUR_DATA\"],\n                \"facebook-domain-verification\": [\"YOUR_DATA\"]\n              }\n            },\n            icons: {\n              icon: [\n                {\n                  url: \"/favicon.ico\",\n                  type: \"image/x-icon\"\n                },\n                {\n                  url: \"/favicon-16x16.png\",\n                  sizes: \"16x16\",\n                  type: \"image/png\"\n                }\n                // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png\n              ],\n              shortcut: [\n                {\n                  url: \"/favicon.ico\",\n                  type: \"image/x-icon\"\n                }\n              ],\n              apple: [\n                {\n                  url: \"/apple-icon-57x57.png\",\n                  sizes: \"57x57\",\n                  type: \"image/png\"\n                },\n                {\n                  url: \"/apple-icon-60x60.png\",\n                  sizes: \"60x60\",\n                  type: \"image/png\"\n                }\n                // add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png\n              ]\n            }\n          };\n        And like this for any page.tsx file:\n          import { Metadata } from \"next\";\n\n          export const metadata: Metadata = {\n            title: \"Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu\",\n            description:\n              \"dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.\",\n            keywords: [\n              \"elastic\",\n              \"python\",\n              \"javascript\",\n              \"react\",\n              \"machine learning\",\n              \"data science\"\n            ],\n            openGraph: {\n              url: \"https://dminhvu.com\",\n              type: \"website\",\n              title: \"Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu\",\n              description:\n                \"dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.\",\n              images: [\n                {\n                  url: \"https://dminhvu.com/images/home/thumbnail.png\",\n                  width: 1200,\n                  height: 630,\n                  alt: \"dminhvu\"\n                }\n              ]\n            },\n            twitter: {\n              card: \"summary_large_image\",\n              title: \"Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu\",\n              description:\n                \"dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.\",\n              creator: \"@dminhvu02\",\n              site: \"@dminhvu02\",\n              images: [\n                {\n                  url: \"https://dminhvu.com/images/home/thumbnail.png\",\n                  width: 1200,\n                  height: 630,\n                  alt: \"dminhvu\"\n                }\n              ]\n            },\n            alternates: {\n              canonical: \"https://dminhvu.com\"\n            }\n          };\n\n          Note that the charSet and viewport are automatically added by Next.js App Router, so you don't need to define them.\n\n        For applications using the app router, dynamic metadata can be defined by using the generateMetadata function, this is useful when you have dynamic pages like [slug]/page.tsx, or [id]/page.tsx:\n\n        import type { Metadata, ResolvingMetadata } from \"next\";\n\n        type Params = {\n          slug: string;\n        };\n        \n        type Props = {\n          params: Params;\n          searchParams: { [key: string]: string | string[] | undefined };\n        };\n        \n        export async function generateMetadata(\n          { params, searchParams }: Props,\n          parent: ResolvingMetadata\n        ): Promise<Metadata> {\n          const { slug } = params;\n        \n          const post: Post = await fetch(\"YOUR_ENDPOINT\", {\n            method: \"GET\",\n            next: {\n              revalidate: 60 * 60 * 24\n            }\n          }).then((res) => res.json());\n        \n          return {\n            title: \"{post.title} | dminhvu\",\n            authors: [\n              {\n                name: post.author || \"Minh Vu\"\n              }\n            ],\n            description: post.description,\n            keywords: post.keywords,\n            openGraph: {\n              title: \"{post.title} | dminhvu\",\n              description: post.description,\n              type: \"article\",\n              url: \"https://dminhvu.com/{post.slug}\",\n              publishedTime: post.created_at,\n              modifiedTime: post.modified_at,\n              authors: [\"https://dminhvu.com/about\"],\n              tags: post.categories,\n              images: [\n                {\n                  url: \"https://ik.imagekit.io/dminhvu/assets/{post.slug}/thumbnail.png?tr=f-png\",\n                  width: 1024,\n                  height: 576,\n                  alt: post.title,\n                  type: \"image/png\"\n                }\n              ]\n            },\n            twitter: {\n              card: \"summary_large_image\",\n              site: \"@dminhvu02\",\n              creator: \"@dminhvu02\",\n              title: \"{post.title} | dminhvu\",\n              description: post.description,\n              images: [\n                {\n                  url: \"https://ik.imagekit.io/dminhvu/assets/{post.slug}/thumbnail.png?tr=f-png\",\n                  width: 1024,\n                  height: 576,\n                  alt: post.title\n                }\n              ]\n            },\n            alternates: {\n              canonical: \"https://dminhvu.com/{post.slug}\"\n            }\n          };\n        }\n\n        \n      2. JSON-LD Schema\n\n      JSON-LD is a format for structured data that can be used by search engines to understand your content. For example, you can use it to describe a person, an event, an organization, a movie, a book, a recipe, and many other types of entities.\n\n      Our current recommendation for JSON-LD is to render structured data as a <script> tag in your layout.js or page.js components. For example:\n      export default async function Page({ params }) {\n        const { id } = await params\n        const product = await getProduct(id)\n      \n        const jsonLd = {\n          '@context': 'https://schema.org',\n          '@type': 'Product',\n          name: product.name,\n          image: product.image,\n          description: product.description,\n        }\n      \n        return (\n          <section>\n            {/* Add JSON-LD to your page */}\n            <script\n              type=\"application/ld+json\"\n              dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}\n            />\n            {/* ... */}\n          </section>\n        )\n      }\n      \n      You can type your JSON-LD with TypeScript using community packages like schema-dts:\n\n\n      import { Product, WithContext } from 'schema-dts'\n      \n      const jsonLd: WithContext<Product> = {\n        '@context': 'https://schema.org',\n        '@type': 'Product',\n        name: 'Next.js Sticker',\n        image: 'https://nextjs.org/imgs/sticker.png',\n        description: 'Dynamic at the speed of static.',\n      }\n      3. Sitemap\n      Your website should provide a sitemap so that search engines can easily crawl and index your pages.\n\n        Generate Sitemap for Next.js Pages Router\n        For Next.js Pages Router, you can use next-sitemap to generate a sitemap for your Next.js website after building.\n\n        For example, running the following command will install next-sitemap and generate a sitemap for this blog:\n\n\n        npm install next-sitemap\n        npx next-sitemap\n        A sitemap will be generated at public/sitemap.xml:\n\n        public/sitemap.xml\n\n        <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\" xmlns:news=\"http://www.google.com/schemas/sitemap-news/0.9\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\" xmlns:mobile=\"http://www.google.com/schemas/sitemap-mobile/1.0\" xmlns:image=\"http://www.google.com/schemas/sitemap-image/1.1\" xmlns:video=\"http://www.google.com/schemas/sitemap-video/1.1\">\n        <url>\n          <loc>https://dminhvu.com</loc>\n            <lastmod>2024-01-11T02:03:09.613Z</lastmod>\n            <changefreq>daily</changefreq>\n          <priority>0.7</priority>\n        </url>\n        <!-- other pages -->\n        </urlset>\n        Please visit the next-sitemap page for more information.\n\n        Generate Sitemap for Next.js App Router\n        For Next.js App Router, you can define the sitemap.ts file at app/sitemap.ts:\n\n        app/sitemap.ts\n\n        import {\n          getAllCategories,\n          getAllPostSlugsWithModifyTime\n        } from \"@/utils/getData\";\n        import { MetadataRoute } from \"next\";\n        \n        export default async function sitemap(): Promise<MetadataRoute.Sitemap> {\n          const defaultPages = [\n            {\n              url: \"https://dminhvu.com\",\n              lastModified: new Date(),\n              changeFrequency: \"daily\",\n              priority: 1\n            },\n            {\n              url: \"https://dminhvu.com/about\",\n              lastModified: new Date(),\n              changeFrequency: \"monthly\",\n              priority: 0.9\n            },\n            {\n              url: \"https://dminhvu.com/contact\",\n              lastModified: new Date(),\n              changeFrequency: \"monthly\",\n              priority: 0.9\n            }\n            // other pages\n          ];\n        \n          const postSlugs = await getAllPostSlugsWithModifyTime();\n          const categorySlugs = await getAllCategories();\n        \n          const sitemap = [\n            ...defaultPages,\n            ...postSlugs.map((e: any) => ({\n              url: \"https://dminhvu.com/{e.slug}\",\n              lastModified: e.modified_at,\n              changeFrequency: \"daily\",\n              priority: 0.8\n            })),\n            ...categorySlugs.map((e: any) => ({\n              url: \"https://dminhvu.com/category/{e}\",\n              lastModified: new Date(),\n              changeFrequency: \"daily\",\n              priority: 0.7\n            }))\n          ];\n        \n          return sitemap;\n        }\n        With this sitemap.ts file created, you can access the sitemap at https://dminhvu.com/sitemap.xml.\n\n\n        <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n          <url>\n            <loc>https://dminhvu.com</loc>\n            <lastmod>2024-01-11T02:03:09.613Z</lastmod>\n            <changefreq>daily</changefreq>\n            <priority>0.7</priority>\n          </url>\n          <!-- other pages -->\n        </urlset>\n      4. robots.txt\n      A robots.txt file should be added to tell search engines which pages to crawl and which pages to ignore.\n\n        robots.txt for Next.js Pages Router\n        For Next.js Pages Router, you can create a robots.txt file at public/robots.txt:\n\n        public/robots.txt\n\n        User-agent: *\n        Disallow:\n        Sitemap: https://dminhvu.com/sitemap.xml\n        You can prevent the search engine from crawling a page (usually search result pages, noindex pages, etc.) by adding the following line:\n\n        public/robots.txt\n\n        User-agent: *\n        Disallow: /search?q=\n        Disallow: /admin\n        robots.txt for Next.js App Router\n        For Next.js App Router, you don't need to manually define a robots.txt file. Instead, you can define the robots.ts file at app/robots.ts:\n\n        app/robots.ts\n\n        import { MetadataRoute } from \"next\";\n        \n        export default function robots(): MetadataRoute.Robots {\n          return {\n            rules: {\n              userAgent: \"*\",\n              allow: [\"/\"],\n              disallow: [\"/search?q=\", \"/admin/\"]\n            },\n            sitemap: [\"https://dminhvu.com/sitemap.xml\"]\n          };\n        }\n        With this robots.ts file created, you can access the robots.txt file at https://dminhvu.com/robots.txt.\n\n\n        User-agent: *\n        Allow: /\n        Disallow: /search?q=\n        Disallow: /admin\n        \n        Sitemap: https://dminhvu.com/sitemap.xml\n      5. Link tags\n      Link Tags for Next.js Pages Router\n      For example, the current page has the following link tags if I use the Pages Router:\n\n      pages/_app.tsx\n\n      import Head from \"next/head\";\n      \n      export default function Page() {\n        return (\n          <Head>\n            {/* other parts */}\n            <link\n              rel=\"alternate\"\n              type=\"application/rss+xml\"\n              href=\"https://dminhvu.com/rss.xml\"\n            />\n            <link rel=\"icon\" href=\"/favicon.ico\" type=\"image/x-icon\" />\n            <link rel=\"apple-touch-icon\" sizes=\"57x57\" href=\"/apple-icon-57x57.png\" />\n            <link rel=\"apple-touch-icon\" sizes=\"60x60\" href=\"/apple-icon-60x60.png\" />\n            {/* add apple-touch-icon-72x72.png, apple-touch-icon-76x76.png, apple-touch-icon-114x114.png, apple-touch-icon-120x120.png, apple-touch-icon-144x144.png, apple-touch-icon-152x152.png, apple-touch-icon-180x180.png */}\n            <link\n              rel=\"icon\"\n              type=\"image/png\"\n              href=\"/favicon-16x16.png\"\n              sizes=\"16x16\"\n            />\n            {/* add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png */}\n          </Head>\n        );\n      }\n      pages/[slug].tsx\n\n      import Head from \"next/head\";\n      \n      export default function Page() {\n        return (\n          <Head>\n            {/* other parts */}\n            <link rel=\"canonical\" href=\"https://dminhvu.com/nextjs-seo\" />\n          </Head>\n        );\n      }\n      Link Tags for Next.js App Router\n      For Next.js App Router, the link tags can be defined using the export const metadata or generateMetadata similar to the meta tags section.\n\n      The code below is exactly the same as the meta tags for Next.js App Router section above.\n\n      app/layout.tsx\n\n      export const metadata: Metadata = {\n        // other parts\n        alternates: {\n          types: {\n            \"application/rss+xml\": \"https://dminhvu.com/rss.xml\"\n          }\n        },\n        icons: {\n          icon: [\n            {\n              url: \"/favicon.ico\",\n              type: \"image/x-icon\"\n            },\n            {\n              url: \"/favicon-16x16.png\",\n              sizes: \"16x16\",\n              type: \"image/png\"\n            }\n            // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png\n          ],\n          shortcut: [\n            {\n              url: \"/favicon.ico\",\n              type: \"image/x-icon\"\n            }\n          ],\n          apple: [\n            {\n              url: \"/apple-icon-57x57.png\",\n              sizes: \"57x57\",\n              type: \"image/png\"\n            },\n            {\n              url: \"/apple-icon-60x60.png\",\n              sizes: \"60x60\",\n              type: \"image/png\"\n            }\n            // add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png\n          ]\n        }\n      };\n      app/page.tsx\n\n      export const metadata: Metadata = {\n        // other parts\n        alternates: {\n          canonical: \"https://dminhvu.com\"\n        }\n      };\n      6. Script optimization\n      Script Optimization for General Scripts\n      Next.js provides a built-in component called <Script> to add external scripts to your website.\n\n      For example, you can add Google Analytics to your website by adding the following script tag:\n\n      pages/_app.tsx\n\n      import Head from \"next/head\";\n      import Script from \"next/script\";\n      \n      export default function Page() {\n        return (\n          <Head>\n            {/* other parts */}\n            {process.env.NODE_ENV === \"production\" && (\n              <>\n                <Script async strategy=\"afterInteractive\" id=\"analytics\">\n                  {'\n                    window.dataLayer = window.dataLayer || [];\n                    function gtag(){dataLayer.push(arguments);}\n                    gtag('js', new Date());\n                    gtag('config', 'G-XXXXXXXXXX');\n                  '}\n                </Script>\n              </>\n            )}\n          </Head>\n        );\n      }\n      Script Optimization for Common Third-Party Integrations\n      Next.js App Router introduces a new library called @next/third-parties for:\n\n      Google Tag Manager\n      Google Analytics\n      Google Maps Embed\n      YouTube Embed\n      To use the @next/third-parties library, you need to install it:\n\n\n      npm install @next/third-parties\n      Then, you can add the following code to your app/layout.tsx:\n\n      app/layout.tsx\n\n      import { GoogleTagManager } from \"@next/third-parties/google\";\n      import { GoogleAnalytics } from \"@next/third-parties/google\";\n      import Head from \"next/head\";\n      \n      export default function Page() {\n        return (\n          <html lang=\"en\" className=\"scroll-smooth\" suppressHydrationWarning>\n            {process.env.NODE_ENV === \"production\" && (\n              <>\n                <GoogleAnalytics gaId=\"G-XXXXXXXXXX\" />\n                {/* other scripts */}\n              </>\n            )}\n            {/* other parts */}\n          </html>\n        );\n      }\n      Please note that you don't need to include both GoogleTagManager and GoogleAnalytics if you only use one of them.\n      7. Image optimization\n      Image Optimization\n      This part can be applied to both Pages Router and App Router.\n\n      Image optimization is also an important part of SEO as it helps your website load faster.\n\n      Faster image rendering speed will contribute to the Google PageSpeed score, which can improve user experience and SEO.\n\n      You can use next/image to optimize images in your Next.js website.\n\n      For example, the following code will optimize this post thumbnail:\n\n\n      import Image from \"next/image\";\n      \n      export default function Page() {\n        return (\n          <Image\n            src=\"https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-webp\"\n            alt=\"Next.js SEO\"\n            width={1200}\n            height={630}\n          />\n        );\n      }\n      Remember to use a CDN to serve your media (images, videos, etc.) to improve the loading speed.\n\n      For the image format, use WebP if possible because it has a smaller size than PNG and JPEG.\n\n      Given the provided procedures, begin by analyzing all of our Next.js pages.\n      Check to see what metadata already exists, look for any robot.txt files, and take a closer look at some of the other aspects of our project to determine areas of improvement.\n      Once you've performed this comprehensive analysis, return back a report on what we can do to improve our application.\n      Do not actually make the code changes yet, just return a comprehensive plan that you will ask for approval for.\n      If feedback is provided, adjust the plan accordingly and ask for approval again.\n      If the user approves of the plan, go ahead and proceed to implement all the necessary code changes to completely optimize our application.\n    `,\n    },\n  ],\n}));\n\nserver.tool(\n  \"runDebuggerMode\",\n  \"Run debugger mode to debug an issue in our application\",\n  async () => ({\n    content: [\n      {\n        type: \"text\",\n        text: `\n      Please follow this exact sequence to debug an issue in our application:\n  \n  1. Reflect on 5-7 different possible sources of the problem\n  2. Distill those down to 1-2 most likely sources\n  3. Add additional logs to validate your assumptions and track the transformation of data structures throughout the application control flow before we move onto implementing the actual code fix\n  4. Use the \"getConsoleLogs\", \"getConsoleErrors\", \"getNetworkLogs\" & \"getNetworkErrors\" tools to obtain any newly added web browser logs\n  5. Obtain the server logs as well if accessible - otherwise, ask me to copy/paste them into the chat\n  6. Deeply reflect on what could be wrong + produce a comprehensive analysis of the issue\n  7. Suggest additional logs if the issue persists or if the source is not yet clear\n  8. Once a fix is implemented, ask for approval to remove the previously added logs\n\n  Note: DO NOT run any of our audits (runAccessibilityAudit, runPerformanceAudit, runBestPracticesAudit, runSEOAudit, runNextJSAudit) when in debugging mode unless explicitly asked to do so or unless you switch to audit mode.\n`,\n      },\n    ],\n  })\n);\n\nserver.tool(\n  \"runAuditMode\",\n  \"Run audit mode to optimize our application for SEO, accessibility and performance\",\n  async () => ({\n    content: [\n      {\n        type: \"text\",\n        text: `\n      I want you to enter \"Audit Mode\". Use the following MCP tools one after the other in this exact sequence:\n      \n      1. runAccessibilityAudit\n      2. runPerformanceAudit\n      3. runBestPracticesAudit\n      4. runSEOAudit\n      5. runNextJSAudit (only if our application is ACTUALLY using NextJS)\n\n      After running all of these tools, return back a comprehensive analysis of the audit results.\n\n      Do NOT use runNextJSAudit tool unless you see that our application is ACTUALLY using NextJS.\n\n      DO NOT use the takeScreenshot tool EVER during audit mode. ONLY use it if I specifically ask you to take a screenshot of something.\n\n      DO NOT check console or network logs to get started - your main priority is to run the audits in the sequence defined above.\n      \n      After returning an in-depth analysis, scan through my code and identify various files/parts of my codebase that we want to modify/improve based on the results of our audits.\n\n      After identifying what changes may be needed, do NOT make the actual changes. Instead, return back a comprehensive, step-by-step plan to address all of these changes and ask for approval to execute this plan. If feedback is received, make a new plan and ask for approval again. If approved, execute the ENTIRE plan and after all phases/steps are complete, re-run the auditing tools in the same 4 step sequence again before returning back another analysis for additional changes potentially needed.\n\n      Keep repeating / iterating through this process with the four tools until our application is as optimized as possible for SEO, accessibility and performance.\n\n`,\n      },\n    ],\n  })\n);\n\n// Add tool for Best Practices audits, launches a headless browser instance\nserver.tool(\n  \"runBestPracticesAudit\",\n  \"Run a best practices audit on the current page\",\n  {},\n  async () => {\n    return await withServerConnection(async () => {\n      try {\n        console.log(\n          `Sending POST request to http://${discoveredHost}:${discoveredPort}/best-practices-audit`\n        );\n        const response = await fetch(\n          `http://${discoveredHost}:${discoveredPort}/best-practices-audit`,\n          {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n              Accept: \"application/json\",\n            },\n            body: JSON.stringify({\n              source: \"mcp_tool\",\n              timestamp: Date.now(),\n            }),\n          }\n        );\n\n        // Check for errors\n        if (!response.ok) {\n          const errorText = await response.text();\n          throw new Error(`Server returned ${response.status}: ${errorText}`);\n        }\n\n        const json = await response.json();\n\n        // flatten it by merging metadata with the report contents\n        if (json.report) {\n          const { metadata, report } = json;\n          const flattened = {\n            ...metadata,\n            ...report,\n          };\n\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: JSON.stringify(flattened, null, 2),\n              },\n            ],\n          };\n        } else {\n          // Return as-is if it's not in the new format\n          return {\n            content: [\n              {\n                type: \"text\",\n                text: JSON.stringify(json, null, 2),\n              },\n            ],\n          };\n        }\n      } catch (error) {\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        console.error(\"Error in Best Practices audit:\", errorMessage);\n        return {\n          content: [\n            {\n              type: \"text\",\n              text: `Failed to run Best Practices audit: ${errorMessage}`,\n            },\n          ],\n        };\n      }\n    });\n  }\n);\n\n// Start receiving messages on stdio\n(async () => {\n  try {\n    // Attempt initial server discovery\n    console.error(\"Attempting initial server discovery on startup...\");\n    await discoverServer();\n    if (serverDiscovered) {\n      console.error(\n        `Successfully discovered server at ${discoveredHost}:${discoveredPort}`\n      );\n    } else {\n      console.error(\n        \"Initial server discovery failed. Will try again when tools are used.\"\n      );\n    }\n\n    const transport = new StdioServerTransport();\n\n    // Ensure stdout is only used for JSON messages\n    const originalStdoutWrite = process.stdout.write.bind(process.stdout);\n    process.stdout.write = (chunk: any, encoding?: any, callback?: any) => {\n      // Only allow JSON messages to pass through\n      if (typeof chunk === \"string\" && !chunk.startsWith(\"{\")) {\n        return true; // Silently skip non-JSON messages\n      }\n      return originalStdoutWrite(chunk, encoding, callback);\n    };\n\n    await server.connect(transport);\n  } catch (error) {\n    console.error(\"Failed to initialize MCP server:\", error);\n    process.exit(1);\n  }\n})();\n"
  },
  {
    "path": "browser-tools-mcp/package.json",
    "content": "{\n  \"name\": \"@agentdeskai/browser-tools-mcp\",\n  \"version\": \"1.2.0\",\n  \"description\": \"MCP (Model Context Protocol) server for browser tools integration\",\n  \"main\": \"dist/mcp-server.js\",\n  \"bin\": {\n    \"browser-tools-mcp\": \"dist/mcp-server.js\"\n  },\n  \"scripts\": {\n    \"inspect\": \"tsc && npx @modelcontextprotocol/inspector node -- dist/mcp-server.js\",\n    \"inspect-live\": \"npx @modelcontextprotocol/inspector npx -- @agentdeskai/browser-tools-mcp\",\n    \"build\": \"tsc\",\n    \"start\": \"tsc && node dist/mcp-server.js\",\n    \"prepublishOnly\": \"npm run build\",\n    \"update\": \"npm run build && npm version patch && npm publish\"\n  },\n  \"keywords\": [\n    \"mcp\",\n    \"model-context-protocol\",\n    \"browser\",\n    \"tools\",\n    \"debugging\",\n    \"ai\",\n    \"chrome\",\n    \"extension\"\n  ],\n  \"author\": \"AgentDesk AI\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.4.1\",\n    \"body-parser\": \"^1.20.3\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.21.2\",\n    \"llm-cost\": \"^1.0.5\",\n    \"node-fetch\": \"^2.7.0\",\n    \"ws\": \"^8.18.0\"\n  },\n  \"devDependencies\": {\n    \"@types/ws\": \"^8.5.14\",\n    \"@types/body-parser\": \"^1.19.5\",\n    \"@types/cors\": \"^2.8.17\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/node\": \"^22.13.1\",\n    \"@types/node-fetch\": \"^2.6.11\",\n    \"typescript\": \"^5.7.3\"\n  }\n}\n"
  },
  {
    "path": "browser-tools-mcp/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"esModuleInterop\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \".\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true\n  },\n  \"include\": [\"*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n} "
  },
  {
    "path": "browser-tools-server/README.md",
    "content": "# Browser Tools Server\n\nA 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.\n\n## Features\n\n- Console log capture\n- Network request monitoring\n- Screenshot capture\n- Element selection tracking\n- WebSocket real-time communication\n- Configurable log limits and settings\n- Lighthouse-powered accessibility, performance, SEO, and best practices audits\n\n## Installation\n\n```bash\nnpx @agentdeskai/browser-tools-server\n```\n\nOr install globally:\n\n```bash\nnpm install -g @agentdeskai/browser-tools-server\n```\n\n## Usage\n\n1. Start the server:\n\n```bash\nnpx @agentdeskai/browser-tools-server\n```\n\n2. The server will start on port 3025 by default\n\n3. Install and enable the Browser Tools Chrome Extension\n\n4. The server exposes the following endpoints:\n\n- `/console-logs` - Get console logs\n- `/console-errors` - Get console errors\n- `/network-errors` - Get network error logs\n- `/network-success` - Get successful network requests\n- `/all-xhr` - Get all network requests\n- `/screenshot` - Capture screenshots\n- `/selected-element` - Get currently selected DOM element\n- `/accessibility-audit` - Run accessibility audit on current page\n- `/performance-audit` - Run performance audit on current page\n- `/seo-audit` - Run SEO audit on current page\n\n## API Documentation\n\n### GET Endpoints\n\n- `GET /console-logs` - Returns recent console logs\n- `GET /console-errors` - Returns recent console errors\n- `GET /network-errors` - Returns recent network errors\n- `GET /network-success` - Returns recent successful network requests\n- `GET /all-xhr` - Returns all recent network requests\n- `GET /selected-element` - Returns the currently selected DOM element\n\n### POST Endpoints\n\n- `POST /extension-log` - Receive logs from the extension\n- `POST /screenshot` - Capture and save screenshots\n- `POST /selected-element` - Update the selected element\n- `POST /wipelogs` - Clear all stored logs\n- `POST /accessibility-audit` - Run a WCAG-compliant accessibility audit on the current page\n- `POST /performance-audit` - Run a performance audit on the current page\n- `POST /seo-audit` - Run a SEO audit on the current page\n\n# Audit Functionality\n\nThe 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.\n\n## Smart Limit Implementation\n\nAll audit tools implement a \"smart limit\" approach to provide the most relevant information based on impact severity:\n\n- **Critical issues**: No limit (all issues are shown)\n- **Serious issues**: Up to 15 items per issue\n- **Moderate issues**: Up to 10 items per issue\n- **Minor issues**: Up to 3 items per issue\n\nThis 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.\n\n## Common Audit Response Structure\n\nAll audit responses follow a similar structure:\n\n```json\n{\n  \"metadata\": {\n    \"url\": \"https://example.com\",\n    \"timestamp\": \"2025-03-06T16:28:30.930Z\",\n    \"device\": \"desktop\",\n    \"lighthouseVersion\": \"11.7.1\"\n  },\n  \"report\": {\n    \"score\": 88,\n    \"audit_counts\": {\n      \"failed\": 2,\n      \"passed\": 17,\n      \"manual\": 10,\n      \"informative\": 0,\n      \"not_applicable\": 42\n    }\n    // Audit-specific content\n    // ...\n  }\n}\n```\n\n## Accessibility Audit (`/accessibility-audit`)\n\nThe accessibility audit evaluates web pages against WCAG standards, identifying issues that affect users with disabilities.\n\n### Response Format\n\n```json\n{\n  \"metadata\": {\n    \"url\": \"https://example.com\",\n    \"timestamp\": \"2025-03-06T16:28:30.930Z\",\n    \"device\": \"desktop\",\n    \"lighthouseVersion\": \"11.7.1\"\n  },\n  \"report\": {\n    \"score\": 88,\n    \"audit_counts\": {\n      \"failed\": 2,\n      \"passed\": 17,\n      \"manual\": 10,\n      \"informative\": 0,\n      \"not_applicable\": 42\n    },\n    \"issues\": [\n      {\n        \"id\": \"meta-viewport\",\n        \"title\": \"`[user-scalable=\\\"no\\\"]` is used in the `<meta name=\\\"viewport\\\">` element or the `[maximum-scale]` attribute is less than 5.\",\n        \"impact\": \"critical\",\n        \"category\": \"a11y-best-practices\",\n        \"elements\": [\n          {\n            \"selector\": \"head > meta\",\n            \"snippet\": \"<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0\\\">\",\n            \"label\": \"head > meta\",\n            \"issue_description\": \"Fix any of the following: user-scalable on <meta> tag disables zooming on mobile devices\"\n          }\n        ],\n        \"score\": 0\n      }\n    ],\n    \"categories\": {\n      \"a11y-navigation\": { \"score\": 0, \"issues_count\": 0 },\n      \"a11y-aria\": { \"score\": 0, \"issues_count\": 1 },\n      \"a11y-best-practices\": { \"score\": 0, \"issues_count\": 1 }\n    },\n    \"critical_elements\": [\n      {\n        \"selector\": \"head > meta\",\n        \"snippet\": \"<meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0\\\">\",\n        \"label\": \"head > meta\",\n        \"issue_description\": \"Fix any of the following: user-scalable on <meta> tag disables zooming on mobile devices\"\n      }\n    ],\n    \"prioritized_recommendations\": [\n      \"Fix ARIA attributes and roles\",\n      \"Fix 1 issues in a11y-best-practices\"\n    ]\n  }\n}\n```\n\n### Key Features\n\n- **Issues Categorized by Impact**: Critical, serious, moderate, and minor\n- **Element-Specific Information**: Selectors, snippets, and labels for affected elements\n- **Issue Categories**: ARIA, navigation, color contrast, forms, keyboard access, etc.\n- **Critical Elements List**: Quick access to the most serious issues\n- **Prioritized Recommendations**: Actionable advice in order of importance\n\n## Performance Audit (`/performance-audit`)\n\nThe performance audit analyzes page load speed, Core Web Vitals, and optimization opportunities.\n\n### Response Format\n\n```json\n{\n  \"metadata\": {\n    \"url\": \"https://example.com\",\n    \"timestamp\": \"2025-03-06T16:27:44.900Z\",\n    \"device\": \"desktop\",\n    \"lighthouseVersion\": \"11.7.1\"\n  },\n  \"report\": {\n    \"score\": 60,\n    \"audit_counts\": {\n      \"failed\": 11,\n      \"passed\": 21,\n      \"manual\": 0,\n      \"informative\": 20,\n      \"not_applicable\": 8\n    },\n    \"metrics\": [\n      {\n        \"id\": \"lcp\",\n        \"score\": 0,\n        \"value_ms\": 14149,\n        \"passes_core_web_vital\": false,\n        \"element_selector\": \"div.heading > span\",\n        \"element_type\": \"text\",\n        \"element_content\": \"Welcome to Example\"\n      },\n      {\n        \"id\": \"fcp\",\n        \"score\": 0.53,\n        \"value_ms\": 1542,\n        \"passes_core_web_vital\": false\n      },\n      {\n        \"id\": \"si\",\n        \"score\": 0,\n        \"value_ms\": 6883\n      },\n      {\n        \"id\": \"tti\",\n        \"score\": 0,\n        \"value_ms\": 14746\n      },\n      {\n        \"id\": \"cls\",\n        \"score\": 1,\n        \"value_ms\": 0.001,\n        \"passes_core_web_vital\": true\n      },\n      {\n        \"id\": \"tbt\",\n        \"score\": 1,\n        \"value_ms\": 43,\n        \"passes_core_web_vital\": true\n      }\n    ],\n    \"opportunities\": [\n      {\n        \"id\": \"render_blocking_resources\",\n        \"savings_ms\": 1270,\n        \"severity\": \"serious\",\n        \"resources\": [\n          {\n            \"url\": \"styles.css\",\n            \"savings_ms\": 781\n          }\n        ]\n      }\n    ],\n    \"page_stats\": {\n      \"total_size_kb\": 2190,\n      \"total_requests\": 108,\n      \"resource_counts\": {\n        \"js\": 86,\n        \"css\": 1,\n        \"img\": 3,\n        \"font\": 3,\n        \"other\": 15\n      },\n      \"third_party_size_kb\": 2110,\n      \"main_thread_blocking_time_ms\": 693\n    },\n    \"prioritized_recommendations\": [\"Improve Largest Contentful Paint (LCP)\"]\n  }\n}\n```\n\n### Key Features\n\n- **Core Web Vitals Analysis**: LCP, FCP, CLS, TBT with pass/fail status\n- **Element Information for LCP**: Identifies what's causing the largest contentful paint\n- **Optimization Opportunities**: Specific actions to improve performance with estimated time savings\n- **Resource Breakdown**: By type, size, and origin (first vs. third party)\n- **Main Thread Analysis**: Blocking time metrics to identify JavaScript performance issues\n- **Resource-Specific Recommendations**: For each optimization opportunity\n\n## SEO Audit (`/seo-audit`)\n\nThe SEO audit checks search engine optimization best practices and identifies issues that could affect search ranking.\n\n### Response Format\n\n```json\n{\n  \"metadata\": {\n    \"url\": \"https://example.com\",\n    \"timestamp\": \"2025-03-06T16:29:12.455Z\",\n    \"device\": \"desktop\",\n    \"lighthouseVersion\": \"11.7.1\"\n  },\n  \"report\": {\n    \"score\": 91,\n    \"audit_counts\": {\n      \"failed\": 1,\n      \"passed\": 10,\n      \"manual\": 1,\n      \"informative\": 0,\n      \"not_applicable\": 3\n    },\n    \"issues\": [\n      {\n        \"id\": \"is-crawlable\",\n        \"title\": \"Page is blocked from indexing\",\n        \"impact\": \"critical\",\n        \"category\": \"crawlability\",\n        \"score\": 0\n      }\n    ],\n    \"categories\": {\n      \"content\": { \"score\": 0, \"issues_count\": 0 },\n      \"mobile\": { \"score\": 0, \"issues_count\": 0 },\n      \"crawlability\": { \"score\": 0, \"issues_count\": 1 },\n      \"other\": { \"score\": 0, \"issues_count\": 0 }\n    },\n    \"prioritized_recommendations\": [\n      \"Fix crawlability issues (1 issues): robots.txt, sitemaps, and redirects\"\n    ]\n  }\n}\n```\n\n### Key Features\n\n- **Issues Categorized by Impact**: Critical, serious, moderate, and minor\n- **SEO Categories**: Content, mobile friendliness, crawlability\n- **Issue Details**: Information about what's causing each SEO problem\n- **Prioritized Recommendations**: Actionable advice in order of importance\n\n## Best Practices Audit (`/best-practices-audit`)\n\nThe best practices audit evaluates adherence to web development best practices related to security, trust, user experience, and browser compatibility.\n\n### Response Format\n\n```json\n{\n  \"metadata\": {\n    \"url\": \"https://example.com\",\n    \"timestamp\": \"2025-03-06T17:01:38.029Z\",\n    \"device\": \"desktop\",\n    \"lighthouseVersion\": \"11.7.1\"\n  },\n  \"report\": {\n    \"score\": 74,\n    \"audit_counts\": {\n      \"failed\": 4,\n      \"passed\": 10,\n      \"manual\": 0,\n      \"informative\": 2,\n      \"not_applicable\": 1\n    },\n    \"issues\": [\n      {\n        \"id\": \"deprecations\",\n        \"title\": \"Uses deprecated APIs\",\n        \"impact\": \"critical\",\n        \"category\": \"security\",\n        \"score\": 0,\n        \"details\": [\n          {\n            \"value\": \"UnloadHandler\"\n          }\n        ]\n      },\n      {\n        \"id\": \"errors-in-console\",\n        \"title\": \"Browser errors were logged to the console\",\n        \"impact\": \"serious\",\n        \"category\": \"user-experience\",\n        \"score\": 0,\n        \"details\": [\n          {\n            \"source\": \"console.error\",\n            \"description\": \"ReferenceError: variable is not defined\"\n          }\n        ]\n      }\n    ],\n    \"categories\": {\n      \"security\": { \"score\": 75, \"issues_count\": 1 },\n      \"trust\": { \"score\": 100, \"issues_count\": 0 },\n      \"user-experience\": { \"score\": 50, \"issues_count\": 1 },\n      \"browser-compat\": { \"score\": 100, \"issues_count\": 0 },\n      \"other\": { \"score\": 75, \"issues_count\": 2 }\n    },\n    \"prioritized_recommendations\": [\n      \"Address 1 security issues: vulnerabilities, CSP, deprecations\",\n      \"Improve 1 user experience issues: console errors, user interactions\"\n    ]\n  }\n}\n```\n\n### Key Features\n\n- **Issues Categorized by Impact**: Critical, serious, moderate, and minor\n- **Best Practice Categories**: Security, trust, user experience, browser compatibility\n- **Detailed Issue Information**: Specific problems affecting best practices compliance\n- **Security Focus**: Special attention to security vulnerabilities and deprecated APIs\n- **Prioritized Recommendations**: Actionable advice in order of importance\n\n## License\n\nMIT\n\n# Puppeteer Service\n\nA comprehensive browser automation service built on Puppeteer to provide reliable cross-platform browser control capabilities.\n\n## Features\n\n- **Cross-Platform Browser Support**:\n\n  - Windows, macOS, and Linux support\n  - Chrome, Edge, Brave, and Firefox detection\n  - Fallback strategy for finding browser executables\n\n- **Smart Browser Management**:\n\n  - Singleton browser instance with automatic cleanup\n  - Connection retry mechanisms\n  - Temporary user data directories with cleanup\n\n- **Rich Configuration Options**:\n  - Custom browser paths\n  - Network condition emulation\n  - Device emulation (mobile, tablet, desktop)\n  - Resource blocking\n  - Cookies and headers customization\n  - Locale and timezone emulation\n"
  },
  {
    "path": "browser-tools-server/browser-connector.ts",
    "content": "#!/usr/bin/env node\n\nimport express from \"express\";\nimport cors from \"cors\";\nimport bodyParser from \"body-parser\";\nimport { tokenizeAndEstimateCost } from \"llm-cost\";\nimport { WebSocketServer, WebSocket } from \"ws\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport { IncomingMessage } from \"http\";\nimport { Socket } from \"net\";\nimport os from \"os\";\nimport { exec } from \"child_process\";\nimport {\n  runPerformanceAudit,\n  runAccessibilityAudit,\n  runSEOAudit,\n  AuditCategory,\n  LighthouseReport,\n} from \"./lighthouse/index.js\";\nimport * as net from \"net\";\nimport { runBestPracticesAudit } from \"./lighthouse/best-practices.js\";\n\n/**\n * Converts a file path to the appropriate format for the current platform\n * Handles Windows, WSL, macOS and Linux path formats\n *\n * @param inputPath - The path to convert\n * @returns The converted path appropriate for the current platform\n */\nfunction convertPathForCurrentPlatform(inputPath: string): string {\n  const platform = os.platform();\n\n  // If no path provided, return as is\n  if (!inputPath) return inputPath;\n\n  console.log(`Converting path \"${inputPath}\" for platform: ${platform}`);\n\n  // Windows-specific conversion\n  if (platform === \"win32\") {\n    // Convert forward slashes to backslashes\n    return inputPath.replace(/\\//g, \"\\\\\");\n  }\n\n  // Linux/Mac-specific conversion\n  if (platform === \"linux\" || platform === \"darwin\") {\n    // Check if this is a Windows UNC path (starts with \\\\)\n    if (inputPath.startsWith(\"\\\\\\\\\") || inputPath.includes(\"\\\\\")) {\n      // Check if this is a WSL path (contains wsl.localhost or wsl$)\n      if (inputPath.includes(\"wsl.localhost\") || inputPath.includes(\"wsl$\")) {\n        // Extract the path after the distribution name\n        // Handle both \\\\wsl.localhost\\Ubuntu\\path and \\\\wsl$\\Ubuntu\\path formats\n        const parts = inputPath.split(\"\\\\\").filter((part) => part.length > 0);\n        console.log(\"Path parts:\", parts);\n\n        // Find the index after the distribution name\n        const distNames = [\n          \"Ubuntu\",\n          \"Debian\",\n          \"kali\",\n          \"openSUSE\",\n          \"SLES\",\n          \"Fedora\",\n        ];\n\n        // Find the distribution name in the path\n        let distIndex = -1;\n        for (const dist of distNames) {\n          const index = parts.findIndex(\n            (part) => part === dist || part.toLowerCase() === dist.toLowerCase()\n          );\n          if (index !== -1) {\n            distIndex = index;\n            break;\n          }\n        }\n\n        if (distIndex !== -1 && distIndex + 1 < parts.length) {\n          // Reconstruct the path as a native Linux path\n          const linuxPath = \"/\" + parts.slice(distIndex + 1).join(\"/\");\n          console.log(\n            `Converted Windows WSL path \"${inputPath}\" to Linux path \"${linuxPath}\"`\n          );\n          return linuxPath;\n        }\n\n        // If we couldn't find a distribution name but it's clearly a WSL path,\n        // try to extract everything after wsl.localhost or wsl$\n        const wslIndex = parts.findIndex(\n          (part) =>\n            part === \"wsl.localhost\" ||\n            part === \"wsl$\" ||\n            part.toLowerCase() === \"wsl.localhost\" ||\n            part.toLowerCase() === \"wsl$\"\n        );\n\n        if (wslIndex !== -1 && wslIndex + 2 < parts.length) {\n          // Skip the WSL prefix and distribution name\n          const linuxPath = \"/\" + parts.slice(wslIndex + 2).join(\"/\");\n          console.log(\n            `Converted Windows WSL path \"${inputPath}\" to Linux path \"${linuxPath}\"`\n          );\n          return linuxPath;\n        }\n      }\n\n      // For non-WSL Windows paths, just normalize the slashes\n      const normalizedPath = inputPath\n        .replace(/\\\\\\\\/g, \"/\")\n        .replace(/\\\\/g, \"/\");\n      console.log(\n        `Converted Windows UNC path \"${inputPath}\" to \"${normalizedPath}\"`\n      );\n      return normalizedPath;\n    }\n\n    // Handle Windows drive letters (e.g., C:\\path\\to\\file)\n    if (/^[A-Z]:\\\\/i.test(inputPath)) {\n      // Convert Windows drive path to Linux/Mac compatible path\n      const normalizedPath = inputPath\n        .replace(/^[A-Z]:\\\\/i, \"/\")\n        .replace(/\\\\/g, \"/\");\n      console.log(\n        `Converted Windows drive path \"${inputPath}\" to \"${normalizedPath}\"`\n      );\n      return normalizedPath;\n    }\n  }\n\n  // Return the original path if no conversion was needed or possible\n  return inputPath;\n}\n\n// Function to get default downloads folder\nfunction getDefaultDownloadsFolder(): string {\n  const homeDir = os.homedir();\n  // Downloads folder is typically the same path on Windows, macOS, and Linux\n  const downloadsPath = path.join(homeDir, \"Downloads\", \"mcp-screenshots\");\n  return downloadsPath;\n}\n\n// We store logs in memory\nconst consoleLogs: any[] = [];\nconst consoleErrors: any[] = [];\nconst networkErrors: any[] = [];\nconst networkSuccess: any[] = [];\nconst allXhr: any[] = [];\n\n// Store the current URL from the extension\nlet currentUrl: string = \"\";\n\n// Store the current tab ID from the extension\nlet currentTabId: string | number | null = null;\n\n// Add settings state\nlet currentSettings = {\n  logLimit: 50,\n  queryLimit: 30000,\n  showRequestHeaders: false,\n  showResponseHeaders: false,\n  model: \"claude-3-sonnet\",\n  stringSizeLimit: 500,\n  maxLogSize: 20000,\n  screenshotPath: getDefaultDownloadsFolder(),\n  // Add server host configuration\n  serverHost: process.env.SERVER_HOST || \"0.0.0.0\", // Default to all interfaces\n};\n\n// Add new storage for selected element\nlet selectedElement: any = null;\n\n// Add new state for tracking screenshot requests\ninterface ScreenshotCallback {\n  resolve: (value: {\n    data: string;\n    path?: string;\n    autoPaste?: boolean;\n  }) => void;\n  reject: (reason: Error) => void;\n}\n\nconst screenshotCallbacks = new Map<string, ScreenshotCallback>();\n\n// Function to get available port starting with the given port\nasync function getAvailablePort(\n  startPort: number,\n  maxAttempts: number = 10\n): Promise<number> {\n  let currentPort = startPort;\n  let attempts = 0;\n\n  while (attempts < maxAttempts) {\n    try {\n      // Try to create a server on the current port\n      // We'll use a raw Node.js net server for just testing port availability\n      await new Promise<void>((resolve, reject) => {\n        const testServer = net.createServer();\n\n        // Handle errors (e.g., port in use)\n        testServer.once(\"error\", (err: any) => {\n          if (err.code === \"EADDRINUSE\") {\n            console.log(`Port ${currentPort} is in use, trying next port...`);\n            currentPort++;\n            attempts++;\n            resolve(); // Continue to next iteration\n          } else {\n            reject(err); // Different error, propagate it\n          }\n        });\n\n        // If we can listen, the port is available\n        testServer.once(\"listening\", () => {\n          // Make sure to close the server to release the port\n          testServer.close(() => {\n            console.log(`Found available port: ${currentPort}`);\n            resolve();\n          });\n        });\n\n        // Try to listen on the current port\n        testServer.listen(currentPort, currentSettings.serverHost);\n      });\n\n      // If we reach here without incrementing the port, it means the port is available\n      return currentPort;\n    } catch (error: any) {\n      console.error(`Error checking port ${currentPort}:`, error);\n      // For non-EADDRINUSE errors, try the next port\n      currentPort++;\n      attempts++;\n    }\n  }\n\n  // If we've exhausted all attempts, throw an error\n  throw new Error(\n    `Could not find an available port after ${maxAttempts} attempts starting from ${startPort}`\n  );\n}\n\n// Start with requested port and find an available one\nconst REQUESTED_PORT = parseInt(process.env.PORT || \"3025\", 10);\nlet PORT = REQUESTED_PORT;\n\n// Create application and initialize middleware\nconst app = express();\napp.use(cors());\n// Increase JSON body parser limit to 50MB to handle large screenshots\napp.use(bodyParser.json({ limit: \"50mb\" }));\napp.use(bodyParser.urlencoded({ limit: \"50mb\", extended: true }));\n\n// Helper to recursively truncate strings in any data structure\nfunction truncateStringsInData(data: any, maxLength: number): any {\n  if (typeof data === \"string\") {\n    return data.length > maxLength\n      ? data.substring(0, maxLength) + \"... (truncated)\"\n      : data;\n  }\n\n  if (Array.isArray(data)) {\n    return data.map((item) => truncateStringsInData(item, maxLength));\n  }\n\n  if (typeof data === \"object\" && data !== null) {\n    const result: any = {};\n    for (const [key, value] of Object.entries(data)) {\n      result[key] = truncateStringsInData(value, maxLength);\n    }\n    return result;\n  }\n\n  return data;\n}\n\n// Helper to safely parse and process JSON strings\nfunction processJsonString(jsonString: string, maxLength: number): string {\n  try {\n    // Try to parse the string as JSON\n    const parsed = JSON.parse(jsonString);\n    // Process any strings within the parsed JSON\n    const processed = truncateStringsInData(parsed, maxLength);\n    // Stringify the processed data\n    return JSON.stringify(processed);\n  } catch (e) {\n    // If it's not valid JSON, treat it as a regular string\n    return truncateStringsInData(jsonString, maxLength);\n  }\n}\n\n// Helper to process logs based on settings\nfunction processLogsWithSettings(logs: any[]) {\n  return logs.map((log) => {\n    const processedLog = { ...log };\n\n    if (log.type === \"network-request\") {\n      // Handle headers visibility\n      if (!currentSettings.showRequestHeaders) {\n        delete processedLog.requestHeaders;\n      }\n      if (!currentSettings.showResponseHeaders) {\n        delete processedLog.responseHeaders;\n      }\n    }\n\n    return processedLog;\n  });\n}\n\n// Helper to calculate size of a log entry\nfunction calculateLogSize(log: any): number {\n  return JSON.stringify(log).length;\n}\n\n// Helper to truncate logs based on character limit\nfunction truncateLogsToQueryLimit(logs: any[]): any[] {\n  if (logs.length === 0) return logs;\n\n  // First process logs according to current settings\n  const processedLogs = processLogsWithSettings(logs);\n\n  let currentSize = 0;\n  const result = [];\n\n  for (const log of processedLogs) {\n    const logSize = calculateLogSize(log);\n\n    // Check if adding this log would exceed the limit\n    if (currentSize + logSize > currentSettings.queryLimit) {\n      console.log(\n        `Reached query limit (${currentSize}/${currentSettings.queryLimit}), truncating logs`\n      );\n      break;\n    }\n\n    // Add log and update size\n    result.push(log);\n    currentSize += logSize;\n    console.log(`Added log of size ${logSize}, total size now: ${currentSize}`);\n  }\n\n  return result;\n}\n\n// Endpoint for the extension to POST data\napp.post(\"/extension-log\", (req, res) => {\n  console.log(\"\\n=== Received Extension Log ===\");\n  console.log(\"Request body:\", {\n    dataType: req.body.data?.type,\n    timestamp: req.body.data?.timestamp,\n    hasSettings: !!req.body.settings,\n  });\n\n  const { data, settings } = req.body;\n\n  // Update settings if provided\n  if (settings) {\n    console.log(\"Updating settings:\", settings);\n    currentSettings = {\n      ...currentSettings,\n      ...settings,\n    };\n  }\n\n  if (!data) {\n    console.log(\"Warning: No data received in log request\");\n    res.status(400).json({ status: \"error\", message: \"No data provided\" });\n    return;\n  }\n\n  console.log(`Processing ${data.type} log entry`);\n\n  switch (data.type) {\n    case \"page-navigated\":\n      // Handle page navigation event via HTTP POST\n      // Note: This is also handled in the WebSocket message handler\n      // as the extension may send navigation events through either channel\n      console.log(\"Received page navigation event with URL:\", data.url);\n      currentUrl = data.url;\n\n      // Also update the tab ID if provided\n      if (data.tabId) {\n        console.log(\"Updating tab ID from page navigation event:\", data.tabId);\n        currentTabId = data.tabId;\n      }\n\n      console.log(\"Updated current URL:\", currentUrl);\n      break;\n    case \"console-log\":\n      console.log(\"Adding console log:\", {\n        level: data.level,\n        message:\n          data.message?.substring(0, 100) +\n          (data.message?.length > 100 ? \"...\" : \"\"),\n        timestamp: data.timestamp,\n      });\n      consoleLogs.push(data);\n      if (consoleLogs.length > currentSettings.logLimit) {\n        console.log(\n          `Console logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`\n        );\n        consoleLogs.shift();\n      }\n      break;\n    case \"console-error\":\n      console.log(\"Adding console error:\", {\n        level: data.level,\n        message:\n          data.message?.substring(0, 100) +\n          (data.message?.length > 100 ? \"...\" : \"\"),\n        timestamp: data.timestamp,\n      });\n      consoleErrors.push(data);\n      if (consoleErrors.length > currentSettings.logLimit) {\n        console.log(\n          `Console errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`\n        );\n        consoleErrors.shift();\n      }\n      break;\n    case \"network-request\":\n      const logEntry = {\n        url: data.url,\n        method: data.method,\n        status: data.status,\n        timestamp: data.timestamp,\n      };\n      console.log(\"Adding network request:\", logEntry);\n\n      // Route network requests based on status code\n      if (data.status >= 400) {\n        networkErrors.push(data);\n        if (networkErrors.length > currentSettings.logLimit) {\n          console.log(\n            `Network errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`\n          );\n          networkErrors.shift();\n        }\n      } else {\n        networkSuccess.push(data);\n        if (networkSuccess.length > currentSettings.logLimit) {\n          console.log(\n            `Network success logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`\n          );\n          networkSuccess.shift();\n        }\n      }\n      break;\n    case \"selected-element\":\n      console.log(\"Updating selected element:\", {\n        tagName: data.element?.tagName,\n        id: data.element?.id,\n        className: data.element?.className,\n      });\n      selectedElement = data.element;\n      break;\n    default:\n      console.log(\"Unknown log type:\", data.type);\n  }\n\n  console.log(\"Current log counts:\", {\n    consoleLogs: consoleLogs.length,\n    consoleErrors: consoleErrors.length,\n    networkErrors: networkErrors.length,\n    networkSuccess: networkSuccess.length,\n  });\n  console.log(\"=== End Extension Log ===\\n\");\n\n  res.json({ status: \"ok\" });\n});\n\n// Update GET endpoints to use the new function\napp.get(\"/console-logs\", (req, res) => {\n  const truncatedLogs = truncateLogsToQueryLimit(consoleLogs);\n  res.json(truncatedLogs);\n});\n\napp.get(\"/console-errors\", (req, res) => {\n  const truncatedLogs = truncateLogsToQueryLimit(consoleErrors);\n  res.json(truncatedLogs);\n});\n\napp.get(\"/network-errors\", (req, res) => {\n  const truncatedLogs = truncateLogsToQueryLimit(networkErrors);\n  res.json(truncatedLogs);\n});\n\napp.get(\"/network-success\", (req, res) => {\n  const truncatedLogs = truncateLogsToQueryLimit(networkSuccess);\n  res.json(truncatedLogs);\n});\n\napp.get(\"/all-xhr\", (req, res) => {\n  // Merge and sort network success and error logs by timestamp\n  const mergedLogs = [...networkSuccess, ...networkErrors].sort(\n    (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()\n  );\n  const truncatedLogs = truncateLogsToQueryLimit(mergedLogs);\n  res.json(truncatedLogs);\n});\n\n// Add new endpoint for selected element\napp.post(\"/selected-element\", (req, res) => {\n  const { data } = req.body;\n  selectedElement = data;\n  res.json({ status: \"ok\" });\n});\n\napp.get(\"/selected-element\", (req, res) => {\n  res.json(selectedElement || { message: \"No element selected\" });\n});\n\napp.get(\"/.port\", (req, res) => {\n  res.send(PORT.toString());\n});\n\n// Add new identity endpoint with a unique signature\napp.get(\"/.identity\", (req, res) => {\n  res.json({\n    port: PORT,\n    name: \"browser-tools-server\",\n    version: \"1.2.0\",\n    signature: \"mcp-browser-connector-24x7\",\n  });\n});\n\n// Add function to clear all logs\nfunction clearAllLogs() {\n  console.log(\"Wiping all logs...\");\n  consoleLogs.length = 0;\n  consoleErrors.length = 0;\n  networkErrors.length = 0;\n  networkSuccess.length = 0;\n  allXhr.length = 0;\n  selectedElement = null;\n  console.log(\"All logs have been wiped\");\n}\n\n// Add endpoint to wipe logs\napp.post(\"/wipelogs\", (req, res) => {\n  clearAllLogs();\n  res.json({ status: \"ok\", message: \"All logs cleared successfully\" });\n});\n\n// Add endpoint for the extension to report the current URL\napp.post(\"/current-url\", (req, res) => {\n  console.log(\n    \"Received current URL update request:\",\n    JSON.stringify(req.body, null, 2)\n  );\n\n  if (req.body && req.body.url) {\n    const oldUrl = currentUrl;\n    currentUrl = req.body.url;\n\n    // Update the current tab ID if provided\n    if (req.body.tabId) {\n      const oldTabId = currentTabId;\n      currentTabId = req.body.tabId;\n      console.log(`Updated current tab ID: ${oldTabId} -> ${currentTabId}`);\n    }\n\n    // Log the source of the update if provided\n    const source = req.body.source || \"unknown\";\n    const tabId = req.body.tabId || \"unknown\";\n    const timestamp = req.body.timestamp\n      ? new Date(req.body.timestamp).toISOString()\n      : \"unknown\";\n\n    console.log(\n      `Updated current URL via dedicated endpoint: ${oldUrl} -> ${currentUrl}`\n    );\n    console.log(\n      `URL update details: source=${source}, tabId=${tabId}, timestamp=${timestamp}`\n    );\n\n    res.json({\n      status: \"ok\",\n      url: currentUrl,\n      tabId: currentTabId,\n      previousUrl: oldUrl,\n      updated: oldUrl !== currentUrl,\n    });\n  } else {\n    console.log(\"No URL provided in current-url request\");\n    res.status(400).json({ status: \"error\", message: \"No URL provided\" });\n  }\n});\n\n// Add endpoint to get the current URL\napp.get(\"/current-url\", (req, res) => {\n  console.log(\"Current URL requested, returning:\", currentUrl);\n  res.json({ url: currentUrl });\n});\n\ninterface ScreenshotMessage {\n  type: \"screenshot-data\" | \"screenshot-error\";\n  data?: string;\n  path?: string;\n  error?: string;\n  autoPaste?: boolean;\n}\n\nexport class BrowserConnector {\n  private wss: WebSocketServer;\n  private activeConnection: WebSocket | null = null;\n  private app: express.Application;\n  private server: any;\n  private urlRequestCallbacks: Map<string, (url: string) => void> = new Map();\n\n  constructor(app: express.Application, server: any) {\n    this.app = app;\n    this.server = server;\n\n    // Initialize WebSocket server using the existing HTTP server\n    this.wss = new WebSocketServer({\n      noServer: true,\n      path: \"/extension-ws\",\n    });\n\n    // Register the capture-screenshot endpoint\n    this.app.post(\n      \"/capture-screenshot\",\n      async (req: express.Request, res: express.Response) => {\n        console.log(\n          \"Browser Connector: Received request to /capture-screenshot endpoint\"\n        );\n        console.log(\"Browser Connector: Request body:\", req.body);\n        console.log(\n          \"Browser Connector: Active WebSocket connection:\",\n          !!this.activeConnection\n        );\n        await this.captureScreenshot(req, res);\n      }\n    );\n\n    // Set up accessibility audit endpoint\n    this.setupAccessibilityAudit();\n\n    // Set up performance audit endpoint\n    this.setupPerformanceAudit();\n\n    // Set up SEO audit endpoint\n    this.setupSEOAudit();\n\n    // Set up Best Practices audit endpoint\n    this.setupBestPracticesAudit();\n\n    // Handle upgrade requests for WebSocket\n    this.server.on(\n      \"upgrade\",\n      (request: IncomingMessage, socket: Socket, head: Buffer) => {\n        if (request.url === \"/extension-ws\") {\n          this.wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {\n            this.wss.emit(\"connection\", ws, request);\n          });\n        }\n      }\n    );\n\n    this.wss.on(\"connection\", (ws: WebSocket) => {\n      console.log(\"Chrome extension connected via WebSocket\");\n      this.activeConnection = ws;\n\n      ws.on(\"message\", (message: string | Buffer | ArrayBuffer | Buffer[]) => {\n        try {\n          const data = JSON.parse(message.toString());\n          // Log message without the base64 data\n          console.log(\"Received WebSocket message:\", {\n            ...data,\n            data: data.data ? \"[base64 data]\" : undefined,\n          });\n\n          // Handle URL response\n          if (data.type === \"current-url-response\" && data.url) {\n            console.log(\"Received current URL from browser:\", data.url);\n            currentUrl = data.url;\n\n            // Also update the tab ID if provided\n            if (data.tabId) {\n              console.log(\n                \"Updating tab ID from WebSocket message:\",\n                data.tabId\n              );\n              currentTabId = data.tabId;\n            }\n\n            // Call the callback if exists\n            if (\n              data.requestId &&\n              this.urlRequestCallbacks.has(data.requestId)\n            ) {\n              const callback = this.urlRequestCallbacks.get(data.requestId);\n              if (callback) callback(data.url);\n              this.urlRequestCallbacks.delete(data.requestId);\n            }\n          }\n          // Handle page navigation event via WebSocket\n          // Note: This is intentionally duplicated from the HTTP handler in /extension-log\n          // as the extension may send navigation events through either channel\n          if (data.type === \"page-navigated\" && data.url) {\n            console.log(\"Page navigated to:\", data.url);\n            currentUrl = data.url;\n\n            // Also update the tab ID if provided\n            if (data.tabId) {\n              console.log(\n                \"Updating tab ID from page navigation event:\",\n                data.tabId\n              );\n              currentTabId = data.tabId;\n            }\n          }\n          // Handle screenshot response\n          if (data.type === \"screenshot-data\" && data.data) {\n            console.log(\"Received screenshot data\");\n            console.log(\"Screenshot path from extension:\", data.path);\n            console.log(\"Auto-paste setting from extension:\", data.autoPaste);\n            // Get the most recent callback since we're not using requestId anymore\n            const callbacks = Array.from(screenshotCallbacks.values());\n            if (callbacks.length > 0) {\n              const callback = callbacks[0];\n              console.log(\"Found callback, resolving promise\");\n              // Pass both the data, path and autoPaste to the resolver\n              callback.resolve({\n                data: data.data,\n                path: data.path,\n                autoPaste: data.autoPaste,\n              });\n              screenshotCallbacks.clear(); // Clear all callbacks\n            } else {\n              console.log(\"No callbacks found for screenshot\");\n            }\n          }\n          // Handle screenshot error\n          else if (data.type === \"screenshot-error\") {\n            console.log(\"Received screenshot error:\", data.error);\n            const callbacks = Array.from(screenshotCallbacks.values());\n            if (callbacks.length > 0) {\n              const callback = callbacks[0];\n              callback.reject(\n                new Error(data.error || \"Screenshot capture failed\")\n              );\n              screenshotCallbacks.clear(); // Clear all callbacks\n            }\n          } else {\n            console.log(\"Unhandled message type:\", data.type);\n          }\n        } catch (error) {\n          console.error(\"Error processing WebSocket message:\", error);\n        }\n      });\n\n      ws.on(\"close\", () => {\n        console.log(\"Chrome extension disconnected\");\n        if (this.activeConnection === ws) {\n          this.activeConnection = null;\n        }\n      });\n    });\n\n    // Add screenshot endpoint\n    this.app.post(\n      \"/screenshot\",\n      (req: express.Request, res: express.Response): void => {\n        console.log(\n          \"Browser Connector: Received request to /screenshot endpoint\"\n        );\n        console.log(\"Browser Connector: Request body:\", req.body);\n        try {\n          console.log(\"Received screenshot capture request\");\n          const { data, path: outputPath } = req.body;\n\n          if (!data) {\n            console.log(\"Screenshot request missing data\");\n            res.status(400).json({ error: \"Missing screenshot data\" });\n            return;\n          }\n\n          // Use provided path or default to downloads folder\n          const targetPath = outputPath || getDefaultDownloadsFolder();\n          console.log(`Using screenshot path: ${targetPath}`);\n\n          // Remove the data:image/png;base64, prefix\n          const base64Data = data.replace(/^data:image\\/png;base64,/, \"\");\n\n          // Create the full directory path if it doesn't exist\n          fs.mkdirSync(targetPath, { recursive: true });\n          console.log(`Created/verified directory: ${targetPath}`);\n\n          // Generate a unique filename using timestamp\n          const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n          const filename = `screenshot-${timestamp}.png`;\n          const fullPath = path.join(targetPath, filename);\n          console.log(`Saving screenshot to: ${fullPath}`);\n\n          // Write the file\n          fs.writeFileSync(fullPath, base64Data, \"base64\");\n          console.log(\"Screenshot saved successfully\");\n\n          res.json({\n            path: fullPath,\n            filename: filename,\n          });\n        } catch (error: unknown) {\n          console.error(\"Error saving screenshot:\", error);\n          if (error instanceof Error) {\n            res.status(500).json({ error: error.message });\n          } else {\n            res.status(500).json({ error: \"An unknown error occurred\" });\n          }\n        }\n      }\n    );\n  }\n\n  private async handleScreenshot(req: express.Request, res: express.Response) {\n    if (!this.activeConnection) {\n      return res.status(503).json({ error: \"Chrome extension not connected\" });\n    }\n\n    try {\n      const result = await new Promise((resolve, reject) => {\n        // Set up one-time message handler for this screenshot request\n        const messageHandler = (\n          message: string | Buffer | ArrayBuffer | Buffer[]\n        ) => {\n          try {\n            const response: ScreenshotMessage = JSON.parse(message.toString());\n\n            if (response.type === \"screenshot-error\") {\n              reject(new Error(response.error));\n              return;\n            }\n\n            if (\n              response.type === \"screenshot-data\" &&\n              response.data &&\n              response.path\n            ) {\n              // Remove the data:image/png;base64, prefix\n              const base64Data = response.data.replace(\n                /^data:image\\/png;base64,/,\n                \"\"\n              );\n\n              // Ensure the directory exists\n              const dir = path.dirname(response.path);\n              fs.mkdirSync(dir, { recursive: true });\n\n              // Generate a unique filename using timestamp\n              const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n              const filename = `screenshot-${timestamp}.png`;\n              const fullPath = path.join(response.path, filename);\n\n              // Write the file\n              fs.writeFileSync(fullPath, base64Data, \"base64\");\n              resolve({\n                path: fullPath,\n                filename: filename,\n              });\n            }\n          } catch (error) {\n            reject(error);\n          } finally {\n            this.activeConnection?.removeListener(\"message\", messageHandler);\n          }\n        };\n\n        // Add temporary message handler\n        this.activeConnection?.on(\"message\", messageHandler);\n\n        // Request screenshot\n        this.activeConnection?.send(\n          JSON.stringify({ type: \"take-screenshot\" })\n        );\n\n        // Set timeout\n        setTimeout(() => {\n          this.activeConnection?.removeListener(\"message\", messageHandler);\n          reject(new Error(\"Screenshot timeout\"));\n        }, 30000); // 30 second timeout\n      });\n\n      res.json(result);\n    } catch (error: unknown) {\n      if (error instanceof Error) {\n        res.status(500).json({ error: error.message });\n      } else {\n        res.status(500).json({ error: \"An unknown error occurred\" });\n      }\n    }\n  }\n\n  // Updated method to get URL for audits with improved connection tracking and waiting\n  private async getUrlForAudit(): Promise<string | null> {\n    try {\n      console.log(\"getUrlForAudit called\");\n\n      // Use the stored URL if available immediately\n      if (currentUrl && currentUrl !== \"\" && currentUrl !== \"about:blank\") {\n        console.log(`Using existing URL immediately: ${currentUrl}`);\n        return currentUrl;\n      }\n\n      // Wait for a URL to become available (retry loop)\n      console.log(\"No valid URL available yet, waiting for navigation...\");\n\n      // Wait up to 10 seconds for a URL to be set (20 attempts x 500ms)\n      const maxAttempts = 50;\n      const waitTime = 500; // ms\n\n      for (let attempt = 0; attempt < maxAttempts; attempt++) {\n        // Check if URL is available now\n        if (currentUrl && currentUrl !== \"\" && currentUrl !== \"about:blank\") {\n          console.log(`URL became available after waiting: ${currentUrl}`);\n          return currentUrl;\n        }\n\n        // Wait before checking again\n        console.log(\n          `Waiting for URL (attempt ${attempt + 1}/${maxAttempts})...`\n        );\n        await new Promise((resolve) => setTimeout(resolve, waitTime));\n      }\n\n      // If we reach here, no URL became available after waiting\n      console.log(\"Timed out waiting for URL, returning null\");\n      return null;\n    } catch (error) {\n      console.error(\"Error in getUrlForAudit:\", error);\n      return null; // Return null to trigger an error\n    }\n  }\n\n  // Public method to check if there's an active connection\n  public hasActiveConnection(): boolean {\n    return this.activeConnection !== null;\n  }\n\n  // Add new endpoint for programmatic screenshot capture\n  async captureScreenshot(req: express.Request, res: express.Response) {\n    console.log(\"Browser Connector: Starting captureScreenshot method\");\n    console.log(\"Browser Connector: Request headers:\", req.headers);\n    console.log(\"Browser Connector: Request method:\", req.method);\n\n    if (!this.activeConnection) {\n      console.log(\n        \"Browser Connector: No active WebSocket connection to Chrome extension\"\n      );\n      return res.status(503).json({ error: \"Chrome extension not connected\" });\n    }\n\n    try {\n      console.log(\"Browser Connector: Starting screenshot capture...\");\n      const requestId = Date.now().toString();\n      console.log(\"Browser Connector: Generated requestId:\", requestId);\n\n      // Create promise that will resolve when we get the screenshot data\n      const screenshotPromise = new Promise<{\n        data: string;\n        path?: string;\n        autoPaste?: boolean;\n      }>((resolve, reject) => {\n        console.log(\n          `Browser Connector: Setting up screenshot callback for requestId: ${requestId}`\n        );\n        // Store callback in map\n        screenshotCallbacks.set(requestId, { resolve, reject });\n        console.log(\n          \"Browser Connector: Current callbacks:\",\n          Array.from(screenshotCallbacks.keys())\n        );\n\n        // Set timeout to clean up if we don't get a response\n        setTimeout(() => {\n          if (screenshotCallbacks.has(requestId)) {\n            console.log(\n              `Browser Connector: Screenshot capture timed out for requestId: ${requestId}`\n            );\n            screenshotCallbacks.delete(requestId);\n            reject(\n              new Error(\n                \"Screenshot capture timed out - no response from Chrome extension\"\n              )\n            );\n          }\n        }, 10000);\n      });\n\n      // Send screenshot request to extension\n      const message = JSON.stringify({\n        type: \"take-screenshot\",\n        requestId: requestId,\n      });\n      console.log(\n        `Browser Connector: Sending WebSocket message to extension:`,\n        message\n      );\n      this.activeConnection.send(message);\n\n      // Wait for screenshot data\n      console.log(\"Browser Connector: Waiting for screenshot data...\");\n      const {\n        data: base64Data,\n        path: customPath,\n        autoPaste,\n      } = await screenshotPromise;\n      console.log(\"Browser Connector: Received screenshot data, saving...\");\n      console.log(\"Browser Connector: Custom path from extension:\", customPath);\n      console.log(\"Browser Connector: Auto-paste setting:\", autoPaste);\n\n      // Always prioritize the path from the Chrome extension\n      let targetPath = customPath;\n\n      // If no path provided by extension, fall back to defaults\n      if (!targetPath) {\n        targetPath =\n          currentSettings.screenshotPath || getDefaultDownloadsFolder();\n      }\n\n      // Convert the path for the current platform\n      targetPath = convertPathForCurrentPlatform(targetPath);\n\n      console.log(`Browser Connector: Using path: ${targetPath}`);\n\n      if (!base64Data) {\n        throw new Error(\"No screenshot data received from Chrome extension\");\n      }\n\n      try {\n        fs.mkdirSync(targetPath, { recursive: true });\n        console.log(`Browser Connector: Created directory: ${targetPath}`);\n      } catch (err) {\n        console.error(\n          `Browser Connector: Error creating directory: ${targetPath}`,\n          err\n        );\n        throw new Error(\n          `Failed to create screenshot directory: ${\n            err instanceof Error ? err.message : String(err)\n          }`\n        );\n      }\n\n      const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n      const filename = `screenshot-${timestamp}.png`;\n      const fullPath = path.join(targetPath, filename);\n      console.log(`Browser Connector: Full screenshot path: ${fullPath}`);\n\n      // Remove the data:image/png;base64, prefix if present\n      const cleanBase64 = base64Data.replace(/^data:image\\/png;base64,/, \"\");\n\n      // Save the file\n      try {\n        fs.writeFileSync(fullPath, cleanBase64, \"base64\");\n        console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);\n      } catch (err) {\n        console.error(\n          `Browser Connector: Error saving screenshot to: ${fullPath}`,\n          err\n        );\n        throw new Error(\n          `Failed to save screenshot: ${\n            err instanceof Error ? err.message : String(err)\n          }`\n        );\n      }\n\n      // Check if running on macOS before executing AppleScript\n      if (os.platform() === \"darwin\" && autoPaste === true) {\n        console.log(\n          \"Browser Connector: Running on macOS with auto-paste enabled, executing AppleScript to paste into Cursor\"\n        );\n\n        // Create the AppleScript to copy the image to clipboard and paste into Cursor\n        // This version is more robust and includes debugging\n        const appleScript = `\n          -- Set path to the screenshot\n          set imagePath to \"${fullPath}\"\n          \n          -- Copy the image to clipboard\n          try\n            set the clipboard to (read (POSIX file imagePath) as «class PNGf»)\n          on error errMsg\n            log \"Error copying image to clipboard: \" & errMsg\n            return \"Failed to copy image to clipboard: \" & errMsg\n          end try\n          \n          -- Activate Cursor application\n          try\n            tell application \"Cursor\"\n              activate\n            end tell\n          on error errMsg\n            log \"Error activating Cursor: \" & errMsg\n            return \"Failed to activate Cursor: \" & errMsg\n          end try\n          \n          -- Wait for the application to fully activate\n          delay 3\n          \n          -- Try to interact with Cursor\n          try\n            tell application \"System Events\"\n              tell process \"Cursor\"\n                -- Get the frontmost window\n                if (count of windows) is 0 then\n                  return \"No windows found in Cursor\"\n                end if\n                \n                set cursorWindow to window 1\n                \n                -- Try Method 1: Look for elements of class \"Text Area\"\n                set foundElements to {}\n                \n                -- Try different selectors to find the text input area\n                try\n                  -- Try with class\n                  set textAreas to UI elements of cursorWindow whose class is \"Text Area\"\n                  if (count of textAreas) > 0 then\n                    set foundElements to textAreas\n                  end if\n                end try\n                \n                if (count of foundElements) is 0 then\n                  try\n                    -- Try with AXTextField role\n                    set textFields to UI elements of cursorWindow whose role is \"AXTextField\"\n                    if (count of textFields) > 0 then\n                      set foundElements to textFields\n                    end if\n                  end try\n                end if\n                \n                if (count of foundElements) is 0 then\n                  try\n                    -- Try with AXTextArea role in nested elements\n                    set allElements to UI elements of cursorWindow\n                    repeat with anElement in allElements\n                      try\n                        set childElements to UI elements of anElement\n                        repeat with aChild in childElements\n                          try\n                            if role of aChild is \"AXTextArea\" or role of aChild is \"AXTextField\" then\n                              set end of foundElements to aChild\n                            end if\n                          end try\n                        end repeat\n                      end try\n                    end repeat\n                  end try\n                end if\n                \n                -- If no elements found with specific attributes, try a broader approach\n                if (count of foundElements) is 0 then\n                  -- Just try to use the Command+V shortcut on the active window\n                   -- This assumes Cursor already has focus on the right element\n                    keystroke \"v\" using command down\n                    delay 1\n                    keystroke \"here is the screenshot\"\n                    delay 1\n                   -- Try multiple methods to press Enter\n                   key code 36 -- Use key code for Return key\n                   delay 0.5\n                   keystroke return -- Use keystroke return as alternative\n                   return \"Used fallback method: Command+V on active window\"\n                else\n                  -- We found a potential text input element\n                  set inputElement to item 1 of foundElements\n                  \n                  -- Try to focus and paste\n                  try\n                    set focused of inputElement to true\n                    delay 0.5\n                    \n                    -- Paste the image\n                    keystroke \"v\" using command down\n                    delay 1\n                    \n                    -- Type the text\n                    keystroke \"here is the screenshot\"\n                    delay 1\n                    -- Try multiple methods to press Enter\n                    key code 36 -- Use key code for Return key\n                    delay 0.5\n                    keystroke return -- Use keystroke return as alternative\n                    return \"Successfully pasted screenshot into Cursor text element\"\n                  on error errMsg\n                    log \"Error interacting with found element: \" & errMsg\n                    -- Fallback to just sending the key commands\n                    keystroke \"v\" using command down\n                    delay 1\n                    keystroke \"here is the screenshot\"\n                    delay 1\n                    -- Try multiple methods to press Enter\n                    key code 36 -- Use key code for Return key\n                    delay 0.5\n                    keystroke return -- Use keystroke return as alternative\n                    return \"Used fallback after element focus error: \" & errMsg\n                  end try\n                end if\n              end tell\n            end tell\n          on error errMsg\n            log \"Error in System Events block: \" & errMsg\n            return \"Failed in System Events: \" & errMsg\n          end try\n        `;\n\n        // Execute the AppleScript\n        exec(`osascript -e '${appleScript}'`, (error, stdout, stderr) => {\n          if (error) {\n            console.error(\n              `Browser Connector: Error executing AppleScript: ${error.message}`\n            );\n            console.error(`Browser Connector: stderr: ${stderr}`);\n            // Don't fail the response; log the error and proceed\n          } else {\n            console.log(`Browser Connector: AppleScript executed successfully`);\n            console.log(`Browser Connector: stdout: ${stdout}`);\n          }\n        });\n      } else {\n        if (os.platform() === \"darwin\" && !autoPaste) {\n          console.log(\n            `Browser Connector: Running on macOS but auto-paste is disabled, skipping AppleScript execution`\n          );\n        } else {\n          console.log(\n            `Browser Connector: Not running on macOS, skipping AppleScript execution`\n          );\n        }\n      }\n\n      res.json({\n        path: fullPath,\n        filename: filename,\n      });\n    } catch (error) {\n      const errorMessage =\n        error instanceof Error ? error.message : String(error);\n      console.error(\n        \"Browser Connector: Error capturing screenshot:\",\n        errorMessage\n      );\n      res.status(500).json({\n        error: errorMessage,\n      });\n    }\n  }\n\n  // Add shutdown method\n  public shutdown() {\n    return new Promise<void>((resolve) => {\n      console.log(\"Shutting down WebSocket server...\");\n\n      // Send close message to client if connection is active\n      if (\n        this.activeConnection &&\n        this.activeConnection.readyState === WebSocket.OPEN\n      ) {\n        console.log(\"Notifying client to close connection...\");\n        try {\n          this.activeConnection.send(\n            JSON.stringify({ type: \"server-shutdown\" })\n          );\n        } catch (err) {\n          console.error(\"Error sending shutdown message to client:\", err);\n        }\n      }\n\n      // Set a timeout to force close after 2 seconds\n      const forceCloseTimeout = setTimeout(() => {\n        console.log(\"Force closing connections after timeout...\");\n        if (this.activeConnection) {\n          this.activeConnection.terminate(); // Force close the connection\n          this.activeConnection = null;\n        }\n        this.wss.close();\n        resolve();\n      }, 2000);\n\n      // Close active WebSocket connection if exists\n      if (this.activeConnection) {\n        this.activeConnection.close(1000, \"Server shutting down\");\n        this.activeConnection = null;\n      }\n\n      // Close WebSocket server\n      this.wss.close(() => {\n        clearTimeout(forceCloseTimeout);\n        console.log(\"WebSocket server closed gracefully\");\n        resolve();\n      });\n    });\n  }\n\n  // Sets up the accessibility audit endpoint\n  private setupAccessibilityAudit() {\n    this.setupAuditEndpoint(\n      AuditCategory.ACCESSIBILITY,\n      \"/accessibility-audit\",\n      runAccessibilityAudit\n    );\n  }\n\n  // Sets up the performance audit endpoint\n  private setupPerformanceAudit() {\n    this.setupAuditEndpoint(\n      AuditCategory.PERFORMANCE,\n      \"/performance-audit\",\n      runPerformanceAudit\n    );\n  }\n\n  // Set up SEO audit endpoint\n  private setupSEOAudit() {\n    this.setupAuditEndpoint(AuditCategory.SEO, \"/seo-audit\", runSEOAudit);\n  }\n\n  // Add a setup method for Best Practices audit\n  private setupBestPracticesAudit() {\n    this.setupAuditEndpoint(\n      AuditCategory.BEST_PRACTICES,\n      \"/best-practices-audit\",\n      runBestPracticesAudit\n    );\n  }\n\n  /**\n   * Generic method to set up an audit endpoint\n   * @param auditType The type of audit (accessibility, performance, SEO)\n   * @param endpoint The endpoint path\n   * @param auditFunction The audit function to call\n   */\n  private setupAuditEndpoint(\n    auditType: string,\n    endpoint: string,\n    auditFunction: (url: string) => Promise<LighthouseReport>\n  ) {\n    // Add server identity validation endpoint\n    this.app.get(\"/.identity\", (req, res) => {\n      res.json({\n        signature: \"mcp-browser-connector-24x7\",\n        version: \"1.2.0\",\n      });\n    });\n\n    this.app.post(endpoint, async (req: any, res: any) => {\n      try {\n        console.log(`${auditType} audit request received`);\n\n        // Get URL using our helper method\n        const url = await this.getUrlForAudit();\n\n        if (!url) {\n          console.log(`No URL available for ${auditType} audit`);\n          return res.status(400).json({\n            error: `URL is required for ${auditType} audit. Make sure you navigate to a page in the browser first, and the browser-tool extension tab is open.`,\n          });\n        }\n\n        // If we're using the stored URL (not from request body), log it now\n        if (!req.body?.url && url === currentUrl) {\n          console.log(`Using stored URL for ${auditType} audit:`, url);\n        }\n\n        // Check if we're using the default URL\n        if (url === \"about:blank\") {\n          console.log(`Cannot run ${auditType} audit on about:blank`);\n          return res.status(400).json({\n            error: `Cannot run ${auditType} audit on about:blank`,\n          });\n        }\n\n        console.log(`Preparing to run ${auditType} audit for: ${url}`);\n\n        // Run the audit using the provided function\n        try {\n          const result = await auditFunction(url);\n\n          console.log(`${auditType} audit completed successfully`);\n          // Return the results\n          res.json(result);\n        } catch (auditError) {\n          console.error(`${auditType} audit failed:`, auditError);\n          const errorMessage =\n            auditError instanceof Error\n              ? auditError.message\n              : String(auditError);\n          res.status(500).json({\n            error: `Failed to run ${auditType} audit: ${errorMessage}`,\n          });\n        }\n      } catch (error) {\n        console.error(`Error in ${auditType} audit endpoint:`, error);\n        const errorMessage =\n          error instanceof Error ? error.message : String(error);\n        res.status(500).json({\n          error: `Error in ${auditType} audit endpoint: ${errorMessage}`,\n        });\n      }\n    });\n  }\n}\n\n// Use an async IIFE to allow for async/await in the initial setup\n(async () => {\n  try {\n    console.log(`Starting Browser Tools Server...`);\n    console.log(`Requested port: ${REQUESTED_PORT}`);\n\n    // Find an available port\n    try {\n      PORT = await getAvailablePort(REQUESTED_PORT);\n\n      if (PORT !== REQUESTED_PORT) {\n        console.log(`\\n====================================`);\n        console.log(`NOTICE: Requested port ${REQUESTED_PORT} was in use.`);\n        console.log(`Using port ${PORT} instead.`);\n        console.log(`====================================\\n`);\n      }\n    } catch (portError) {\n      console.error(`Failed to find an available port:`, portError);\n      process.exit(1);\n    }\n\n    // Create the server with the available port\n    const server = app.listen(PORT, currentSettings.serverHost, () => {\n      console.log(`\\n=== Browser Tools Server Started ===`);\n      console.log(\n        `Aggregator listening on http://${currentSettings.serverHost}:${PORT}`\n      );\n\n      if (PORT !== REQUESTED_PORT) {\n        console.log(\n          `NOTE: Using fallback port ${PORT} instead of requested port ${REQUESTED_PORT}`\n        );\n      }\n\n      // Log all available network interfaces for easier discovery\n      const networkInterfaces = os.networkInterfaces();\n      console.log(\"\\nAvailable on the following network addresses:\");\n\n      Object.keys(networkInterfaces).forEach((interfaceName) => {\n        const interfaces = networkInterfaces[interfaceName];\n        if (interfaces) {\n          interfaces.forEach((iface) => {\n            if (!iface.internal && iface.family === \"IPv4\") {\n              console.log(`  - http://${iface.address}:${PORT}`);\n            }\n          });\n        }\n      });\n\n      console.log(`\\nFor local access use: http://localhost:${PORT}`);\n    });\n\n    // Handle server startup errors\n    server.on(\"error\", (err: any) => {\n      if (err.code === \"EADDRINUSE\") {\n        console.error(\n          `ERROR: Port ${PORT} is still in use, despite our checks!`\n        );\n        console.error(\n          `This might indicate another process started using this port after our check.`\n        );\n      } else {\n        console.error(`Server error:`, err);\n      }\n      process.exit(1);\n    });\n\n    // Initialize the browser connector with the existing app AND server\n    const browserConnector = new BrowserConnector(app, server);\n\n    // Handle shutdown gracefully with improved error handling\n    process.on(\"SIGINT\", async () => {\n      console.log(\"\\nReceived SIGINT signal. Starting graceful shutdown...\");\n\n      try {\n        // First shutdown WebSocket connections\n        await browserConnector.shutdown();\n\n        // Then close the HTTP server\n        await new Promise<void>((resolve, reject) => {\n          server.close((err) => {\n            if (err) {\n              console.error(\"Error closing HTTP server:\", err);\n              reject(err);\n            } else {\n              console.log(\"HTTP server closed successfully\");\n              resolve();\n            }\n          });\n        });\n\n        // Clear all logs\n        clearAllLogs();\n\n        console.log(\"Shutdown completed successfully\");\n        process.exit(0);\n      } catch (error) {\n        console.error(\"Error during shutdown:\", error);\n        // Force exit in case of error\n        process.exit(1);\n      }\n    });\n\n    // Also handle SIGTERM\n    process.on(\"SIGTERM\", () => {\n      console.log(\"\\nReceived SIGTERM signal\");\n      process.emit(\"SIGINT\");\n    });\n  } catch (error) {\n    console.error(\"Failed to start server:\", error);\n    process.exit(1);\n  }\n})().catch((err) => {\n  console.error(\"Unhandled error during server startup:\", err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "browser-tools-server/lighthouse/accessibility.ts",
    "content": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\nimport { runLighthouseAudit } from \"./index.js\";\n\n// === Accessibility Report Types ===\n\n/**\n * Accessibility-specific report content structure\n */\nexport interface AccessibilityReportContent {\n  score: number; // Overall score (0-100)\n  audit_counts: {\n    // Counts of different audit types\n    failed: number;\n    passed: number;\n    manual: number;\n    informative: number;\n    not_applicable: number;\n  };\n  issues: AIAccessibilityIssue[];\n  categories: {\n    [category: string]: {\n      score: number;\n      issues_count: number;\n    };\n  };\n  critical_elements: AIAccessibilityElement[];\n  prioritized_recommendations?: string[]; // Ordered list of recommendations\n}\n\n/**\n * Full accessibility report implementing the base LighthouseReport interface\n */\nexport type AIOptimizedAccessibilityReport =\n  LighthouseReport<AccessibilityReportContent>;\n\n/**\n * AI-optimized accessibility issue\n */\ninterface AIAccessibilityIssue {\n  id: string; // e.g., \"color-contrast\"\n  title: string; // e.g., \"Color contrast is sufficient\"\n  impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\";\n  category: string; // e.g., \"contrast\", \"aria\", \"forms\", \"keyboard\"\n  elements?: AIAccessibilityElement[]; // Elements with issues\n  score: number | null; // 0-1 or null\n}\n\n/**\n * Accessibility element with issues\n */\ninterface AIAccessibilityElement {\n  selector: string; // CSS selector\n  snippet?: string; // HTML snippet\n  label?: string; // Element label\n  issue_description?: string; // Description of the issue\n  value?: string | number; // Current value (e.g., contrast ratio)\n}\n\n// Original interfaces for backward compatibility\ninterface AccessibilityAudit {\n  id: string; // e.g., \"color-contrast\"\n  title: string; // e.g., \"Color contrast is sufficient\"\n  description: string; // e.g., \"Ensures text is readable...\"\n  score: number | null; // 0-1 (normalized), null for manual/informative\n  scoreDisplayMode: string; // e.g., \"binary\", \"numeric\", \"manual\"\n  details?: AuditDetails; // Optional, structured details\n  weight?: number; // Optional, audit weight for impact calculation\n}\n\ntype AuditDetails = {\n  items?: Array<{\n    node?: {\n      selector: string; // e.g., \".my-class\"\n      snippet?: string; // HTML snippet\n      nodeLabel?: string; // e.g., \"Modify logging size limits / truncation\"\n      explanation?: string; // Explanation of why the node fails the audit\n    };\n    value?: string | number; // Specific value (e.g., contrast ratio)\n    explanation?: string; // Explanation at the item level\n  }>;\n  debugData?: string; // Optional, debug information\n  [key: string]: any; // Flexible for other detail types (tables, etc.)\n};\n\n// Original limits were optimized for human consumption\n// This ensures we always include critical issues while limiting less important ones\nconst DETAIL_LIMITS = {\n  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues\n  serious: 15, // Up to 15 items for serious issues\n  moderate: 10, // Up to 10 items for moderate issues\n  minor: 3, // Up to 3 items for minor issues\n};\n\n/**\n * Runs an accessibility audit on the specified URL\n * @param url The URL to audit\n * @returns Promise resolving to AI-optimized accessibility audit results\n */\nexport async function runAccessibilityAudit(\n  url: string\n): Promise<AIOptimizedAccessibilityReport> {\n  try {\n    const lhr = await runLighthouseAudit(url, [AuditCategory.ACCESSIBILITY]);\n    return extractAIOptimizedData(lhr, url);\n  } catch (error) {\n    throw new Error(\n      `Accessibility audit failed: ${\n        error instanceof Error ? error.message : String(error)\n      }`\n    );\n  }\n}\n\n/**\n * Extract AI-optimized accessibility data from Lighthouse results\n */\nconst extractAIOptimizedData = (\n  lhr: LighthouseResult,\n  url: string\n): AIOptimizedAccessibilityReport => {\n  const categoryData = lhr.categories[AuditCategory.ACCESSIBILITY];\n  const audits = lhr.audits || {};\n\n  // Add metadata\n  const metadata = {\n    url,\n    timestamp: lhr.fetchTime || new Date().toISOString(),\n    device: \"desktop\", // This could be made configurable\n    lighthouseVersion: lhr.lighthouseVersion,\n  };\n\n  // Initialize variables\n  const issues: AIAccessibilityIssue[] = [];\n  const criticalElements: AIAccessibilityElement[] = [];\n  const categories: {\n    [category: string]: { score: number; issues_count: number };\n  } = {};\n\n  // Count audits by type\n  let failedCount = 0;\n  let passedCount = 0;\n  let manualCount = 0;\n  let informativeCount = 0;\n  let notApplicableCount = 0;\n\n  // Process audit refs\n  const auditRefs = categoryData?.auditRefs || [];\n\n  // First pass: count audits by type and initialize categories\n  auditRefs.forEach((ref) => {\n    const audit = audits[ref.id];\n    if (!audit) return;\n\n    // Count by scoreDisplayMode\n    if (audit.scoreDisplayMode === \"manual\") {\n      manualCount++;\n    } else if (audit.scoreDisplayMode === \"informative\") {\n      informativeCount++;\n    } else if (audit.scoreDisplayMode === \"notApplicable\") {\n      notApplicableCount++;\n    } else if (audit.score !== null) {\n      // Binary pass/fail\n      if (audit.score >= 0.9) {\n        passedCount++;\n      } else {\n        failedCount++;\n      }\n    }\n\n    // Process categories\n    if (ref.group) {\n      // Initialize category if not exists\n      if (!categories[ref.group]) {\n        categories[ref.group] = { score: 0, issues_count: 0 };\n      }\n\n      // Update category score and issues count\n      if (audit.score !== null && audit.score < 0.9) {\n        categories[ref.group].issues_count++;\n      }\n    }\n  });\n\n  // Second pass: process failed audits into AI-friendly format\n  auditRefs\n    .filter((ref) => {\n      const audit = audits[ref.id];\n      return audit && audit.score !== null && audit.score < 0.9;\n    })\n    .sort((a, b) => (b.weight || 0) - (a.weight || 0))\n    // No limit on number of failed audits - we'll show them all\n    .forEach((ref) => {\n      const audit = audits[ref.id];\n\n      // Determine impact level based on score and weight\n      let impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\" = \"moderate\";\n      if (audit.score === 0) {\n        impact = \"critical\";\n      } else if (audit.score !== null && audit.score <= 0.5) {\n        impact = \"serious\";\n      } else if (audit.score !== null && audit.score > 0.7) {\n        impact = \"minor\";\n      }\n\n      // Create elements array\n      const elements: AIAccessibilityElement[] = [];\n\n      if (audit.details) {\n        const details = audit.details as any;\n        if (details.items && Array.isArray(details.items)) {\n          const items = details.items;\n          // Apply limits based on impact level\n          const itemLimit = DETAIL_LIMITS[impact];\n          items.slice(0, itemLimit).forEach((item: any) => {\n            if (item.node) {\n              const element: AIAccessibilityElement = {\n                selector: item.node.selector,\n                snippet: item.node.snippet,\n                label: item.node.nodeLabel,\n                issue_description: item.node.explanation || item.explanation,\n              };\n\n              if (item.value !== undefined) {\n                element.value = item.value;\n              }\n\n              elements.push(element);\n\n              // Add to critical elements if impact is critical or serious\n              if (impact === \"critical\" || impact === \"serious\") {\n                criticalElements.push(element);\n              }\n            }\n          });\n        }\n      }\n\n      // Create the issue\n      const issue: AIAccessibilityIssue = {\n        id: ref.id,\n        title: audit.title,\n        impact,\n        category: ref.group || \"other\",\n        elements: elements.length > 0 ? elements : undefined,\n        score: audit.score,\n      };\n\n      issues.push(issue);\n    });\n\n  // Calculate overall score\n  const score = Math.round((categoryData?.score || 0) * 100);\n\n  // Generate prioritized recommendations\n  const prioritized_recommendations: string[] = [];\n\n  // Add category-specific recommendations\n  Object.entries(categories)\n    .filter(([_, data]) => data.issues_count > 0)\n    .sort(([_, a], [__, b]) => b.issues_count - a.issues_count)\n    .forEach(([category, data]) => {\n      let recommendation = \"\";\n\n      switch (category) {\n        case \"a11y-color-contrast\":\n          recommendation = \"Improve color contrast for better readability\";\n          break;\n        case \"a11y-names-labels\":\n          recommendation = \"Add proper labels to all interactive elements\";\n          break;\n        case \"a11y-aria\":\n          recommendation = \"Fix ARIA attributes and roles\";\n          break;\n        case \"a11y-navigation\":\n          recommendation = \"Improve keyboard navigation and focus management\";\n          break;\n        case \"a11y-language\":\n          recommendation = \"Add proper language attributes to HTML\";\n          break;\n        case \"a11y-tables-lists\":\n          recommendation = \"Fix table and list structures for screen readers\";\n          break;\n        default:\n          recommendation = `Fix ${data.issues_count} issues in ${category}`;\n      }\n\n      prioritized_recommendations.push(recommendation);\n    });\n\n  // Add specific high-impact recommendations\n  if (issues.some((issue) => issue.id === \"color-contrast\")) {\n    prioritized_recommendations.push(\n      \"Fix low contrast text for better readability\"\n    );\n  }\n\n  if (issues.some((issue) => issue.id === \"document-title\")) {\n    prioritized_recommendations.push(\"Add a descriptive page title\");\n  }\n\n  if (issues.some((issue) => issue.id === \"image-alt\")) {\n    prioritized_recommendations.push(\"Add alt text to all images\");\n  }\n\n  // Create the report content\n  const reportContent: AccessibilityReportContent = {\n    score,\n    audit_counts: {\n      failed: failedCount,\n      passed: passedCount,\n      manual: manualCount,\n      informative: informativeCount,\n      not_applicable: notApplicableCount,\n    },\n    issues,\n    categories,\n    critical_elements: criticalElements,\n    prioritized_recommendations:\n      prioritized_recommendations.length > 0\n        ? prioritized_recommendations\n        : undefined,\n  };\n\n  // Return the full report following the LighthouseReport interface\n  return {\n    metadata,\n    report: reportContent,\n  };\n};\n"
  },
  {
    "path": "browser-tools-server/lighthouse/best-practices.ts",
    "content": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\nimport { runLighthouseAudit } from \"./index.js\";\n\n// === Best Practices Report Types ===\n\n/**\n * Best Practices-specific report content structure\n */\nexport interface BestPracticesReportContent {\n  score: number; // Overall score (0-100)\n  audit_counts: {\n    // Counts of different audit types\n    failed: number;\n    passed: number;\n    manual: number;\n    informative: number;\n    not_applicable: number;\n  };\n  issues: AIBestPracticesIssue[];\n  categories: {\n    [category: string]: {\n      score: number;\n      issues_count: number;\n    };\n  };\n  prioritized_recommendations?: string[]; // Ordered list of recommendations\n}\n\n/**\n * Full Best Practices report implementing the base LighthouseReport interface\n */\nexport type AIOptimizedBestPracticesReport =\n  LighthouseReport<BestPracticesReportContent>;\n\n/**\n * AI-optimized Best Practices issue\n */\ninterface AIBestPracticesIssue {\n  id: string; // e.g., \"js-libraries\"\n  title: string; // e.g., \"Detected JavaScript libraries\"\n  impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\";\n  category: string; // e.g., \"security\", \"trust\", \"user-experience\", \"browser-compat\"\n  details?: {\n    name?: string; // Name of the item (e.g., library name, vulnerability)\n    version?: string; // Version information if applicable\n    value?: string; // Current value or status\n    issue?: string; // Description of the issue\n  }[];\n  score: number | null; // 0-1 or null\n}\n\n// Original interfaces for backward compatibility\ninterface BestPracticesAudit {\n  id: string;\n  title: string;\n  description: string;\n  score: number | null;\n  scoreDisplayMode: string;\n  details?: BestPracticesAuditDetails;\n}\n\ninterface BestPracticesAuditDetails {\n  items?: Array<Record<string, unknown>>;\n  type?: string; // e.g., \"table\"\n}\n\n// This ensures we always include critical issues while limiting less important ones\nconst DETAIL_LIMITS: Record<string, number> = {\n  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues\n  serious: 15, // Up to 15 items for serious issues\n  moderate: 10, // Up to 10 items for moderate issues\n  minor: 3, // Up to 3 items for minor issues\n};\n\n/**\n * Runs a Best Practices audit on the specified URL\n * @param url The URL to audit\n * @returns Promise resolving to AI-optimized Best Practices audit results\n */\nexport async function runBestPracticesAudit(\n  url: string\n): Promise<AIOptimizedBestPracticesReport> {\n  try {\n    const lhr = await runLighthouseAudit(url, [AuditCategory.BEST_PRACTICES]);\n    return extractAIOptimizedData(lhr, url);\n  } catch (error) {\n    throw new Error(\n      `Best Practices audit failed: ${\n        error instanceof Error ? error.message : String(error)\n      }`\n    );\n  }\n}\n\n/**\n * Extract AI-optimized Best Practices data from Lighthouse results\n */\nconst extractAIOptimizedData = (\n  lhr: LighthouseResult,\n  url: string\n): AIOptimizedBestPracticesReport => {\n  const categoryData = lhr.categories[AuditCategory.BEST_PRACTICES];\n  const audits = lhr.audits || {};\n\n  // Add metadata\n  const metadata = {\n    url,\n    timestamp: lhr.fetchTime || new Date().toISOString(),\n    device: lhr.configSettings?.formFactor || \"desktop\",\n    lighthouseVersion: lhr.lighthouseVersion || \"unknown\",\n  };\n\n  // Process audit results\n  const issues: AIBestPracticesIssue[] = [];\n  const categories: { [key: string]: { score: number; issues_count: number } } =\n    {\n      security: { score: 0, issues_count: 0 },\n      trust: { score: 0, issues_count: 0 },\n      \"user-experience\": { score: 0, issues_count: 0 },\n      \"browser-compat\": { score: 0, issues_count: 0 },\n      other: { score: 0, issues_count: 0 },\n    };\n\n  // Counters for audit types\n  let failedCount = 0;\n  let passedCount = 0;\n  let manualCount = 0;\n  let informativeCount = 0;\n  let notApplicableCount = 0;\n\n  // Process failed audits (score < 1)\n  const failedAudits = Object.entries(audits)\n    .filter(([, audit]) => {\n      const score = audit.score;\n      return (\n        score !== null &&\n        score < 1 &&\n        audit.scoreDisplayMode !== \"manual\" &&\n        audit.scoreDisplayMode !== \"notApplicable\"\n      );\n    })\n    .map(([auditId, audit]) => ({ auditId, ...audit }));\n\n  // Update counters\n  Object.values(audits).forEach((audit) => {\n    const { score, scoreDisplayMode } = audit;\n\n    if (scoreDisplayMode === \"manual\") {\n      manualCount++;\n    } else if (scoreDisplayMode === \"informative\") {\n      informativeCount++;\n    } else if (scoreDisplayMode === \"notApplicable\") {\n      notApplicableCount++;\n    } else if (score === 1) {\n      passedCount++;\n    } else if (score !== null && score < 1) {\n      failedCount++;\n    }\n  });\n\n  // Process failed audits into AI-friendly format\n  failedAudits.forEach((ref: any) => {\n    // Determine impact level based on audit score and weight\n    let impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\" = \"moderate\";\n    const score = ref.score || 0;\n\n    // Use a more reliable approach to determine impact\n    if (score === 0) {\n      impact = \"critical\";\n    } else if (score < 0.5) {\n      impact = \"serious\";\n    } else if (score < 0.9) {\n      impact = \"moderate\";\n    } else {\n      impact = \"minor\";\n    }\n\n    // Categorize the issue\n    let category = \"other\";\n\n    // Security-related issues\n    if (\n      ref.auditId.includes(\"csp\") ||\n      ref.auditId.includes(\"security\") ||\n      ref.auditId.includes(\"vulnerab\") ||\n      ref.auditId.includes(\"password\") ||\n      ref.auditId.includes(\"cert\") ||\n      ref.auditId.includes(\"deprecat\")\n    ) {\n      category = \"security\";\n    }\n    // Trust and legitimacy issues\n    else if (\n      ref.auditId.includes(\"doctype\") ||\n      ref.auditId.includes(\"charset\") ||\n      ref.auditId.includes(\"legit\") ||\n      ref.auditId.includes(\"trust\")\n    ) {\n      category = \"trust\";\n    }\n    // User experience issues\n    else if (\n      ref.auditId.includes(\"user\") ||\n      ref.auditId.includes(\"experience\") ||\n      ref.auditId.includes(\"console\") ||\n      ref.auditId.includes(\"errors\") ||\n      ref.auditId.includes(\"paste\")\n    ) {\n      category = \"user-experience\";\n    }\n    // Browser compatibility issues\n    else if (\n      ref.auditId.includes(\"compat\") ||\n      ref.auditId.includes(\"browser\") ||\n      ref.auditId.includes(\"vendor\") ||\n      ref.auditId.includes(\"js-lib\")\n    ) {\n      category = \"browser-compat\";\n    }\n\n    // Count issues by category\n    categories[category].issues_count++;\n\n    // Create issue object\n    const issue: AIBestPracticesIssue = {\n      id: ref.auditId,\n      title: ref.title,\n      impact,\n      category,\n      score: ref.score,\n      details: [],\n    };\n\n    // Extract details if available\n    const refDetails = ref.details as BestPracticesAuditDetails | undefined;\n    if (refDetails?.items && Array.isArray(refDetails.items)) {\n      const itemLimit = DETAIL_LIMITS[impact];\n      const detailItems = refDetails.items.slice(0, itemLimit);\n\n      detailItems.forEach((item: Record<string, unknown>) => {\n        issue.details = issue.details || [];\n\n        // Different audits have different detail structures\n        const detail: Record<string, string> = {};\n\n        if (typeof item.name === \"string\") detail.name = item.name;\n        if (typeof item.version === \"string\") detail.version = item.version;\n        if (typeof item.issue === \"string\") detail.issue = item.issue;\n        if (item.value !== undefined) detail.value = String(item.value);\n\n        // For JS libraries, extract name and version\n        if (\n          ref.auditId === \"js-libraries\" &&\n          typeof item.name === \"string\" &&\n          typeof item.version === \"string\"\n        ) {\n          detail.name = item.name;\n          detail.version = item.version;\n        }\n\n        // Add other generic properties that might exist\n        for (const [key, value] of Object.entries(item)) {\n          if (!detail[key] && typeof value === \"string\") {\n            detail[key] = value;\n          }\n        }\n\n        issue.details.push(detail as any);\n      });\n    }\n\n    issues.push(issue);\n  });\n\n  // Calculate category scores (0-100)\n  Object.keys(categories).forEach((category) => {\n    // Simplified scoring: if there are issues in this category, score is reduced proportionally\n    const issueCount = categories[category].issues_count;\n    if (issueCount > 0) {\n      // More issues = lower score, max penalty of 25 points per issue\n      const penalty = Math.min(100, issueCount * 25);\n      categories[category].score = Math.max(0, 100 - penalty);\n    } else {\n      categories[category].score = 100;\n    }\n  });\n\n  // Generate prioritized recommendations\n  const prioritized_recommendations: string[] = [];\n\n  // Prioritize recommendations by category with most issues\n  Object.entries(categories)\n    .filter(([_, data]) => data.issues_count > 0)\n    .sort(([_, a], [__, b]) => b.issues_count - a.issues_count)\n    .forEach(([category, data]) => {\n      let recommendation = \"\";\n\n      switch (category) {\n        case \"security\":\n          recommendation = `Address ${data.issues_count} security issues: vulnerabilities, CSP, deprecations`;\n          break;\n        case \"trust\":\n          recommendation = `Fix ${data.issues_count} trust & legitimacy issues: doctype, charset`;\n          break;\n        case \"user-experience\":\n          recommendation = `Improve ${data.issues_count} user experience issues: console errors, user interactions`;\n          break;\n        case \"browser-compat\":\n          recommendation = `Resolve ${data.issues_count} browser compatibility issues: outdated libraries, vendor prefixes`;\n          break;\n        default:\n          recommendation = `Fix ${data.issues_count} other best practice issues`;\n      }\n\n      prioritized_recommendations.push(recommendation);\n    });\n\n  // Return the optimized report\n  return {\n    metadata,\n    report: {\n      score: categoryData?.score ? Math.round(categoryData.score * 100) : 0,\n      audit_counts: {\n        failed: failedCount,\n        passed: passedCount,\n        manual: manualCount,\n        informative: informativeCount,\n        not_applicable: notApplicableCount,\n      },\n      issues,\n      categories,\n      prioritized_recommendations,\n    },\n  };\n};\n"
  },
  {
    "path": "browser-tools-server/lighthouse/index.ts",
    "content": "import lighthouse from \"lighthouse\";\nimport type { Result as LighthouseResult, Flags } from \"lighthouse\";\nimport {\n  connectToHeadlessBrowser,\n  scheduleBrowserCleanup,\n} from \"../puppeteer-service.js\";\nimport { LighthouseConfig, AuditCategory } from \"./types.js\";\n\n/**\n * Creates a Lighthouse configuration object\n * @param categories Array of categories to audit\n * @returns Lighthouse configuration and flags\n */\nexport function createLighthouseConfig(\n  categories: string[] = [AuditCategory.ACCESSIBILITY]\n): LighthouseConfig {\n  return {\n    flags: {\n      output: [\"json\"],\n      onlyCategories: categories,\n      formFactor: \"desktop\",\n      port: undefined as number | undefined,\n      screenEmulation: {\n        mobile: false,\n        width: 1350,\n        height: 940,\n        deviceScaleFactor: 1,\n        disabled: false,\n      },\n    },\n    config: {\n      extends: \"lighthouse:default\",\n      settings: {\n        onlyCategories: categories,\n        emulatedFormFactor: \"desktop\",\n        throttling: { cpuSlowdownMultiplier: 1 },\n      },\n    },\n  };\n}\n\n/**\n * Runs a Lighthouse audit on the specified URL via CDP\n * @param url The URL to audit\n * @param categories Array of categories to audit, defaults to [\"accessibility\"]\n * @returns Promise resolving to the Lighthouse result\n * @throws Error if the URL is invalid or if the audit fails\n */\nexport async function runLighthouseAudit(\n  url: string,\n  categories: string[]\n): Promise<LighthouseResult> {\n  console.log(`Starting Lighthouse ${categories.join(\", \")} audit for: ${url}`);\n\n  if (!url || url === \"about:blank\") {\n    console.error(\"Invalid URL for Lighthouse audit\");\n    throw new Error(\n      \"Cannot run audit on an empty page or about:blank. Please navigate to a valid URL first.\"\n    );\n  }\n\n  try {\n    // Always use a dedicated headless browser for audits\n    console.log(\"Using dedicated headless browser for audit\");\n\n    // Determine if this is a performance audit - we need to load all resources for performance audits\n    const isPerformanceAudit = categories.includes(AuditCategory.PERFORMANCE);\n\n    // For performance audits, we want to load all resources\n    // For accessibility or other audits, we can block non-essential resources\n    try {\n      const { port } = await connectToHeadlessBrowser(url, {\n        blockResources: !isPerformanceAudit,\n      });\n\n      console.log(`Connected to browser on port: ${port}`);\n\n      // Create Lighthouse config\n      const { flags, config } = createLighthouseConfig(categories);\n      flags.port = port;\n\n      console.log(\n        `Running Lighthouse with categories: ${categories.join(\", \")}`\n      );\n      const runnerResult = await lighthouse(url, flags as Flags, config);\n      console.log(\"Lighthouse scan completed\");\n\n      if (!runnerResult?.lhr) {\n        console.error(\"Lighthouse audit failed to produce results\");\n        throw new Error(\"Lighthouse audit failed to produce results\");\n      }\n\n      // Schedule browser cleanup after a delay to allow for subsequent audits\n      scheduleBrowserCleanup();\n\n      // Return the result\n      const result = runnerResult.lhr;\n\n      return result;\n    } catch (browserError) {\n      // Check if the error is related to Chrome/Edge not being available\n      const errorMessage =\n        browserError instanceof Error\n          ? browserError.message\n          : String(browserError);\n      if (\n        errorMessage.includes(\"Chrome could not be found\") ||\n        errorMessage.includes(\"Failed to launch browser\") ||\n        errorMessage.includes(\"spawn ENOENT\")\n      ) {\n        throw new Error(\n          \"Chrome or Edge browser could not be found. Please ensure that Chrome or Edge is installed on your system to run audits.\"\n        );\n      }\n      // Re-throw other errors\n      throw browserError;\n    }\n  } catch (error) {\n    console.error(\"Lighthouse audit failed:\", error);\n    // Schedule browser cleanup even if the audit fails\n    scheduleBrowserCleanup();\n    throw new Error(\n      `Lighthouse audit failed: ${\n        error instanceof Error ? error.message : String(error)\n      }`\n    );\n  }\n}\n\n// Export from specific audit modules\nexport * from \"./accessibility.js\";\nexport * from \"./performance.js\";\nexport * from \"./seo.js\";\nexport * from \"./types.js\";\n"
  },
  {
    "path": "browser-tools-server/lighthouse/performance.ts",
    "content": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\nimport { runLighthouseAudit } from \"./index.js\";\n\n// === Performance Report Types ===\n\n/**\n * Performance-specific report content structure\n */\nexport interface PerformanceReportContent {\n  score: number; // Overall score (0-100)\n  audit_counts: {\n    // Counts of different audit types\n    failed: number;\n    passed: number;\n    manual: number;\n    informative: number;\n    not_applicable: number;\n  };\n  metrics: AIOptimizedMetric[];\n  opportunities: AIOptimizedOpportunity[];\n  page_stats?: AIPageStats; // Optional page statistics\n  prioritized_recommendations?: string[]; // Ordered list of recommendations\n}\n\n/**\n * Full performance report implementing the base LighthouseReport interface\n */\nexport type AIOptimizedPerformanceReport =\n  LighthouseReport<PerformanceReportContent>;\n\n// AI-optimized performance metric format\ninterface AIOptimizedMetric {\n  id: string; // Short ID like \"lcp\", \"fcp\"\n  score: number | null; // 0-1 score\n  value_ms: number; // Value in milliseconds\n  element_type?: string; // For LCP: \"image\", \"text\", etc.\n  element_selector?: string; // DOM selector for the element\n  element_url?: string; // For images/videos\n  element_content?: string; // For text content (truncated)\n  passes_core_web_vital?: boolean; // Whether this metric passes as a Core Web Vital\n}\n\n// AI-optimized opportunity format\ninterface AIOptimizedOpportunity {\n  id: string; // Like \"render_blocking\", \"http2\"\n  savings_ms: number; // Time savings in ms\n  severity?: \"critical\" | \"serious\" | \"moderate\" | \"minor\"; // Severity classification\n  resources: Array<{\n    url: string; // Resource URL\n    savings_ms?: number; // Individual resource savings\n    size_kb?: number; // Size in KB\n    type?: string; // Resource type (js, css, img, etc.)\n    is_third_party?: boolean; // Whether this is a third-party resource\n  }>;\n}\n\n// Page stats for AI analysis\ninterface AIPageStats {\n  total_size_kb: number; // Total page weight in KB\n  total_requests: number; // Total number of requests\n  resource_counts: {\n    // Count by resource type\n    js: number;\n    css: number;\n    img: number;\n    font: number;\n    other: number;\n  };\n  third_party_size_kb: number; // Size of third-party resources\n  main_thread_blocking_time_ms: number; // Time spent blocking the main thread\n}\n\n// This ensures we always include critical issues while limiting less important ones\nconst DETAIL_LIMITS = {\n  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues\n  serious: 15, // Up to 15 items for serious issues\n  moderate: 10, // Up to 10 items for moderate issues\n  minor: 3, // Up to 3 items for minor issues\n};\n\n/**\n * Performance audit adapted for AI consumption\n * This format is optimized for AI agents with:\n * - Concise, relevant information without redundant descriptions\n * - Key metrics and opportunities clearly structured\n * - Only actionable data that an AI can use for recommendations\n */\nexport async function runPerformanceAudit(\n  url: string\n): Promise<AIOptimizedPerformanceReport> {\n  try {\n    const lhr = await runLighthouseAudit(url, [AuditCategory.PERFORMANCE]);\n    return extractAIOptimizedData(lhr, url);\n  } catch (error) {\n    throw new Error(\n      `Performance audit failed: ${\n        error instanceof Error ? error.message : String(error)\n      }`\n    );\n  }\n}\n\n/**\n * Extract AI-optimized performance data from Lighthouse results\n */\nconst extractAIOptimizedData = (\n  lhr: LighthouseResult,\n  url: string\n): AIOptimizedPerformanceReport => {\n  const audits = lhr.audits || {};\n  const categoryData = lhr.categories[AuditCategory.PERFORMANCE];\n  const score = Math.round((categoryData?.score || 0) * 100);\n\n  // Add metadata\n  const metadata = {\n    url,\n    timestamp: lhr.fetchTime || new Date().toISOString(),\n    device: \"desktop\", // This could be made configurable\n    lighthouseVersion: lhr.lighthouseVersion,\n  };\n\n  // Count audits by type\n  const auditRefs = categoryData?.auditRefs || [];\n  let failedCount = 0;\n  let passedCount = 0;\n  let manualCount = 0;\n  let informativeCount = 0;\n  let notApplicableCount = 0;\n\n  auditRefs.forEach((ref) => {\n    const audit = audits[ref.id];\n    if (!audit) return;\n\n    if (audit.scoreDisplayMode === \"manual\") {\n      manualCount++;\n    } else if (audit.scoreDisplayMode === \"informative\") {\n      informativeCount++;\n    } else if (audit.scoreDisplayMode === \"notApplicable\") {\n      notApplicableCount++;\n    } else if (audit.score !== null) {\n      if (audit.score >= 0.9) {\n        passedCount++;\n      } else {\n        failedCount++;\n      }\n    }\n  });\n\n  const audit_counts = {\n    failed: failedCount,\n    passed: passedCount,\n    manual: manualCount,\n    informative: informativeCount,\n    not_applicable: notApplicableCount,\n  };\n\n  const metrics: AIOptimizedMetric[] = [];\n  const opportunities: AIOptimizedOpportunity[] = [];\n\n  // Extract core metrics\n  if (audits[\"largest-contentful-paint\"]) {\n    const lcp = audits[\"largest-contentful-paint\"];\n    const lcpElement = audits[\"largest-contentful-paint-element\"];\n\n    const metric: AIOptimizedMetric = {\n      id: \"lcp\",\n      score: lcp.score,\n      value_ms: Math.round(lcp.numericValue || 0),\n      passes_core_web_vital: lcp.score !== null && lcp.score >= 0.9,\n    };\n\n    // Enhanced LCP element detection\n\n    // 1. Try from largest-contentful-paint-element audit\n    if (lcpElement && lcpElement.details) {\n      const lcpDetails = lcpElement.details as any;\n\n      // First attempt - try to get directly from items\n      if (\n        lcpDetails.items &&\n        Array.isArray(lcpDetails.items) &&\n        lcpDetails.items.length > 0\n      ) {\n        const item = lcpDetails.items[0];\n\n        // For text elements in tables format\n        if (item.type === \"table\" && item.items && item.items.length > 0) {\n          const firstTableItem = item.items[0];\n\n          if (firstTableItem.node) {\n            if (firstTableItem.node.selector) {\n              metric.element_selector = firstTableItem.node.selector;\n            }\n\n            // Determine element type based on path or selector\n            const path = firstTableItem.node.path;\n            const selector = firstTableItem.node.selector || \"\";\n\n            if (path) {\n              if (\n                selector.includes(\" > img\") ||\n                selector.includes(\" img\") ||\n                selector.endsWith(\"img\") ||\n                path.includes(\",IMG\")\n              ) {\n                metric.element_type = \"image\";\n\n                // Try to extract image name from selector\n                const imgMatch = selector.match(/img[.][^> ]+/);\n                if (imgMatch && !metric.element_url) {\n                  metric.element_url = imgMatch[0];\n                }\n              } else if (\n                path.includes(\",SPAN\") ||\n                path.includes(\",P\") ||\n                path.includes(\",H\")\n              ) {\n                metric.element_type = \"text\";\n              }\n            }\n\n            // Try to extract text content if available\n            if (firstTableItem.node.nodeLabel) {\n              metric.element_content = firstTableItem.node.nodeLabel.substring(\n                0,\n                100\n              );\n            }\n          }\n        }\n        // Original handling for direct items\n        else if (item.node?.nodeLabel) {\n          // Determine element type from node label\n          if (item.node.nodeLabel.startsWith(\"<img\")) {\n            metric.element_type = \"image\";\n            // Try to extract image URL from the node snippet\n            const match = item.node.snippet?.match(/src=\"([^\"]+)\"/);\n            if (match && match[1]) {\n              metric.element_url = match[1];\n            }\n          } else if (item.node.nodeLabel.startsWith(\"<video\")) {\n            metric.element_type = \"video\";\n          } else if (item.node.nodeLabel.startsWith(\"<h\")) {\n            metric.element_type = \"heading\";\n          } else {\n            metric.element_type = \"text\";\n          }\n\n          if (item.node?.selector) {\n            metric.element_selector = item.node.selector;\n          }\n        }\n      }\n    }\n\n    // 2. Try from lcp-lazy-loaded audit\n    const lcpImageAudit = audits[\"lcp-lazy-loaded\"];\n    if (lcpImageAudit && lcpImageAudit.details) {\n      const lcpImageDetails = lcpImageAudit.details as any;\n\n      if (\n        lcpImageDetails.items &&\n        Array.isArray(lcpImageDetails.items) &&\n        lcpImageDetails.items.length > 0\n      ) {\n        const item = lcpImageDetails.items[0];\n\n        if (item.url) {\n          metric.element_type = \"image\";\n          metric.element_url = item.url;\n        }\n      }\n    }\n\n    // 3. Try directly from the LCP audit details\n    if (!metric.element_url && lcp.details) {\n      const lcpDirectDetails = lcp.details as any;\n\n      if (lcpDirectDetails.items && Array.isArray(lcpDirectDetails.items)) {\n        for (const item of lcpDirectDetails.items) {\n          if (item.url || (item.node && item.node.path)) {\n            if (item.url) {\n              metric.element_url = item.url;\n              metric.element_type = item.url.match(\n                /\\.(jpg|jpeg|png|gif|webp|svg)$/i\n              )\n                ? \"image\"\n                : \"resource\";\n            }\n            if (item.node && item.node.selector) {\n              metric.element_selector = item.node.selector;\n            }\n            break;\n          }\n        }\n      }\n    }\n\n    // 4. Check for specific audit that might contain image info\n    const largestImageAudit = audits[\"largest-image-paint\"];\n    if (largestImageAudit && largestImageAudit.details) {\n      const imageDetails = largestImageAudit.details as any;\n\n      if (\n        imageDetails.items &&\n        Array.isArray(imageDetails.items) &&\n        imageDetails.items.length > 0\n      ) {\n        const item = imageDetails.items[0];\n\n        if (item.url) {\n          // If we have a large image that's close in time to LCP, it's likely the LCP element\n          metric.element_type = \"image\";\n          metric.element_url = item.url;\n        }\n      }\n    }\n\n    // 5. Check for network requests audit to find image resources\n    if (!metric.element_url) {\n      const networkRequests = audits[\"network-requests\"];\n\n      if (networkRequests && networkRequests.details) {\n        const networkDetails = networkRequests.details as any;\n\n        if (networkDetails.items && Array.isArray(networkDetails.items)) {\n          // Get all image resources loaded close to the LCP time\n          const lcpTime = lcp.numericValue || 0;\n          const imageResources = networkDetails.items\n            .filter(\n              (item: any) =>\n                item.url &&\n                item.mimeType &&\n                item.mimeType.startsWith(\"image/\") &&\n                item.endTime &&\n                Math.abs(item.endTime - lcpTime) < 500 // Within 500ms of LCP\n            )\n            .sort(\n              (a: any, b: any) =>\n                Math.abs(a.endTime - lcpTime) - Math.abs(b.endTime - lcpTime)\n            );\n\n          if (imageResources.length > 0) {\n            const closestImage = imageResources[0];\n\n            if (!metric.element_type) {\n              metric.element_type = \"image\";\n              metric.element_url = closestImage.url;\n            }\n          }\n        }\n      }\n    }\n\n    metrics.push(metric);\n  }\n\n  if (audits[\"first-contentful-paint\"]) {\n    const fcp = audits[\"first-contentful-paint\"];\n    metrics.push({\n      id: \"fcp\",\n      score: fcp.score,\n      value_ms: Math.round(fcp.numericValue || 0),\n      passes_core_web_vital: fcp.score !== null && fcp.score >= 0.9,\n    });\n  }\n\n  if (audits[\"speed-index\"]) {\n    const si = audits[\"speed-index\"];\n    metrics.push({\n      id: \"si\",\n      score: si.score,\n      value_ms: Math.round(si.numericValue || 0),\n    });\n  }\n\n  if (audits[\"interactive\"]) {\n    const tti = audits[\"interactive\"];\n    metrics.push({\n      id: \"tti\",\n      score: tti.score,\n      value_ms: Math.round(tti.numericValue || 0),\n    });\n  }\n\n  // Add CLS (Cumulative Layout Shift)\n  if (audits[\"cumulative-layout-shift\"]) {\n    const cls = audits[\"cumulative-layout-shift\"];\n    metrics.push({\n      id: \"cls\",\n      score: cls.score,\n      // CLS is not in ms, but a unitless value\n      value_ms: Math.round((cls.numericValue || 0) * 1000) / 1000, // Convert to 3 decimal places\n      passes_core_web_vital: cls.score !== null && cls.score >= 0.9,\n    });\n  }\n\n  // Add TBT (Total Blocking Time)\n  if (audits[\"total-blocking-time\"]) {\n    const tbt = audits[\"total-blocking-time\"];\n    metrics.push({\n      id: \"tbt\",\n      score: tbt.score,\n      value_ms: Math.round(tbt.numericValue || 0),\n      passes_core_web_vital: tbt.score !== null && tbt.score >= 0.9,\n    });\n  }\n\n  // Extract opportunities\n  if (audits[\"render-blocking-resources\"]) {\n    const rbrAudit = audits[\"render-blocking-resources\"];\n\n    // Determine impact level based on potential savings\n    let impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\" = \"moderate\";\n    const savings = Math.round(rbrAudit.numericValue || 0);\n\n    if (savings > 2000) {\n      impact = \"critical\";\n    } else if (savings > 1000) {\n      impact = \"serious\";\n    } else if (savings < 300) {\n      impact = \"minor\";\n    }\n\n    const opportunity: AIOptimizedOpportunity = {\n      id: \"render_blocking_resources\",\n      savings_ms: savings,\n      severity: impact,\n      resources: [],\n    };\n\n    const rbrDetails = rbrAudit.details as any;\n    if (rbrDetails && rbrDetails.items && Array.isArray(rbrDetails.items)) {\n      // Determine how many items to include based on impact\n      const itemLimit = DETAIL_LIMITS[impact];\n\n      rbrDetails.items\n        .slice(0, itemLimit)\n        .forEach((item: { url?: string; wastedMs?: number }) => {\n          if (item.url) {\n            // Extract file name from full URL\n            const fileName = item.url.split(\"/\").pop() || item.url;\n            opportunity.resources.push({\n              url: fileName,\n              savings_ms: Math.round(item.wastedMs || 0),\n            });\n          }\n        });\n    }\n\n    if (opportunity.resources.length > 0) {\n      opportunities.push(opportunity);\n    }\n  }\n\n  if (audits[\"uses-http2\"]) {\n    const http2Audit = audits[\"uses-http2\"];\n\n    // Determine impact level based on potential savings\n    let impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\" = \"moderate\";\n    const savings = Math.round(http2Audit.numericValue || 0);\n\n    if (savings > 2000) {\n      impact = \"critical\";\n    } else if (savings > 1000) {\n      impact = \"serious\";\n    } else if (savings < 300) {\n      impact = \"minor\";\n    }\n\n    const opportunity: AIOptimizedOpportunity = {\n      id: \"http2\",\n      savings_ms: savings,\n      severity: impact,\n      resources: [],\n    };\n\n    const http2Details = http2Audit.details as any;\n    if (\n      http2Details &&\n      http2Details.items &&\n      Array.isArray(http2Details.items)\n    ) {\n      // Determine how many items to include based on impact\n      const itemLimit = DETAIL_LIMITS[impact];\n\n      http2Details.items\n        .slice(0, itemLimit)\n        .forEach((item: { url?: string }) => {\n          if (item.url) {\n            // Extract file name from full URL\n            const fileName = item.url.split(\"/\").pop() || item.url;\n            opportunity.resources.push({ url: fileName });\n          }\n        });\n    }\n\n    if (opportunity.resources.length > 0) {\n      opportunities.push(opportunity);\n    }\n  }\n\n  // After extracting all metrics and opportunities, collect page stats\n  // Extract page stats\n  let page_stats: AIPageStats | undefined;\n\n  // Total page stats\n  const totalByteWeight = audits[\"total-byte-weight\"];\n  const networkRequests = audits[\"network-requests\"];\n  const thirdPartyAudit = audits[\"third-party-summary\"];\n  const mainThreadWork = audits[\"mainthread-work-breakdown\"];\n\n  if (networkRequests && networkRequests.details) {\n    const resourceDetails = networkRequests.details as any;\n\n    if (resourceDetails.items && Array.isArray(resourceDetails.items)) {\n      const resources = resourceDetails.items;\n      const totalRequests = resources.length;\n\n      // Calculate total size and counts by type\n      let totalSizeKb = 0;\n      let jsCount = 0,\n        cssCount = 0,\n        imgCount = 0,\n        fontCount = 0,\n        otherCount = 0;\n\n      resources.forEach((resource: any) => {\n        const sizeKb = resource.transferSize\n          ? Math.round(resource.transferSize / 1024)\n          : 0;\n        totalSizeKb += sizeKb;\n\n        // Count by mime type\n        const mimeType = resource.mimeType || \"\";\n        if (mimeType.includes(\"javascript\") || resource.url.endsWith(\".js\")) {\n          jsCount++;\n        } else if (mimeType.includes(\"css\") || resource.url.endsWith(\".css\")) {\n          cssCount++;\n        } else if (\n          mimeType.includes(\"image\") ||\n          /\\.(jpg|jpeg|png|gif|webp|svg)$/i.test(resource.url)\n        ) {\n          imgCount++;\n        } else if (\n          mimeType.includes(\"font\") ||\n          /\\.(woff|woff2|ttf|otf|eot)$/i.test(resource.url)\n        ) {\n          fontCount++;\n        } else {\n          otherCount++;\n        }\n      });\n\n      // Calculate third-party size\n      let thirdPartySizeKb = 0;\n      if (thirdPartyAudit && thirdPartyAudit.details) {\n        const thirdPartyDetails = thirdPartyAudit.details as any;\n        if (thirdPartyDetails.items && Array.isArray(thirdPartyDetails.items)) {\n          thirdPartyDetails.items.forEach((item: any) => {\n            if (item.transferSize) {\n              thirdPartySizeKb += Math.round(item.transferSize / 1024);\n            }\n          });\n        }\n      }\n\n      // Get main thread blocking time\n      let mainThreadBlockingTimeMs = 0;\n      if (mainThreadWork && mainThreadWork.numericValue) {\n        mainThreadBlockingTimeMs = Math.round(mainThreadWork.numericValue);\n      }\n\n      // Create page stats object\n      page_stats = {\n        total_size_kb: totalSizeKb,\n        total_requests: totalRequests,\n        resource_counts: {\n          js: jsCount,\n          css: cssCount,\n          img: imgCount,\n          font: fontCount,\n          other: otherCount,\n        },\n        third_party_size_kb: thirdPartySizeKb,\n        main_thread_blocking_time_ms: mainThreadBlockingTimeMs,\n      };\n    }\n  }\n\n  // Generate prioritized recommendations\n  const prioritized_recommendations: string[] = [];\n\n  // Add key recommendations based on failed audits with high impact\n  if (\n    audits[\"render-blocking-resources\"] &&\n    audits[\"render-blocking-resources\"].score !== null &&\n    audits[\"render-blocking-resources\"].score === 0\n  ) {\n    prioritized_recommendations.push(\"Eliminate render-blocking resources\");\n  }\n\n  if (\n    audits[\"uses-responsive-images\"] &&\n    audits[\"uses-responsive-images\"].score !== null &&\n    audits[\"uses-responsive-images\"].score === 0\n  ) {\n    prioritized_recommendations.push(\"Properly size images\");\n  }\n\n  if (\n    audits[\"uses-optimized-images\"] &&\n    audits[\"uses-optimized-images\"].score !== null &&\n    audits[\"uses-optimized-images\"].score === 0\n  ) {\n    prioritized_recommendations.push(\"Efficiently encode images\");\n  }\n\n  if (\n    audits[\"uses-text-compression\"] &&\n    audits[\"uses-text-compression\"].score !== null &&\n    audits[\"uses-text-compression\"].score === 0\n  ) {\n    prioritized_recommendations.push(\"Enable text compression\");\n  }\n\n  if (\n    audits[\"uses-http2\"] &&\n    audits[\"uses-http2\"].score !== null &&\n    audits[\"uses-http2\"].score === 0\n  ) {\n    prioritized_recommendations.push(\"Use HTTP/2\");\n  }\n\n  // Add more specific recommendations based on Core Web Vitals\n  if (\n    audits[\"largest-contentful-paint\"] &&\n    audits[\"largest-contentful-paint\"].score !== null &&\n    audits[\"largest-contentful-paint\"].score < 0.5\n  ) {\n    prioritized_recommendations.push(\"Improve Largest Contentful Paint (LCP)\");\n  }\n\n  if (\n    audits[\"cumulative-layout-shift\"] &&\n    audits[\"cumulative-layout-shift\"].score !== null &&\n    audits[\"cumulative-layout-shift\"].score < 0.5\n  ) {\n    prioritized_recommendations.push(\"Reduce layout shifts (CLS)\");\n  }\n\n  if (\n    audits[\"total-blocking-time\"] &&\n    audits[\"total-blocking-time\"].score !== null &&\n    audits[\"total-blocking-time\"].score < 0.5\n  ) {\n    prioritized_recommendations.push(\"Reduce JavaScript execution time\");\n  }\n\n  // Create the performance report content\n  const reportContent: PerformanceReportContent = {\n    score,\n    audit_counts,\n    metrics,\n    opportunities,\n    page_stats,\n    prioritized_recommendations:\n      prioritized_recommendations.length > 0\n        ? prioritized_recommendations\n        : undefined,\n  };\n\n  // Return the full report following the LighthouseReport interface\n  return {\n    metadata,\n    report: reportContent,\n  };\n};\n"
  },
  {
    "path": "browser-tools-server/lighthouse/seo.ts",
    "content": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\nimport { runLighthouseAudit } from \"./index.js\";\n\n// === SEO Report Types ===\n\n/**\n * SEO-specific report content structure\n */\nexport interface SEOReportContent {\n  score: number; // Overall score (0-100)\n  audit_counts: {\n    // Counts of different audit types\n    failed: number;\n    passed: number;\n    manual: number;\n    informative: number;\n    not_applicable: number;\n  };\n  issues: AISEOIssue[];\n  categories: {\n    [category: string]: {\n      score: number;\n      issues_count: number;\n    };\n  };\n  prioritized_recommendations?: string[]; // Ordered list of recommendations\n}\n\n/**\n * Full SEO report implementing the base LighthouseReport interface\n */\nexport type AIOptimizedSEOReport = LighthouseReport<SEOReportContent>;\n\n/**\n * AI-optimized SEO issue\n */\ninterface AISEOIssue {\n  id: string; // e.g., \"meta-description\"\n  title: string; // e.g., \"Document has a meta description\"\n  impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\";\n  category: string; // e.g., \"content\", \"mobile\", \"crawlability\"\n  details?: {\n    selector?: string; // CSS selector if applicable\n    value?: string; // Current value\n    issue?: string; // Description of the issue\n  }[];\n  score: number | null; // 0-1 or null\n}\n\n// Original interfaces for backward compatibility\ninterface SEOAudit {\n  id: string; // e.g., \"meta-description\"\n  title: string; // e.g., \"Document has a meta description\"\n  description: string; // e.g., \"Meta descriptions improve SEO...\"\n  score: number | null; // 0-1 or null\n  scoreDisplayMode: string; // e.g., \"binary\"\n  details?: SEOAuditDetails; // Optional, structured details\n  weight?: number; // For prioritization\n}\n\ninterface SEOAuditDetails {\n  items?: Array<{\n    selector?: string; // e.g., \"meta[name='description']\"\n    issue?: string; // e.g., \"Meta description is missing\"\n    value?: string; // e.g., Current meta description text\n  }>;\n  type?: string; // e.g., \"table\"\n}\n\n// This ensures we always include critical issues while limiting less important ones\nconst DETAIL_LIMITS = {\n  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues\n  serious: 15, // Up to 15 items for serious issues\n  moderate: 10, // Up to 10 items for moderate issues\n  minor: 3, // Up to 3 items for minor issues\n};\n\n/**\n * Runs an SEO audit on the specified URL\n * @param url The URL to audit\n * @returns Promise resolving to AI-optimized SEO audit results\n */\nexport async function runSEOAudit(url: string): Promise<AIOptimizedSEOReport> {\n  try {\n    const lhr = await runLighthouseAudit(url, [AuditCategory.SEO]);\n    return extractAIOptimizedData(lhr, url);\n  } catch (error) {\n    throw new Error(\n      `SEO audit failed: ${\n        error instanceof Error ? error.message : String(error)\n      }`\n    );\n  }\n}\n\n/**\n * Extract AI-optimized SEO data from Lighthouse results\n */\nconst extractAIOptimizedData = (\n  lhr: LighthouseResult,\n  url: string\n): AIOptimizedSEOReport => {\n  const categoryData = lhr.categories[AuditCategory.SEO];\n  const audits = lhr.audits || {};\n\n  // Add metadata\n  const metadata = {\n    url,\n    timestamp: lhr.fetchTime || new Date().toISOString(),\n    device: \"desktop\", // This could be made configurable\n    lighthouseVersion: lhr.lighthouseVersion,\n  };\n\n  // Initialize variables\n  const issues: AISEOIssue[] = [];\n  const categories: {\n    [category: string]: { score: number; issues_count: number };\n  } = {\n    content: { score: 0, issues_count: 0 },\n    mobile: { score: 0, issues_count: 0 },\n    crawlability: { score: 0, issues_count: 0 },\n    other: { score: 0, issues_count: 0 },\n  };\n\n  // Count audits by type\n  let failedCount = 0;\n  let passedCount = 0;\n  let manualCount = 0;\n  let informativeCount = 0;\n  let notApplicableCount = 0;\n\n  // Process audit refs\n  const auditRefs = categoryData?.auditRefs || [];\n\n  // First pass: count audits by type and initialize categories\n  auditRefs.forEach((ref) => {\n    const audit = audits[ref.id];\n    if (!audit) return;\n\n    // Count by scoreDisplayMode\n    if (audit.scoreDisplayMode === \"manual\") {\n      manualCount++;\n    } else if (audit.scoreDisplayMode === \"informative\") {\n      informativeCount++;\n    } else if (audit.scoreDisplayMode === \"notApplicable\") {\n      notApplicableCount++;\n    } else if (audit.score !== null) {\n      // Binary pass/fail\n      if (audit.score >= 0.9) {\n        passedCount++;\n      } else {\n        failedCount++;\n      }\n    }\n\n    // Categorize the issue\n    let category = \"other\";\n    if (\n      ref.id.includes(\"crawl\") ||\n      ref.id.includes(\"http\") ||\n      ref.id.includes(\"redirect\") ||\n      ref.id.includes(\"robots\")\n    ) {\n      category = \"crawlability\";\n    } else if (\n      ref.id.includes(\"viewport\") ||\n      ref.id.includes(\"font-size\") ||\n      ref.id.includes(\"tap-targets\")\n    ) {\n      category = \"mobile\";\n    } else if (\n      ref.id.includes(\"document\") ||\n      ref.id.includes(\"meta\") ||\n      ref.id.includes(\"description\") ||\n      ref.id.includes(\"canonical\") ||\n      ref.id.includes(\"title\") ||\n      ref.id.includes(\"link\")\n    ) {\n      category = \"content\";\n    }\n\n    // Update category score and issues count\n    if (audit.score !== null && audit.score < 0.9) {\n      categories[category].issues_count++;\n    }\n  });\n\n  // Second pass: process failed audits into AI-friendly format\n  auditRefs\n    .filter((ref) => {\n      const audit = audits[ref.id];\n      return audit && audit.score !== null && audit.score < 0.9;\n    })\n    .sort((a, b) => (b.weight || 0) - (a.weight || 0))\n    // No limit on failed audits - we'll filter dynamically based on impact\n    .forEach((ref) => {\n      const audit = audits[ref.id];\n\n      // Determine impact level based on score and weight\n      let impact: \"critical\" | \"serious\" | \"moderate\" | \"minor\" = \"moderate\";\n      if (audit.score === 0) {\n        impact = \"critical\";\n      } else if (audit.score !== null && audit.score <= 0.5) {\n        impact = \"serious\";\n      } else if (audit.score !== null && audit.score > 0.7) {\n        impact = \"minor\";\n      }\n\n      // Categorize the issue\n      let category = \"other\";\n      if (\n        ref.id.includes(\"crawl\") ||\n        ref.id.includes(\"http\") ||\n        ref.id.includes(\"redirect\") ||\n        ref.id.includes(\"robots\")\n      ) {\n        category = \"crawlability\";\n      } else if (\n        ref.id.includes(\"viewport\") ||\n        ref.id.includes(\"font-size\") ||\n        ref.id.includes(\"tap-targets\")\n      ) {\n        category = \"mobile\";\n      } else if (\n        ref.id.includes(\"document\") ||\n        ref.id.includes(\"meta\") ||\n        ref.id.includes(\"description\") ||\n        ref.id.includes(\"canonical\") ||\n        ref.id.includes(\"title\") ||\n        ref.id.includes(\"link\")\n      ) {\n        category = \"content\";\n      }\n\n      // Extract details\n      const details: { selector?: string; value?: string; issue?: string }[] =\n        [];\n\n      if (audit.details) {\n        const auditDetails = audit.details as any;\n        if (auditDetails.items && Array.isArray(auditDetails.items)) {\n          // Determine item limit based on impact\n          const itemLimit = DETAIL_LIMITS[impact];\n\n          auditDetails.items.slice(0, itemLimit).forEach((item: any) => {\n            const detail: {\n              selector?: string;\n              value?: string;\n              issue?: string;\n            } = {};\n\n            if (item.selector) {\n              detail.selector = item.selector;\n            }\n\n            if (item.value !== undefined) {\n              detail.value = item.value;\n            }\n\n            if (item.issue) {\n              detail.issue = item.issue;\n            }\n\n            if (Object.keys(detail).length > 0) {\n              details.push(detail);\n            }\n          });\n        }\n      }\n\n      // Create the issue\n      const issue: AISEOIssue = {\n        id: ref.id,\n        title: audit.title,\n        impact,\n        category,\n        details: details.length > 0 ? details : undefined,\n        score: audit.score,\n      };\n\n      issues.push(issue);\n    });\n\n  // Calculate overall score\n  const score = Math.round((categoryData?.score || 0) * 100);\n\n  // Generate prioritized recommendations\n  const prioritized_recommendations: string[] = [];\n\n  // Add category-specific recommendations\n  Object.entries(categories)\n    .filter(([_, data]) => data.issues_count > 0)\n    .sort(([_, a], [__, b]) => b.issues_count - a.issues_count)\n    .forEach(([category, data]) => {\n      if (data.issues_count === 0) return;\n\n      let recommendation = \"\";\n\n      switch (category) {\n        case \"content\":\n          recommendation = `Improve SEO content (${data.issues_count} issues): titles, descriptions, and headers`;\n          break;\n        case \"mobile\":\n          recommendation = `Optimize for mobile devices (${data.issues_count} issues)`;\n          break;\n        case \"crawlability\":\n          recommendation = `Fix crawlability issues (${data.issues_count} issues): robots.txt, sitemaps, and redirects`;\n          break;\n        default:\n          recommendation = `Fix ${data.issues_count} SEO issues in category: ${category}`;\n      }\n\n      prioritized_recommendations.push(recommendation);\n    });\n\n  // Add specific high-impact recommendations\n  if (issues.some((issue) => issue.id === \"meta-description\")) {\n    prioritized_recommendations.push(\n      \"Add a meta description to improve click-through rate\"\n    );\n  }\n\n  if (issues.some((issue) => issue.id === \"document-title\")) {\n    prioritized_recommendations.push(\n      \"Add a descriptive page title with keywords\"\n    );\n  }\n\n  if (issues.some((issue) => issue.id === \"hreflang\")) {\n    prioritized_recommendations.push(\n      \"Fix hreflang implementation for international SEO\"\n    );\n  }\n\n  if (issues.some((issue) => issue.id === \"canonical\")) {\n    prioritized_recommendations.push(\"Implement proper canonical tags\");\n  }\n\n  // Create the report content\n  const reportContent: SEOReportContent = {\n    score,\n    audit_counts: {\n      failed: failedCount,\n      passed: passedCount,\n      manual: manualCount,\n      informative: informativeCount,\n      not_applicable: notApplicableCount,\n    },\n    issues,\n    categories,\n    prioritized_recommendations:\n      prioritized_recommendations.length > 0\n        ? prioritized_recommendations\n        : undefined,\n  };\n\n  // Return the full report following the LighthouseReport interface\n  return {\n    metadata,\n    report: reportContent,\n  };\n};\n"
  },
  {
    "path": "browser-tools-server/lighthouse/types.ts",
    "content": "/**\n * Audit categories available in Lighthouse\n */\nexport enum AuditCategory {\n  ACCESSIBILITY = \"accessibility\",\n  PERFORMANCE = \"performance\",\n  SEO = \"seo\",\n  BEST_PRACTICES = \"best-practices\", // Not yet implemented\n  PWA = \"pwa\", // Not yet implemented\n}\n\n/**\n * Base interface for Lighthouse report metadata\n */\nexport interface LighthouseReport<T = any> {\n  metadata: {\n    url: string;\n    timestamp: string; // ISO 8601, e.g., \"2025-02-27T14:30:00Z\"\n    device: string; // e.g., \"mobile\", \"desktop\"\n    lighthouseVersion: string; // e.g., \"10.4.0\"\n  };\n\n  // For backward compatibility with existing report formats\n  overallScore?: number;\n  failedAuditsCount?: number;\n  passedAuditsCount?: number;\n  manualAuditsCount?: number;\n  informativeAuditsCount?: number;\n  notApplicableAuditsCount?: number;\n  failedAudits?: any[];\n\n  // New format for specialized reports\n  report?: T; // Generic report data that will be specialized by each audit type\n}\n\n/**\n * Configuration options for Lighthouse audits\n */\nexport interface LighthouseConfig {\n  flags: {\n    output: string[];\n    onlyCategories: string[];\n    formFactor: string;\n    port: number | undefined;\n    screenEmulation: {\n      mobile: boolean;\n      width: number;\n      height: number;\n      deviceScaleFactor: number;\n      disabled: boolean;\n    };\n  };\n  config: {\n    extends: string;\n    settings: {\n      onlyCategories: string[];\n      emulatedFormFactor: string;\n      throttling: {\n        cpuSlowdownMultiplier: number;\n      };\n    };\n  };\n}\n"
  },
  {
    "path": "browser-tools-server/package.json",
    "content": "{\n  \"name\": \"@agentdeskai/browser-tools-server\",\n  \"version\": \"1.2.0\",\n  \"description\": \"A browser tools server for capturing and managing browser events, logs, and screenshots\",\n  \"type\": \"module\",\n  \"main\": \"dist/browser-connector.js\",\n  \"bin\": {\n    \"browser-tools-server\": \"./dist/browser-connector.js\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc\",\n    \"start\": \"tsc && node dist/browser-connector.js\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"keywords\": [\n    \"browser\",\n    \"tools\",\n    \"debugging\",\n    \"logging\",\n    \"screenshots\",\n    \"chrome\",\n    \"extension\"\n  ],\n  \"author\": \"AgentDesk AI\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.4.1\",\n    \"body-parser\": \"^1.20.3\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.21.2\",\n    \"lighthouse\": \"^11.6.0\",\n    \"llm-cost\": \"^1.0.5\",\n    \"node-fetch\": \"^2.7.0\",\n    \"puppeteer-core\": \"^22.4.1\",\n    \"ws\": \"^8.18.0\"\n  },\n  \"optionalDependencies\": {\n    \"chrome-launcher\": \"^1.1.2\"\n  },\n  \"devDependencies\": {\n    \"@types/ws\": \"^8.5.14\",\n    \"@types/body-parser\": \"^1.19.5\",\n    \"@types/cors\": \"^2.8.17\",\n    \"@types/express\": \"^5.0.0\",\n    \"@types/node\": \"^22.13.1\",\n    \"@types/node-fetch\": \"^2.6.11\",\n    \"@types/puppeteer-core\": \"^7.0.4\",\n    \"typescript\": \"^5.7.3\"\n  }\n}\n"
  },
  {
    "path": "browser-tools-server/puppeteer-service.ts",
    "content": "import fs from \"fs\";\nimport puppeteer from \"puppeteer-core\";\nimport path from \"path\";\nimport os from \"os\";\nimport { execSync } from \"child_process\";\nimport * as ChromeLauncher from \"chrome-launcher\";\n// ===== Configuration Types and Defaults =====\n\n/**\n * Configuration interface for the Puppeteer service\n */\nexport interface PuppeteerServiceConfig {\n  // Browser preferences\n  preferredBrowsers?: string[]; // Order of browser preference (\"chrome\", \"edge\", \"brave\", \"firefox\")\n  customBrowserPaths?: { [key: string]: string }; // Custom browser executable paths\n\n  // Connection settings\n  debugPorts?: number[]; // Ports to try when connecting to existing browsers\n  connectionTimeout?: number; // Timeout for connection attempts in ms\n  maxRetries?: number; // Maximum number of retries for connections\n\n  // Browser cleanup settings\n  browserCleanupTimeout?: number; // Timeout before closing inactive browsers (ms)\n\n  // Performance settings\n  blockResourceTypes?: string[]; // Resource types to block for performance\n}\n\n// Default configuration values\nconst DEFAULT_CONFIG: PuppeteerServiceConfig = {\n  preferredBrowsers: [\"chrome\", \"edge\", \"brave\", \"firefox\"],\n  debugPorts: [9222, 9223, 9224, 9225],\n  connectionTimeout: 10000,\n  maxRetries: 3,\n  browserCleanupTimeout: 60000,\n  blockResourceTypes: [\"image\", \"font\", \"media\"],\n};\n\n// Browser support notes:\n// - Chrome/Chromium: Fully supported (primary target)\n// - Edge: Fully supported (Chromium-based)\n// - Brave: Fully supported (Chromium-based)\n// - Firefox: Partially supported (some features may not work)\n// - Safari: Not supported by Puppeteer\n\n// ===== Global State =====\n\n// Current active configuration\nlet currentConfig: PuppeteerServiceConfig = { ...DEFAULT_CONFIG };\n\n// Browser instance management\nlet headlessBrowserInstance: puppeteer.Browser | null = null;\nlet launchedBrowserWSEndpoint: string | null = null;\n\n// Cleanup management\nlet browserCleanupTimeout: NodeJS.Timeout | null = null;\nlet BROWSER_CLEANUP_TIMEOUT = 60000; // 60 seconds default\n\n// Cache for browser executable paths\nlet detectedBrowserPath: string | null = null;\n\n// ===== Configuration Functions =====\n\n/**\n * Configure the Puppeteer service with custom settings\n * @param config Partial configuration to override defaults\n */\nexport function configurePuppeteerService(\n  config: Partial<PuppeteerServiceConfig>\n): void {\n  currentConfig = { ...DEFAULT_CONFIG, ...config };\n\n  // Update the timeout if it was changed\n  if (\n    config.browserCleanupTimeout &&\n    config.browserCleanupTimeout !== BROWSER_CLEANUP_TIMEOUT\n  ) {\n    BROWSER_CLEANUP_TIMEOUT = config.browserCleanupTimeout;\n  }\n\n  console.log(\"Puppeteer service configured:\", currentConfig);\n}\n\n// ===== Browser Management =====\n\n/**\n * Get or create a headless browser instance\n * @returns Promise resolving to a browser instance\n */\nasync function getHeadlessBrowserInstance(): Promise<puppeteer.Browser> {\n  console.log(\"Browser instance request started\");\n\n  // Cancel any scheduled cleanup\n  cancelScheduledCleanup();\n\n  // Try to reuse existing browser\n  if (headlessBrowserInstance) {\n    try {\n      const pages = await headlessBrowserInstance.pages();\n      console.log(\n        `Reusing existing headless browser with ${pages.length} pages`\n      );\n      return headlessBrowserInstance;\n    } catch (error) {\n      console.log(\n        \"Existing browser instance is no longer valid, creating a new one\"\n      );\n      headlessBrowserInstance = null;\n      launchedBrowserWSEndpoint = null;\n    }\n  }\n\n  // Create a new browser instance\n  return launchNewBrowser();\n}\n\n/**\n * Launches a new browser instance\n * @returns Promise resolving to a browser instance\n */\nasync function launchNewBrowser(): Promise<puppeteer.Browser> {\n  console.log(\"Creating new headless browser instance\");\n\n  // Setup temporary user data directory\n  const userDataDir = createTempUserDataDir();\n  let browser: puppeteer.Browser | null = null;\n\n  try {\n    // Configure launch options\n    const launchOptions = configureLaunchOptions(userDataDir);\n\n    // Set custom browser executable\n    await setCustomBrowserExecutable(launchOptions);\n\n    // Launch the browser\n    console.log(\n      \"Launching browser with options:\",\n      JSON.stringify({\n        headless: launchOptions.headless,\n        executablePath: launchOptions.executablePath,\n      })\n    );\n\n    browser = await puppeteer.launch(launchOptions);\n\n    // Store references to the browser instance\n    launchedBrowserWSEndpoint = browser.wsEndpoint();\n    headlessBrowserInstance = browser;\n\n    // Setup cleanup handlers\n    setupBrowserCleanupHandlers(browser, userDataDir);\n\n    console.log(\"Browser ready\");\n    return browser;\n  } catch (error) {\n    console.error(\"Failed to launch browser:\", error);\n\n    // Clean up resources\n    if (browser) {\n      try {\n        await browser.close();\n      } catch (closeError) {\n        console.error(\"Error closing browser:\", closeError);\n      }\n      headlessBrowserInstance = null;\n      launchedBrowserWSEndpoint = null;\n    }\n\n    // Clean up the temporary directory\n    try {\n      fs.rmSync(userDataDir, { recursive: true, force: true });\n    } catch (fsError) {\n      console.error(\"Error removing temporary directory:\", fsError);\n    }\n\n    throw error;\n  }\n}\n\n/**\n * Creates a temporary user data directory for the browser\n * @returns Path to the created directory\n */\nfunction createTempUserDataDir(): string {\n  const tempDir = os.tmpdir();\n  const uniqueId = `${Date.now().toString()}-${Math.random()\n    .toString(36)\n    .substring(2)}`;\n  const userDataDir = path.join(tempDir, `browser-debug-profile-${uniqueId}`);\n  fs.mkdirSync(userDataDir, { recursive: true });\n  console.log(`Using temporary user data directory: ${userDataDir}`);\n  return userDataDir;\n}\n\n/**\n * Configures browser launch options\n * @param userDataDir Path to the user data directory\n * @returns Launch options object\n */\nfunction configureLaunchOptions(userDataDir: string): any {\n  const launchOptions: any = {\n    args: [\n      \"--remote-debugging-port=0\", // Use dynamic port\n      `--user-data-dir=${userDataDir}`,\n      \"--no-first-run\",\n      \"--no-default-browser-check\",\n      \"--disable-dev-shm-usage\",\n      \"--disable-extensions\",\n      \"--disable-component-extensions-with-background-pages\",\n      \"--disable-background-networking\",\n      \"--disable-backgrounding-occluded-windows\",\n      \"--disable-default-apps\",\n      \"--disable-sync\",\n      \"--disable-translate\",\n      \"--metrics-recording-only\",\n      \"--no-pings\",\n      \"--safebrowsing-disable-auto-update\",\n    ],\n  };\n\n  // Add headless mode (using any to bypass type checking issues)\n  launchOptions.headless = \"new\";\n\n  return launchOptions;\n}\n\n/**\n * Sets a custom browser executable path if configured\n * @param launchOptions Launch options object to modify\n */\nasync function setCustomBrowserExecutable(launchOptions: any): Promise<void> {\n  // First, try to use a custom browser path from configuration\n  if (\n    currentConfig.customBrowserPaths &&\n    Object.keys(currentConfig.customBrowserPaths).length > 0\n  ) {\n    const preferredBrowsers = currentConfig.preferredBrowsers || [\n      \"chrome\",\n      \"edge\",\n      \"brave\",\n      \"firefox\",\n    ];\n\n    for (const browser of preferredBrowsers) {\n      if (\n        currentConfig.customBrowserPaths[browser] &&\n        fs.existsSync(currentConfig.customBrowserPaths[browser])\n      ) {\n        launchOptions.executablePath =\n          currentConfig.customBrowserPaths[browser];\n\n        // Set product to firefox if using Firefox browser\n        if (browser === \"firefox\") {\n          launchOptions.product = \"firefox\";\n        }\n\n        console.log(\n          `Using custom ${browser} path: ${launchOptions.executablePath}`\n        );\n        return;\n      }\n    }\n  }\n\n  // If no custom path is found, use cached path or detect a new one\n  try {\n    if (detectedBrowserPath && fs.existsSync(detectedBrowserPath)) {\n      console.log(`Using cached browser path: ${detectedBrowserPath}`);\n      launchOptions.executablePath = detectedBrowserPath;\n\n      // Check if the detected browser is Firefox\n      if (detectedBrowserPath.includes(\"firefox\")) {\n        launchOptions.product = \"firefox\";\n        console.log(\"Setting product to firefox for Firefox browser\");\n      }\n    } else {\n      detectedBrowserPath = await findBrowserExecutablePath();\n      launchOptions.executablePath = detectedBrowserPath;\n\n      // Check if the detected browser is Firefox\n      if (detectedBrowserPath.includes(\"firefox\")) {\n        launchOptions.product = \"firefox\";\n        console.log(\"Setting product to firefox for Firefox browser\");\n      }\n\n      console.log(\n        `Using detected browser path: ${launchOptions.executablePath}`\n      );\n    }\n  } catch (error) {\n    console.error(\"Failed to detect browser executable path:\", error);\n    throw new Error(\n      \"No browser executable path found. Please specify a custom browser path in the configuration.\"\n    );\n  }\n}\n\n/**\n * Find a browser executable path on the current system\n * @returns Path to a browser executable\n */\nasync function findBrowserExecutablePath(): Promise<string> {\n  // Try to use chrome-launcher (most reliable method)\n  try {\n    console.log(\"Attempting to find Chrome using chrome-launcher...\");\n\n    // Launch Chrome using chrome-launcher\n    const chrome = await ChromeLauncher.launch({\n      chromeFlags: [\"--headless\"],\n      handleSIGINT: false,\n    });\n\n    // chrome-launcher stores the Chrome executable path differently than Puppeteer\n    // Let's try different approaches to get it\n\n    // First check if we can access it directly\n    let chromePath = \"\";\n\n    // Chrome version data often contains the path\n    if (chrome.process && chrome.process.spawnfile) {\n      chromePath = chrome.process.spawnfile;\n      console.log(\"Found Chrome path from process.spawnfile\");\n    } else {\n      // Try to get the Chrome path from chrome-launcher\n      // In newer versions, it's directly accessible\n      console.log(\"Trying to determine Chrome path using other methods\");\n\n      // This will actually return the real Chrome path for us\n      // chrome-launcher has this inside but doesn't expose it directly\n      const possiblePaths = [\n        process.env.CHROME_PATH,\n        // Common paths by OS\n        ...(process.platform === \"darwin\"\n          ? [\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"]\n          : process.platform === \"win32\"\n          ? [\n              `${process.env.PROGRAMFILES}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n              `${process.env[\"PROGRAMFILES(X86)\"]}\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe`,\n            ]\n          : [\"/usr/bin/google-chrome\"]),\n      ].filter(Boolean);\n\n      // Use the first valid path\n      for (const p of possiblePaths) {\n        if (p && fs.existsSync(p)) {\n          chromePath = p;\n          console.log(\"Found Chrome path from common locations\");\n          break;\n        }\n      }\n    }\n\n    // Always kill the Chrome instance we just launched\n    await chrome.kill();\n\n    if (chromePath) {\n      console.log(`Chrome found via chrome-launcher: ${chromePath}`);\n      return chromePath;\n    } else {\n      console.log(\"Chrome launched but couldn't determine executable path\");\n    }\n  } catch (error) {\n    // Check if it's a ChromeNotInstalledError\n    const errorMessage = error instanceof Error ? error.message : String(error);\n    if (\n      errorMessage.includes(\"No Chrome installations found\") ||\n      (error as any)?.code === \"ERR_LAUNCHER_NOT_INSTALLED\"\n    ) {\n      console.log(\"Chrome not installed. Falling back to manual detection\");\n    } else {\n      console.error(\"Failed to find Chrome using chrome-launcher:\", error);\n      console.log(\"Falling back to manual detection\");\n    }\n  }\n\n  // If chrome-launcher failed, use manual detection\n\n  const platform = process.platform;\n  const preferredBrowsers = currentConfig.preferredBrowsers || [\n    \"chrome\",\n    \"edge\",\n    \"brave\",\n    \"firefox\",\n  ];\n\n  console.log(`Attempting to detect browser executable path on ${platform}...`);\n\n  // Platform-specific detection strategies\n  if (platform === \"win32\") {\n    // Windows - try registry detection for Chrome\n    let registryPath = null;\n    try {\n      console.log(\"Checking Windows registry for Chrome...\");\n      // Try HKLM first\n      const regOutput = execSync(\n        'reg query \"HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\chrome.exe\" /ve',\n        { encoding: \"utf8\" }\n      );\n\n      // Extract path from registry output\n      const match = regOutput.match(/REG_(?:SZ|EXPAND_SZ)\\s+([^\\s]+)/i);\n      if (match && match[1]) {\n        registryPath = match[1].replace(/\\\\\"/g, \"\");\n        // Verify the path exists\n        if (fs.existsSync(registryPath)) {\n          console.log(`Found Chrome via HKLM registry: ${registryPath}`);\n          return registryPath;\n        }\n      }\n    } catch (e) {\n      // Try HKCU if HKLM fails\n      try {\n        console.log(\"Checking user registry for Chrome...\");\n        const regOutput = execSync(\n          'reg query \"HKEY_CURRENT_USER\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\chrome.exe\" /ve',\n          { encoding: \"utf8\" }\n        );\n\n        // Extract path from registry output\n        const match = regOutput.match(/REG_(?:SZ|EXPAND_SZ)\\s+([^\\s]+)/i);\n        if (match && match[1]) {\n          registryPath = match[1].replace(/\\\\\"/g, \"\");\n          // Verify the path exists\n          if (fs.existsSync(registryPath)) {\n            console.log(`Found Chrome via HKCU registry: ${registryPath}`);\n            return registryPath;\n          }\n        }\n      } catch (innerError) {\n        console.log(\n          \"Failed to find Chrome via registry, continuing with path checks\"\n        );\n      }\n    }\n\n    // Try to find Chrome through BLBeacon registry key (version info)\n    try {\n      console.log(\"Checking Chrome BLBeacon registry...\");\n      const regOutput = execSync(\n        'reg query \"HKEY_CURRENT_USER\\\\Software\\\\Google\\\\Chrome\\\\BLBeacon\" /v version',\n        { encoding: \"utf8\" }\n      );\n\n      if (regOutput) {\n        // If BLBeacon exists, Chrome is likely installed in the default location\n        const programFiles = process.env.PROGRAMFILES || \"C:\\\\Program Files\";\n        const programFilesX86 =\n          process.env[\"PROGRAMFILES(X86)\"] || \"C:\\\\Program Files (x86)\";\n\n        const defaultChromePaths = [\n          path.join(programFiles, \"Google\\\\Chrome\\\\Application\\\\chrome.exe\"),\n          path.join(programFilesX86, \"Google\\\\Chrome\\\\Application\\\\chrome.exe\"),\n        ];\n\n        for (const chromePath of defaultChromePaths) {\n          if (fs.existsSync(chromePath)) {\n            console.log(\n              `Found Chrome via BLBeacon registry hint: ${chromePath}`\n            );\n            return chromePath;\n          }\n        }\n      }\n    } catch (e) {\n      console.log(\"Failed to find Chrome via BLBeacon registry\");\n    }\n\n    // Continue with regular path checks\n    const programFiles = process.env.PROGRAMFILES || \"C:\\\\Program Files\";\n    const programFilesX86 =\n      process.env[\"PROGRAMFILES(X86)\"] || \"C:\\\\Program Files (x86)\";\n\n    // Common Windows browser paths\n    const winBrowserPaths = {\n      chrome: [\n        path.join(programFiles, \"Google\\\\Chrome\\\\Application\\\\chrome.exe\"),\n        path.join(programFilesX86, \"Google\\\\Chrome\\\\Application\\\\chrome.exe\"),\n      ],\n      edge: [\n        path.join(programFiles, \"Microsoft\\\\Edge\\\\Application\\\\msedge.exe\"),\n        path.join(programFilesX86, \"Microsoft\\\\Edge\\\\Application\\\\msedge.exe\"),\n      ],\n      brave: [\n        path.join(\n          programFiles,\n          \"BraveSoftware\\\\Brave-Browser\\\\Application\\\\brave.exe\"\n        ),\n        path.join(\n          programFilesX86,\n          \"BraveSoftware\\\\Brave-Browser\\\\Application\\\\brave.exe\"\n        ),\n      ],\n      firefox: [\n        path.join(programFiles, \"Mozilla Firefox\\\\firefox.exe\"),\n        path.join(programFilesX86, \"Mozilla Firefox\\\\firefox.exe\"),\n      ],\n    };\n\n    // Check each browser in preferred order\n    for (const browser of preferredBrowsers) {\n      const paths =\n        winBrowserPaths[browser as keyof typeof winBrowserPaths] || [];\n      for (const browserPath of paths) {\n        if (fs.existsSync(browserPath)) {\n          console.log(`Found ${browser} at ${browserPath}`);\n          return browserPath;\n        }\n      }\n    }\n  } else if (platform === \"darwin\") {\n    // macOS browser paths\n    const macBrowserPaths = {\n      chrome: [\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"],\n      edge: [\"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge\"],\n      brave: [\"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser\"],\n      firefox: [\"/Applications/Firefox.app/Contents/MacOS/firefox\"],\n      safari: [\"/Applications/Safari.app/Contents/MacOS/Safari\"],\n    };\n\n    // Check each browser in preferred order\n    for (const browser of preferredBrowsers) {\n      const paths =\n        macBrowserPaths[browser as keyof typeof macBrowserPaths] || [];\n      for (const browserPath of paths) {\n        if (fs.existsSync(browserPath)) {\n          console.log(`Found ${browser} at ${browserPath}`);\n          // Safari is detected but not supported by Puppeteer\n          if (browser === \"safari\") {\n            console.log(\n              \"Safari detected but not supported by Puppeteer. Continuing search...\"\n            );\n            continue;\n          }\n          return browserPath;\n        }\n      }\n    }\n  } else if (platform === \"linux\") {\n    // Linux browser commands\n    const linuxBrowserCommands = {\n      chrome: [\"google-chrome\", \"chromium\", \"chromium-browser\"],\n      edge: [\"microsoft-edge\"],\n      brave: [\"brave-browser\"],\n      firefox: [\"firefox\"],\n    };\n\n    // Check each browser in preferred order\n    for (const browser of preferredBrowsers) {\n      const commands =\n        linuxBrowserCommands[browser as keyof typeof linuxBrowserCommands] ||\n        [];\n      for (const cmd of commands) {\n        try {\n          // Use more universal commands for Linux to find executables\n          // command -v works in most shells, fallback to which or type\n          const browserPath = execSync(\n            `command -v ${cmd} || which ${cmd} || type -p ${cmd} 2>/dev/null`,\n            { encoding: \"utf8\" }\n          ).trim();\n\n          if (browserPath && fs.existsSync(browserPath)) {\n            console.log(`Found ${browser} at ${browserPath}`);\n            return browserPath;\n          }\n        } catch (e) {\n          // Command not found, continue to next\n        }\n      }\n    }\n\n    // Additional check for unusual locations on Linux\n    const alternativeLocations = [\n      \"/usr/bin/google-chrome\",\n      \"/usr/bin/chromium\",\n      \"/usr/bin/chromium-browser\",\n      \"/snap/bin/chromium\",\n      \"/snap/bin/google-chrome\",\n      \"/opt/google/chrome/chrome\",\n    ];\n\n    for (const location of alternativeLocations) {\n      if (fs.existsSync(location)) {\n        console.log(`Found browser at alternative location: ${location}`);\n        return location;\n      }\n    }\n  }\n\n  throw new Error(\n    `No browser executable found for platform ${platform}. Please specify a custom browser path.`\n  );\n}\n\n/**\n * Sets up cleanup handlers for the browser instance\n * @param browser Browser instance\n * @param userDataDir Path to the user data directory to clean up\n */\nfunction setupBrowserCleanupHandlers(\n  browser: puppeteer.Browser,\n  userDataDir: string\n): void {\n  browser.on(\"disconnected\", () => {\n    console.log(`Browser disconnected. Scheduling cleanup for: ${userDataDir}`);\n\n    // Clear any existing cleanup timeout when browser is disconnected\n    cancelScheduledCleanup();\n\n    // Delayed cleanup to avoid conflicts with potential new browser instances\n    setTimeout(() => {\n      // Only remove the directory if no new browser has been launched\n      if (!headlessBrowserInstance) {\n        console.log(`Cleaning up temporary directory: ${userDataDir}`);\n        try {\n          fs.rmSync(userDataDir, { recursive: true, force: true });\n          console.log(`Successfully removed directory: ${userDataDir}`);\n        } catch (error) {\n          console.error(`Failed to remove directory ${userDataDir}:`, error);\n        }\n      } else {\n        console.log(\n          `Skipping cleanup for ${userDataDir} as new browser instance is active`\n        );\n      }\n    }, 5000); // 5-second delay for cleanup\n\n    // Reset browser instance variables\n    launchedBrowserWSEndpoint = null;\n    headlessBrowserInstance = null;\n  });\n}\n\n// ===== Cleanup Management =====\n\n/**\n * Cancels any scheduled browser cleanup\n */\nfunction cancelScheduledCleanup(): void {\n  if (browserCleanupTimeout) {\n    console.log(\"Cancelling scheduled browser cleanup\");\n    clearTimeout(browserCleanupTimeout);\n    browserCleanupTimeout = null;\n  }\n}\n\n/**\n * Schedules automatic cleanup of the browser instance after inactivity\n */\nexport function scheduleBrowserCleanup(): void {\n  // Clear any existing timeout first\n  cancelScheduledCleanup();\n\n  // Only schedule cleanup if we have an active browser instance\n  if (headlessBrowserInstance) {\n    console.log(\n      `Scheduling browser cleanup in ${BROWSER_CLEANUP_TIMEOUT / 1000} seconds`\n    );\n\n    browserCleanupTimeout = setTimeout(() => {\n      console.log(\"Executing scheduled browser cleanup\");\n      if (headlessBrowserInstance) {\n        console.log(\"Closing headless browser instance\");\n        headlessBrowserInstance.close();\n        headlessBrowserInstance = null;\n        launchedBrowserWSEndpoint = null;\n      }\n      browserCleanupTimeout = null;\n    }, BROWSER_CLEANUP_TIMEOUT);\n  }\n}\n\n// ===== Public Browser Connection API =====\n\n/**\n * Connects to a headless browser for web operations\n * @param url The URL to navigate to\n * @param options Connection and emulation options\n * @returns Promise resolving to browser, port, and page objects\n */\nexport async function connectToHeadlessBrowser(\n  url: string,\n  options: {\n    blockResources?: boolean;\n    customResourceBlockList?: string[];\n    emulateDevice?: \"mobile\" | \"tablet\" | \"desktop\";\n    emulateNetworkCondition?: \"slow3G\" | \"fast3G\" | \"4G\" | \"offline\";\n    viewport?: { width: number; height: number };\n    locale?: string;\n    timezoneId?: string;\n    userAgent?: string;\n    waitForSelector?: string;\n    waitForTimeout?: number;\n    cookies?: Array<{\n      name: string;\n      value: string;\n      domain?: string;\n      path?: string;\n    }>;\n    headers?: Record<string, string>;\n  } = {}\n): Promise<{\n  browser: puppeteer.Browser;\n  port: number;\n  page: puppeteer.Page;\n}> {\n  console.log(\n    `Connecting to headless browser for ${url}${\n      options.blockResources ? \" (blocking non-essential resources)\" : \"\"\n    }`\n  );\n\n  try {\n    // Validate URL format\n    try {\n      new URL(url);\n    } catch (e) {\n      throw new Error(`Invalid URL format: ${url}`);\n    }\n\n    // Get or create a browser instance\n    const browser = await getHeadlessBrowserInstance();\n\n    if (!launchedBrowserWSEndpoint) {\n      throw new Error(\"Failed to retrieve WebSocket endpoint for browser\");\n    }\n\n    // Extract port from WebSocket endpoint\n    const port = parseInt(\n      launchedBrowserWSEndpoint.split(\":\")[2].split(\"/\")[0]\n    );\n\n    // Always create a new page for each audit to avoid request interception conflicts\n    console.log(\"Creating a new page for this audit\");\n    const page = await browser.newPage();\n\n    // Set a longer timeout for navigation\n    const navigationTimeout = 10000; // 10 seconds\n    page.setDefaultNavigationTimeout(navigationTimeout);\n\n    // Navigate to the URL\n    console.log(`Navigating to ${url}`);\n    await page.goto(url, {\n      waitUntil: \"networkidle2\", // Wait until there are no more network connections for at least 500ms\n      timeout: navigationTimeout,\n    });\n\n    // Set custom headers if provided\n    if (options.headers && Object.keys(options.headers).length > 0) {\n      await page.setExtraHTTPHeaders(options.headers);\n      console.log(\"Set custom HTTP headers\");\n    }\n\n    // Set cookies if provided\n    if (options.cookies && options.cookies.length > 0) {\n      const urlObj = new URL(url);\n      const cookiesWithDomain = options.cookies.map((cookie) => ({\n        ...cookie,\n        domain: cookie.domain || urlObj.hostname,\n        path: cookie.path || \"/\",\n      }));\n      await page.setCookie(...cookiesWithDomain);\n      console.log(`Set ${options.cookies.length} cookies`);\n    }\n\n    // Set custom viewport if specified\n    if (options.viewport) {\n      await page.setViewport(options.viewport);\n      console.log(\n        `Set viewport to ${options.viewport.width}x${options.viewport.height}`\n      );\n    } else if (options.emulateDevice) {\n      // Set common device emulation presets\n      let viewport;\n      let userAgent = options.userAgent;\n\n      switch (options.emulateDevice) {\n        case \"mobile\":\n          viewport = {\n            width: 375,\n            height: 667,\n            isMobile: true,\n            hasTouch: true,\n          };\n          userAgent =\n            userAgent ||\n            \"Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)\";\n          break;\n        case \"tablet\":\n          viewport = {\n            width: 768,\n            height: 1024,\n            isMobile: true,\n            hasTouch: true,\n          };\n          userAgent =\n            userAgent || \"Mozilla/5.0 (iPad; CPU OS 13_2_3 like Mac OS X)\";\n          break;\n        case \"desktop\":\n        default:\n          viewport = {\n            width: 1280,\n            height: 800,\n            isMobile: false,\n            hasTouch: false,\n          };\n          break;\n      }\n\n      await page.setViewport(viewport);\n      if (userAgent) await page.setUserAgent(userAgent);\n\n      console.log(`Emulating ${options.emulateDevice} device`);\n    }\n\n    // Set locale and timezone if provided\n    if (options.locale) {\n      await page.evaluateOnNewDocument((locale) => {\n        Object.defineProperty(navigator, \"language\", { get: () => locale });\n        Object.defineProperty(navigator, \"languages\", { get: () => [locale] });\n      }, options.locale);\n      console.log(`Set locale to ${options.locale}`);\n    }\n\n    if (options.timezoneId) {\n      await page.emulateTimezone(options.timezoneId);\n      console.log(`Set timezone to ${options.timezoneId}`);\n    }\n\n    // Emulate network conditions if specified\n    if (options.emulateNetworkCondition) {\n      // Define network condition types that match puppeteer's expected format\n      interface PuppeteerNetworkConditions {\n        offline: boolean;\n        latency?: number;\n        download?: number;\n        upload?: number;\n      }\n\n      let networkConditions: PuppeteerNetworkConditions;\n\n      switch (options.emulateNetworkCondition) {\n        case \"slow3G\":\n          networkConditions = {\n            offline: false,\n            latency: 400,\n            download: (500 * 1024) / 8,\n            upload: (500 * 1024) / 8,\n          };\n          break;\n        case \"fast3G\":\n          networkConditions = {\n            offline: false,\n            latency: 150,\n            download: (1.5 * 1024 * 1024) / 8,\n            upload: (750 * 1024) / 8,\n          };\n          break;\n        case \"4G\":\n          networkConditions = {\n            offline: false,\n            latency: 50,\n            download: (4 * 1024 * 1024) / 8,\n            upload: (2 * 1024 * 1024) / 8,\n          };\n          break;\n        case \"offline\":\n          networkConditions = { offline: true };\n          break;\n        default:\n          networkConditions = { offline: false };\n      }\n\n      // @ts-ignore - Property might not be in types but is supported\n      await page.emulateNetworkConditions(networkConditions);\n      console.log(\n        `Emulating ${options.emulateNetworkCondition} network conditions`\n      );\n    }\n\n    // Check if we should block resources based on the options\n    if (options.blockResources) {\n      const resourceTypesToBlock = options.customResourceBlockList ||\n        currentConfig.blockResourceTypes || [\"image\", \"font\", \"media\"];\n\n      await page.setRequestInterception(true);\n      page.on(\"request\", (request) => {\n        // Block unnecessary resources to speed up loading\n        const resourceType = request.resourceType();\n        if (resourceTypesToBlock.includes(resourceType)) {\n          request.abort();\n        } else {\n          request.continue();\n        }\n      });\n\n      console.log(\n        `Blocking resource types: ${resourceTypesToBlock.join(\", \")}`\n      );\n    }\n\n    // Wait for a specific selector if requested\n    if (options.waitForSelector) {\n      try {\n        console.log(`Waiting for selector: ${options.waitForSelector}`);\n        await page.waitForSelector(options.waitForSelector, {\n          timeout: options.waitForTimeout || 30000,\n        });\n      } catch (selectorError: any) {\n        console.warn(\n          `Failed to find selector \"${options.waitForSelector}\": ${selectorError.message}`\n        );\n        // Continue anyway, don't fail the whole operation\n      }\n    }\n\n    return { browser, port, page };\n  } catch (error) {\n    console.error(\"Failed to connect to headless browser:\", error);\n    throw new Error(\n      `Failed to connect to headless browser: ${\n        error instanceof Error ? error.message : String(error)\n      }`\n    );\n  }\n}\n"
  },
  {
    "path": "browser-tools-server/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"esModuleInterop\": true,\n    \"outDir\": \"./dist\",\n    \"rootDir\": \".\",\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true\n  },\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "chrome-extension/background.js",
    "content": "// Listen for messages from the devtools panel\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  if (message.type === \"GET_CURRENT_URL\" && message.tabId) {\n    getCurrentTabUrl(message.tabId)\n      .then((url) => {\n        sendResponse({ success: true, url: url });\n      })\n      .catch((error) => {\n        sendResponse({ success: false, error: error.message });\n      });\n    return true; // Required to use sendResponse asynchronously\n  }\n\n  // Handle explicit request to update the server with the URL\n  if (message.type === \"UPDATE_SERVER_URL\" && message.tabId && message.url) {\n    console.log(\n      `Background: Received request to update server with URL for tab ${message.tabId}: ${message.url}`\n    );\n    updateServerWithUrl(\n      message.tabId,\n      message.url,\n      message.source || \"explicit_update\"\n    )\n      .then(() => {\n        if (sendResponse) sendResponse({ success: true });\n      })\n      .catch((error) => {\n        console.error(\"Background: Error updating server with URL:\", error);\n        if (sendResponse)\n          sendResponse({ success: false, error: error.message });\n      });\n    return true; // Required to use sendResponse asynchronously\n  }\n\n  if (message.type === \"CAPTURE_SCREENSHOT\" && message.tabId) {\n    // First get the server settings\n    chrome.storage.local.get([\"browserConnectorSettings\"], (result) => {\n      const settings = result.browserConnectorSettings || {\n        serverHost: \"localhost\",\n        serverPort: 3025,\n      };\n\n      // Validate server identity first\n      validateServerIdentity(settings.serverHost, settings.serverPort)\n        .then((isValid) => {\n          if (!isValid) {\n            console.error(\n              \"Cannot capture screenshot: Not connected to a valid browser tools server\"\n            );\n            sendResponse({\n              success: false,\n              error:\n                \"Not connected to a valid browser tools server. Please check your connection settings.\",\n            });\n            return;\n          }\n\n          // Continue with screenshot capture\n          captureAndSendScreenshot(message, settings, sendResponse);\n        })\n        .catch((error) => {\n          console.error(\"Error validating server:\", error);\n          sendResponse({\n            success: false,\n            error: \"Failed to validate server identity: \" + error.message,\n          });\n        });\n    });\n    return true; // Required to use sendResponse asynchronously\n  }\n});\n\n// Validate server identity\nasync function validateServerIdentity(host, port) {\n  try {\n    const response = await fetch(`http://${host}:${port}/.identity`, {\n      signal: AbortSignal.timeout(3000), // 3 second timeout\n    });\n\n    if (!response.ok) {\n      console.error(`Invalid server response: ${response.status}`);\n      return false;\n    }\n\n    const identity = await response.json();\n\n    // Validate the server signature\n    if (identity.signature !== \"mcp-browser-connector-24x7\") {\n      console.error(\"Invalid server signature - not the browser tools server\");\n      return false;\n    }\n\n    return true;\n  } catch (error) {\n    console.error(\"Error validating server identity:\", error);\n    return false;\n  }\n}\n\n// Helper function to process the tab and run the audit\nfunction processTabForAudit(tab, tabId) {\n  const url = tab.url;\n\n  if (!url) {\n    console.error(`No URL available for tab ${tabId}`);\n    return;\n  }\n\n  // Update our cache and the server with this URL\n  tabUrls.set(tabId, url);\n  updateServerWithUrl(tabId, url);\n}\n\n// Track URLs for each tab\nconst tabUrls = new Map();\n\n// Function to get the current URL for a tab\nasync function getCurrentTabUrl(tabId) {\n  try {\n    console.log(\"Background: Getting URL for tab\", tabId);\n\n    // First check if we have it cached\n    if (tabUrls.has(tabId)) {\n      const cachedUrl = tabUrls.get(tabId);\n      console.log(\"Background: Found cached URL:\", cachedUrl);\n      return cachedUrl;\n    }\n\n    // Otherwise get it from the tab\n    try {\n      const tab = await chrome.tabs.get(tabId);\n      if (tab && tab.url) {\n        // Cache the URL\n        tabUrls.set(tabId, tab.url);\n        console.log(\"Background: Got URL from tab:\", tab.url);\n        return tab.url;\n      } else {\n        console.log(\"Background: Tab exists but no URL found\");\n      }\n    } catch (tabError) {\n      console.error(\"Background: Error getting tab:\", tabError);\n    }\n\n    // If we can't get the tab directly, try querying for active tabs\n    try {\n      const tabs = await chrome.tabs.query({\n        active: true,\n        currentWindow: true,\n      });\n      if (tabs && tabs.length > 0 && tabs[0].url) {\n        const activeUrl = tabs[0].url;\n        console.log(\"Background: Got URL from active tab:\", activeUrl);\n        // Cache this URL as well\n        tabUrls.set(tabId, activeUrl);\n        return activeUrl;\n      }\n    } catch (queryError) {\n      console.error(\"Background: Error querying tabs:\", queryError);\n    }\n\n    console.log(\"Background: Could not find URL for tab\", tabId);\n    return null;\n  } catch (error) {\n    console.error(\"Background: Error getting tab URL:\", error);\n    return null;\n  }\n}\n\n// Listen for tab updates to detect page refreshes and URL changes\nchrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {\n  // Track URL changes\n  if (changeInfo.url) {\n    console.log(`URL changed in tab ${tabId} to ${changeInfo.url}`);\n    tabUrls.set(tabId, changeInfo.url);\n\n    // Send URL update to server if possible\n    updateServerWithUrl(tabId, changeInfo.url, \"tab_url_change\");\n  }\n\n  // Check if this is a page refresh (status becoming \"complete\")\n  if (changeInfo.status === \"complete\") {\n    // Update URL in our cache\n    if (tab.url) {\n      tabUrls.set(tabId, tab.url);\n      // Send URL update to server if possible\n      updateServerWithUrl(tabId, tab.url, \"page_complete\");\n    }\n\n    retestConnectionOnRefresh(tabId);\n  }\n});\n\n// Listen for tab activation (switching between tabs)\nchrome.tabs.onActivated.addListener((activeInfo) => {\n  const tabId = activeInfo.tabId;\n  console.log(`Tab activated: ${tabId}`);\n\n  // Get the URL of the newly activated tab\n  chrome.tabs.get(tabId, (tab) => {\n    if (chrome.runtime.lastError) {\n      console.error(\"Error getting tab info:\", chrome.runtime.lastError);\n      return;\n    }\n\n    if (tab && tab.url) {\n      console.log(`Active tab changed to ${tab.url}`);\n\n      // Update our cache\n      tabUrls.set(tabId, tab.url);\n\n      // Send URL update to server\n      updateServerWithUrl(tabId, tab.url, \"tab_activated\");\n    }\n  });\n});\n\n// Function to update the server with the current URL\nasync function updateServerWithUrl(tabId, url, source = \"background_update\") {\n  if (!url) {\n    console.error(\"Cannot update server with empty URL\");\n    return;\n  }\n\n  console.log(`Updating server with URL for tab ${tabId}: ${url}`);\n\n  // Get the saved settings\n  chrome.storage.local.get([\"browserConnectorSettings\"], async (result) => {\n    const settings = result.browserConnectorSettings || {\n      serverHost: \"localhost\",\n      serverPort: 3025,\n    };\n\n    // Maximum number of retry attempts\n    const maxRetries = 3;\n    let retryCount = 0;\n    let success = false;\n\n    while (retryCount < maxRetries && !success) {\n      try {\n        // Send the URL to the server\n        const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/current-url`;\n        console.log(\n          `Attempt ${\n            retryCount + 1\n          }/${maxRetries} to update server with URL: ${url}`\n        );\n\n        const response = await fetch(serverUrl, {\n          method: \"POST\",\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n          body: JSON.stringify({\n            url: url,\n            tabId: tabId,\n            timestamp: Date.now(),\n            source: source,\n          }),\n          // Add a timeout to prevent hanging requests\n          signal: AbortSignal.timeout(5000),\n        });\n\n        if (response.ok) {\n          const responseData = await response.json();\n          console.log(\n            `Successfully updated server with URL: ${url}`,\n            responseData\n          );\n          success = true;\n        } else {\n          console.error(\n            `Server returned error: ${response.status} ${response.statusText}`\n          );\n          retryCount++;\n          // Wait before retrying\n          await new Promise((resolve) => setTimeout(resolve, 500));\n        }\n      } catch (error) {\n        console.error(`Error updating server with URL: ${error.message}`);\n        retryCount++;\n        // Wait before retrying\n        await new Promise((resolve) => setTimeout(resolve, 500));\n      }\n    }\n\n    if (!success) {\n      console.error(\n        `Failed to update server with URL after ${maxRetries} attempts`\n      );\n    }\n  });\n}\n\n// Clean up when tabs are closed\nchrome.tabs.onRemoved.addListener((tabId) => {\n  tabUrls.delete(tabId);\n});\n\n// Function to retest connection when a page is refreshed\nasync function retestConnectionOnRefresh(tabId) {\n  console.log(`Page refreshed in tab ${tabId}, retesting connection...`);\n\n  // Get the saved settings\n  chrome.storage.local.get([\"browserConnectorSettings\"], async (result) => {\n    const settings = result.browserConnectorSettings || {\n      serverHost: \"localhost\",\n      serverPort: 3025,\n    };\n\n    // Test the connection with the last known host and port\n    const isConnected = await validateServerIdentity(\n      settings.serverHost,\n      settings.serverPort\n    );\n\n    // Notify all devtools instances about the connection status\n    chrome.runtime.sendMessage({\n      type: \"CONNECTION_STATUS_UPDATE\",\n      isConnected: isConnected,\n      tabId: tabId,\n    });\n\n    // Always notify for page refresh, whether connected or not\n    // This ensures any ongoing discovery is cancelled and restarted\n    chrome.runtime.sendMessage({\n      type: \"INITIATE_AUTO_DISCOVERY\",\n      reason: \"page_refresh\",\n      tabId: tabId,\n      forceRestart: true, // Add a flag to indicate this should force restart any ongoing processes\n    });\n\n    if (!isConnected) {\n      console.log(\n        \"Connection test failed after page refresh, initiating auto-discovery...\"\n      );\n    } else {\n      console.log(\"Connection test successful after page refresh\");\n    }\n  });\n}\n\n// Function to capture and send screenshot\nfunction captureAndSendScreenshot(message, settings, sendResponse) {\n  // Get the inspected window's tab\n  chrome.tabs.get(message.tabId, (tab) => {\n    if (chrome.runtime.lastError) {\n      console.error(\"Error getting tab:\", chrome.runtime.lastError);\n      sendResponse({\n        success: false,\n        error: chrome.runtime.lastError.message,\n      });\n      return;\n    }\n\n    // Get all windows to find the one containing our tab\n    chrome.windows.getAll({ populate: true }, (windows) => {\n      const targetWindow = windows.find((w) =>\n        w.tabs.some((t) => t.id === message.tabId)\n      );\n\n      if (!targetWindow) {\n        console.error(\"Could not find window containing the inspected tab\");\n        sendResponse({\n          success: false,\n          error: \"Could not find window containing the inspected tab\",\n        });\n        return;\n      }\n\n      // Capture screenshot of the window containing our tab\n      chrome.tabs.captureVisibleTab(\n        targetWindow.id,\n        { format: \"png\" },\n        (dataUrl) => {\n          // Ignore DevTools panel capture error if it occurs\n          if (\n            chrome.runtime.lastError &&\n            !chrome.runtime.lastError.message.includes(\"devtools://\")\n          ) {\n            console.error(\n              \"Error capturing screenshot:\",\n              chrome.runtime.lastError\n            );\n            sendResponse({\n              success: false,\n              error: chrome.runtime.lastError.message,\n            });\n            return;\n          }\n\n          // Send screenshot data to browser connector using configured settings\n          const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/screenshot`;\n          console.log(`Sending screenshot to ${serverUrl}`);\n\n          fetch(serverUrl, {\n            method: \"POST\",\n            headers: {\n              \"Content-Type\": \"application/json\",\n            },\n            body: JSON.stringify({\n              data: dataUrl,\n              path: message.screenshotPath,\n            }),\n          })\n            .then((response) => response.json())\n            .then((result) => {\n              if (result.error) {\n                console.error(\"Error from server:\", result.error);\n                sendResponse({ success: false, error: result.error });\n              } else {\n                console.log(\"Screenshot saved successfully:\", result.path);\n                // Send success response even if DevTools capture failed\n                sendResponse({\n                  success: true,\n                  path: result.path,\n                  title: tab.title || \"Current Tab\",\n                });\n              }\n            })\n            .catch((error) => {\n              console.error(\"Error sending screenshot data:\", error);\n              sendResponse({\n                success: false,\n                error: error.message || \"Failed to save screenshot\",\n              });\n            });\n        }\n      );\n    });\n  });\n}\n"
  },
  {
    "path": "chrome-extension/devtools.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>BrowserTools MCP</title>\n  </head>\n  <body>\n    <!-- DevTools extension script -->\n    <script src=\"devtools.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "chrome-extension/devtools.js",
    "content": "// devtools.js\n\n// Store settings with defaults\nlet settings = {\n  logLimit: 50,\n  queryLimit: 30000,\n  stringSizeLimit: 500,\n  maxLogSize: 20000,\n  showRequestHeaders: false,\n  showResponseHeaders: false,\n  screenshotPath: \"\", // Add new setting for screenshot path\n  serverHost: \"localhost\", // Default server host\n  serverPort: 3025, // Default server port\n  allowAutoPaste: false, // Default auto-paste setting\n};\n\n// Keep track of debugger state\nlet isDebuggerAttached = false;\nlet attachDebuggerRetries = 0;\nconst currentTabId = chrome.devtools.inspectedWindow.tabId;\nconst MAX_ATTACH_RETRIES = 3;\nconst ATTACH_RETRY_DELAY = 1000; // 1 second\n\n// Load saved settings on startup\nchrome.storage.local.get([\"browserConnectorSettings\"], (result) => {\n  if (result.browserConnectorSettings) {\n    settings = { ...settings, ...result.browserConnectorSettings };\n  }\n});\n\n// Listen for settings updates\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  if (message.type === \"SETTINGS_UPDATED\") {\n    settings = message.settings;\n\n    // If server settings changed and we have a WebSocket, reconnect\n    if (\n      ws &&\n      (message.settings.serverHost !== settings.serverHost ||\n        message.settings.serverPort !== settings.serverPort)\n    ) {\n      console.log(\"Server settings changed, reconnecting WebSocket...\");\n      setupWebSocket();\n    }\n  }\n\n  // Handle connection status updates from page refreshes\n  if (message.type === \"CONNECTION_STATUS_UPDATE\") {\n    console.log(\n      `DevTools received connection status update: ${\n        message.isConnected ? \"Connected\" : \"Disconnected\"\n      }`\n    );\n\n    // If connection is lost, try to reestablish WebSocket only if we had a previous connection\n    if (!message.isConnected && ws) {\n      console.log(\n        \"Connection lost after page refresh, will attempt to reconnect WebSocket\"\n      );\n\n      // Only reconnect if we actually have a WebSocket that might be stale\n      if (\n        ws &&\n        (ws.readyState === WebSocket.CLOSED ||\n          ws.readyState === WebSocket.CLOSING)\n      ) {\n        console.log(\"WebSocket is already closed or closing, will reconnect\");\n        setupWebSocket();\n      }\n    }\n  }\n\n  // Handle auto-discovery requests after page refreshes\n  if (message.type === \"INITIATE_AUTO_DISCOVERY\") {\n    console.log(\n      `DevTools initiating WebSocket reconnect after page refresh (reason: ${message.reason})`\n    );\n\n    // For page refreshes with forceRestart, we should always reconnect if our current connection is not working\n    if (\n      (message.reason === \"page_refresh\" || message.forceRestart === true) &&\n      (!ws || ws.readyState !== WebSocket.OPEN)\n    ) {\n      console.log(\n        \"Page refreshed and WebSocket not open - forcing reconnection\"\n      );\n\n      // Close existing WebSocket if any\n      if (ws) {\n        console.log(\"Closing existing WebSocket due to page refresh\");\n        intentionalClosure = true; // Mark as intentional to prevent auto-reconnect\n        try {\n          ws.close();\n        } catch (e) {\n          console.error(\"Error closing WebSocket:\", e);\n        }\n        ws = null;\n        intentionalClosure = false; // Reset flag\n      }\n\n      // Clear any pending reconnect timeouts\n      if (wsReconnectTimeout) {\n        clearTimeout(wsReconnectTimeout);\n        wsReconnectTimeout = null;\n      }\n\n      // Try to reestablish the WebSocket connection\n      setupWebSocket();\n    }\n  }\n});\n\n// Utility to recursively truncate strings in any data structure\nfunction truncateStringsInData(data, maxLength, depth = 0, path = \"\") {\n  // Add depth limit to prevent circular references\n  if (depth > 100) {\n    console.warn(\"Max depth exceeded at path:\", path);\n    return \"[MAX_DEPTH_EXCEEDED]\";\n  }\n\n  console.log(`Processing at path: ${path}, type:`, typeof data);\n\n  if (typeof data === \"string\") {\n    if (data.length > maxLength) {\n      console.log(\n        `Truncating string at path ${path} from ${data.length} to ${maxLength}`\n      );\n      return data.substring(0, maxLength) + \"... (truncated)\";\n    }\n    return data;\n  }\n\n  if (Array.isArray(data)) {\n    console.log(`Processing array at path ${path} with length:`, data.length);\n    return data.map((item, index) =>\n      truncateStringsInData(item, maxLength, depth + 1, `${path}[${index}]`)\n    );\n  }\n\n  if (typeof data === \"object\" && data !== null) {\n    console.log(\n      `Processing object at path ${path} with keys:`,\n      Object.keys(data)\n    );\n    const result = {};\n    for (const [key, value] of Object.entries(data)) {\n      try {\n        result[key] = truncateStringsInData(\n          value,\n          maxLength,\n          depth + 1,\n          path ? `${path}.${key}` : key\n        );\n      } catch (e) {\n        console.error(`Error processing key ${key} at path ${path}:`, e);\n        result[key] = \"[ERROR_PROCESSING]\";\n      }\n    }\n    return result;\n  }\n\n  return data;\n}\n\n// Helper to calculate the size of an object\nfunction calculateObjectSize(obj) {\n  return JSON.stringify(obj).length;\n}\n\n// Helper to process array of objects with size limit\nfunction processArrayWithSizeLimit(array, maxTotalSize, processFunc) {\n  let currentSize = 0;\n  const result = [];\n\n  for (const item of array) {\n    // Process the item first\n    const processedItem = processFunc(item);\n    const itemSize = calculateObjectSize(processedItem);\n\n    // Check if adding this item would exceed the limit\n    if (currentSize + itemSize > maxTotalSize) {\n      console.log(\n        `Reached size limit (${currentSize}/${maxTotalSize}), truncating array`\n      );\n      break;\n    }\n\n    // Add item and update size\n    result.push(processedItem);\n    currentSize += itemSize;\n    console.log(\n      `Added item of size ${itemSize}, total size now: ${currentSize}`\n    );\n  }\n\n  return result;\n}\n\n// Modified processJsonString to handle arrays with size limit\nfunction processJsonString(jsonString, maxLength) {\n  console.log(\"Processing string of length:\", jsonString?.length);\n  try {\n    let parsed;\n    try {\n      parsed = JSON.parse(jsonString);\n      console.log(\n        \"Successfully parsed as JSON, structure:\",\n        JSON.stringify(Object.keys(parsed))\n      );\n    } catch (e) {\n      console.log(\"Not valid JSON, treating as string\");\n      return truncateStringsInData(jsonString, maxLength, 0, \"root\");\n    }\n\n    // If it's an array, process with size limit\n    if (Array.isArray(parsed)) {\n      console.log(\"Processing array of objects with size limit\");\n      const processed = processArrayWithSizeLimit(\n        parsed,\n        settings.maxLogSize,\n        (item) => truncateStringsInData(item, maxLength, 0, \"root\")\n      );\n      const result = JSON.stringify(processed);\n      console.log(\n        `Processed array: ${parsed.length} -> ${processed.length} items`\n      );\n      return result;\n    }\n\n    // Otherwise process as before\n    const processed = truncateStringsInData(parsed, maxLength, 0, \"root\");\n    const result = JSON.stringify(processed);\n    console.log(\"Processed JSON string length:\", result.length);\n    return result;\n  } catch (e) {\n    console.error(\"Error in processJsonString:\", e);\n    return jsonString.substring(0, maxLength) + \"... (truncated)\";\n  }\n}\n\n// Helper to send logs to browser-connector\nasync function sendToBrowserConnector(logData) {\n  if (!logData) {\n    console.error(\"No log data provided to sendToBrowserConnector\");\n    return;\n  }\n\n  // First, ensure we're connecting to the right server\n  if (!(await validateServerIdentity())) {\n    console.error(\n      \"Cannot send logs: Not connected to a valid browser tools server\"\n    );\n    return;\n  }\n\n  console.log(\"Sending log data to browser connector:\", {\n    type: logData.type,\n    timestamp: logData.timestamp,\n  });\n\n  // Process any string fields that might contain JSON\n  const processedData = { ...logData };\n\n  if (logData.type === \"network-request\") {\n    console.log(\"Processing network request\");\n    if (processedData.requestBody) {\n      console.log(\n        \"Request body size before:\",\n        processedData.requestBody.length\n      );\n      processedData.requestBody = processJsonString(\n        processedData.requestBody,\n        settings.stringSizeLimit\n      );\n      console.log(\"Request body size after:\", processedData.requestBody.length);\n    }\n    if (processedData.responseBody) {\n      console.log(\n        \"Response body size before:\",\n        processedData.responseBody.length\n      );\n      processedData.responseBody = processJsonString(\n        processedData.responseBody,\n        settings.stringSizeLimit\n      );\n      console.log(\n        \"Response body size after:\",\n        processedData.responseBody.length\n      );\n    }\n  } else if (\n    logData.type === \"console-log\" ||\n    logData.type === \"console-error\"\n  ) {\n    console.log(\"Processing console message\");\n    if (processedData.message) {\n      console.log(\"Message size before:\", processedData.message.length);\n      processedData.message = processJsonString(\n        processedData.message,\n        settings.stringSizeLimit\n      );\n      console.log(\"Message size after:\", processedData.message.length);\n    }\n  }\n\n  // Add settings to the request\n  const payload = {\n    data: {\n      ...processedData,\n      timestamp: Date.now(),\n    },\n    settings: {\n      logLimit: settings.logLimit,\n      queryLimit: settings.queryLimit,\n      showRequestHeaders: settings.showRequestHeaders,\n      showResponseHeaders: settings.showResponseHeaders,\n    },\n  };\n\n  const finalPayloadSize = JSON.stringify(payload).length;\n  console.log(\"Final payload size:\", finalPayloadSize);\n\n  if (finalPayloadSize > 1000000) {\n    console.warn(\"Warning: Large payload detected:\", finalPayloadSize);\n    console.warn(\n      \"Payload preview:\",\n      JSON.stringify(payload).substring(0, 1000) + \"...\"\n    );\n  }\n\n  const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/extension-log`;\n  console.log(`Sending log to ${serverUrl}`);\n\n  fetch(serverUrl, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n    body: JSON.stringify(payload),\n  })\n    .then((response) => {\n      if (!response.ok) {\n        throw new Error(`HTTP error ${response.status}`);\n      }\n      return response.json();\n    })\n    .then((data) => {\n      console.log(\"Log sent successfully:\", data);\n    })\n    .catch((error) => {\n      console.error(\"Error sending log:\", error);\n    });\n}\n\n// Validate server identity\nasync function validateServerIdentity() {\n  try {\n    console.log(\n      `Validating server identity at ${settings.serverHost}:${settings.serverPort}...`\n    );\n\n    // Use fetch with a timeout to prevent long-hanging requests\n    const response = await fetch(\n      `http://${settings.serverHost}:${settings.serverPort}/.identity`,\n      {\n        signal: AbortSignal.timeout(3000), // 3 second timeout\n      }\n    );\n\n    if (!response.ok) {\n      console.error(\n        `Server identity validation failed: HTTP ${response.status}`\n      );\n\n      // Notify about the connection failure\n      chrome.runtime.sendMessage({\n        type: \"SERVER_VALIDATION_FAILED\",\n        reason: \"http_error\",\n        status: response.status,\n        serverHost: settings.serverHost,\n        serverPort: settings.serverPort,\n      });\n\n      return false;\n    }\n\n    const identity = await response.json();\n\n    // Validate signature\n    if (identity.signature !== \"mcp-browser-connector-24x7\") {\n      console.error(\"Server identity validation failed: Invalid signature\");\n\n      // Notify about the invalid signature\n      chrome.runtime.sendMessage({\n        type: \"SERVER_VALIDATION_FAILED\",\n        reason: \"invalid_signature\",\n        serverHost: settings.serverHost,\n        serverPort: settings.serverPort,\n      });\n\n      return false;\n    }\n\n    console.log(\n      `Server identity confirmed: ${identity.name} v${identity.version}`\n    );\n\n    // Notify about successful validation\n    chrome.runtime.sendMessage({\n      type: \"SERVER_VALIDATION_SUCCESS\",\n      serverInfo: identity,\n      serverHost: settings.serverHost,\n      serverPort: settings.serverPort,\n    });\n\n    return true;\n  } catch (error) {\n    console.error(\"Server identity validation failed:\", error);\n\n    // Notify about the connection error\n    chrome.runtime.sendMessage({\n      type: \"SERVER_VALIDATION_FAILED\",\n      reason: \"connection_error\",\n      error: error.message,\n      serverHost: settings.serverHost,\n      serverPort: settings.serverPort,\n    });\n\n    return false;\n  }\n}\n\n// Function to clear logs on the server\nfunction wipeLogs() {\n  console.log(\"Wiping all logs...\");\n\n  const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/wipelogs`;\n  console.log(`Sending wipe request to ${serverUrl}`);\n\n  fetch(serverUrl, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n  })\n    .then((response) => {\n      if (!response.ok) {\n        throw new Error(`HTTP error ${response.status}`);\n      }\n      return response.json();\n    })\n    .then((data) => {\n      console.log(\"Logs wiped successfully:\", data);\n    })\n    .catch((error) => {\n      console.error(\"Error wiping logs:\", error);\n    });\n}\n\n// Listen for page refreshes\nchrome.devtools.network.onNavigated.addListener((url) => {\n  console.log(\"Page navigated/refreshed - wiping logs\");\n  wipeLogs();\n\n  // Send the new URL to the server\n  if (ws && ws.readyState === WebSocket.OPEN && url) {\n    console.log(\n      \"Chrome Extension: Sending page-navigated event with URL:\",\n      url\n    );\n    ws.send(\n      JSON.stringify({\n        type: \"page-navigated\",\n        url: url,\n        tabId: chrome.devtools.inspectedWindow.tabId,\n        timestamp: Date.now(),\n      })\n    );\n  }\n});\n\n// 1) Listen for network requests\nchrome.devtools.network.onRequestFinished.addListener((request) => {\n  if (request._resourceType === \"xhr\" || request._resourceType === \"fetch\") {\n    request.getContent((responseBody) => {\n      const entry = {\n        type: \"network-request\",\n        url: request.request.url,\n        method: request.request.method,\n        status: request.response.status,\n        requestHeaders: request.request.headers,\n        responseHeaders: request.response.headers,\n        requestBody: request.request.postData?.text ?? \"\",\n        responseBody: responseBody ?? \"\",\n      };\n      sendToBrowserConnector(entry);\n    });\n  }\n});\n\n// Helper function to attach debugger\nasync function attachDebugger() {\n  // First check if we're already attached to this tab\n  chrome.debugger.getTargets((targets) => {\n    const isAlreadyAttached = targets.some(\n      (target) => target.tabId === currentTabId && target.attached\n    );\n\n    if (isAlreadyAttached) {\n      console.log(\"Found existing debugger attachment, detaching first...\");\n      // Force detach first to ensure clean state\n      chrome.debugger.detach({ tabId: currentTabId }, () => {\n        // Ignore any errors during detach\n        if (chrome.runtime.lastError) {\n          console.log(\"Error during forced detach:\", chrome.runtime.lastError);\n        }\n        // Now proceed with fresh attachment\n        performAttach();\n      });\n    } else {\n      // No existing attachment, proceed directly\n      performAttach();\n    }\n  });\n}\n\nfunction performAttach() {\n  console.log(\"Performing debugger attachment to tab:\", currentTabId);\n  chrome.debugger.attach({ tabId: currentTabId }, \"1.3\", () => {\n    if (chrome.runtime.lastError) {\n      console.error(\"Failed to attach debugger:\", chrome.runtime.lastError);\n      isDebuggerAttached = false;\n      return;\n    }\n\n    isDebuggerAttached = true;\n    console.log(\"Debugger successfully attached\");\n\n    // Add the event listener when attaching\n    chrome.debugger.onEvent.addListener(consoleMessageListener);\n\n    chrome.debugger.sendCommand(\n      { tabId: currentTabId },\n      \"Runtime.enable\",\n      {},\n      () => {\n        if (chrome.runtime.lastError) {\n          console.error(\"Failed to enable runtime:\", chrome.runtime.lastError);\n          return;\n        }\n        console.log(\"Runtime API successfully enabled\");\n      }\n    );\n  });\n}\n\n// Helper function to detach debugger\nfunction detachDebugger() {\n  // Remove the event listener first\n  chrome.debugger.onEvent.removeListener(consoleMessageListener);\n\n  // Check if debugger is actually attached before trying to detach\n  chrome.debugger.getTargets((targets) => {\n    const isStillAttached = targets.some(\n      (target) => target.tabId === currentTabId && target.attached\n    );\n\n    if (!isStillAttached) {\n      console.log(\"Debugger already detached\");\n      isDebuggerAttached = false;\n      return;\n    }\n\n    chrome.debugger.detach({ tabId: currentTabId }, () => {\n      if (chrome.runtime.lastError) {\n        console.warn(\n          \"Warning during debugger detach:\",\n          chrome.runtime.lastError\n        );\n      }\n      isDebuggerAttached = false;\n      console.log(\"Debugger detached\");\n    });\n  });\n}\n\n// Move the console message listener outside the panel creation\nconst consoleMessageListener = (source, method, params) => {\n  // Only process events for our tab\n  if (source.tabId !== currentTabId) {\n    return;\n  }\n\n  if (method === \"Runtime.exceptionThrown\") {\n    const entry = {\n      type: \"console-error\",\n      message:\n        params.exceptionDetails.exception?.description ||\n        JSON.stringify(params.exceptionDetails),\n      level: \"error\",\n      timestamp: Date.now(),\n    };\n    console.log(\"Sending runtime exception:\", entry);\n    sendToBrowserConnector(entry);\n  }\n\n  if (method === \"Runtime.consoleAPICalled\") {\n    // Process all arguments from the console call\n    let formattedMessage = \"\";\n    const args = params.args || [];\n\n    // Extract all arguments and combine them\n    if (args.length > 0) {\n      // Try to build a meaningful representation of all arguments\n      try {\n        formattedMessage = args\n          .map((arg) => {\n            // Handle different types of arguments\n            if (arg.type === \"string\") {\n              return arg.value;\n            } else if (arg.type === \"object\" && arg.preview) {\n              // For objects, include their preview or description\n              return JSON.stringify(arg.preview);\n            } else if (arg.description) {\n              // Some objects have descriptions\n              return arg.description;\n            } else {\n              // Fallback for other types\n              return arg.value || arg.description || JSON.stringify(arg);\n            }\n          })\n          .join(\" \");\n      } catch (e) {\n        // Fallback if processing fails\n        console.error(\"Failed to process console arguments:\", e);\n        formattedMessage =\n          args[0]?.value || \"Unable to process console arguments\";\n      }\n    }\n\n    const entry = {\n      type: params.type === \"error\" ? \"console-error\" : \"console-log\",\n      level: params.type,\n      message: formattedMessage,\n      timestamp: Date.now(),\n    };\n    console.log(\"Sending console entry:\", entry);\n    sendToBrowserConnector(entry);\n  }\n};\n\n// 2) Use DevTools Protocol to capture console logs\nchrome.devtools.panels.create(\"BrowserToolsMCP\", \"\", \"panel.html\", (panel) => {\n  // Initial attach - we'll keep the debugger attached as long as DevTools is open\n  attachDebugger();\n\n  // Handle panel showing\n  panel.onShown.addListener((panelWindow) => {\n    if (!isDebuggerAttached) {\n      attachDebugger();\n    }\n  });\n});\n\n// Clean up when DevTools closes\nwindow.addEventListener(\"unload\", () => {\n  // Detach debugger\n  detachDebugger();\n\n  // Set intentional closure flag before closing\n  intentionalClosure = true;\n\n  if (ws) {\n    try {\n      ws.close();\n    } catch (e) {\n      console.error(\"Error closing WebSocket during unload:\", e);\n    }\n    ws = null;\n  }\n\n  if (wsReconnectTimeout) {\n    clearTimeout(wsReconnectTimeout);\n    wsReconnectTimeout = null;\n  }\n\n  if (heartbeatInterval) {\n    clearInterval(heartbeatInterval);\n    heartbeatInterval = null;\n  }\n});\n\n// Function to capture and send element data\nfunction captureAndSendElement() {\n  chrome.devtools.inspectedWindow.eval(\n    `(function() {\n      const el = $0;  // $0 is the currently selected element in DevTools\n      if (!el) return null;\n\n      const rect = el.getBoundingClientRect();\n\n      return {\n        tagName: el.tagName,\n        id: el.id,\n        className: el.className,\n        textContent: el.textContent?.substring(0, 100),\n        attributes: Array.from(el.attributes).map(attr => ({\n          name: attr.name,\n          value: attr.value\n        })),\n        dimensions: {\n          width: rect.width,\n          height: rect.height,\n          top: rect.top,\n          left: rect.left\n        },\n        innerHTML: el.innerHTML.substring(0, 500)\n      };\n    })()`,\n    (result, isException) => {\n      if (isException || !result) return;\n\n      console.log(\"Element selected:\", result);\n\n      // Send to browser connector\n      sendToBrowserConnector({\n        type: \"selected-element\",\n        timestamp: Date.now(),\n        element: result,\n      });\n    }\n  );\n}\n\n// Listen for element selection in the Elements panel\nchrome.devtools.panels.elements.onSelectionChanged.addListener(() => {\n  captureAndSendElement();\n});\n\n// WebSocket connection management\nlet ws = null;\nlet wsReconnectTimeout = null;\nlet heartbeatInterval = null;\nconst WS_RECONNECT_DELAY = 5000; // 5 seconds\nconst HEARTBEAT_INTERVAL = 30000; // 30 seconds\n// Add a flag to track if we need to reconnect after identity validation\nlet reconnectAfterValidation = false;\n// Track if we're intentionally closing the connection\nlet intentionalClosure = false;\n\n// Function to send a heartbeat to keep the WebSocket connection alive\nfunction sendHeartbeat() {\n  if (ws && ws.readyState === WebSocket.OPEN) {\n    console.log(\"Chrome Extension: Sending WebSocket heartbeat\");\n    ws.send(JSON.stringify({ type: \"heartbeat\" }));\n  }\n}\n\nasync function setupWebSocket() {\n  // Clear any pending timeouts\n  if (wsReconnectTimeout) {\n    clearTimeout(wsReconnectTimeout);\n    wsReconnectTimeout = null;\n  }\n\n  if (heartbeatInterval) {\n    clearInterval(heartbeatInterval);\n    heartbeatInterval = null;\n  }\n\n  // Close existing WebSocket if any\n  if (ws) {\n    // Set flag to indicate this is an intentional closure\n    intentionalClosure = true;\n    try {\n      ws.close();\n    } catch (e) {\n      console.error(\"Error closing existing WebSocket:\", e);\n    }\n    ws = null;\n    intentionalClosure = false; // Reset flag\n  }\n\n  // Validate server identity before connecting\n  console.log(\"Validating server identity before WebSocket connection...\");\n  const isValid = await validateServerIdentity();\n\n  if (!isValid) {\n    console.error(\n      \"Cannot establish WebSocket: Not connected to a valid browser tools server\"\n    );\n    // Set flag to indicate we need to reconnect after a page refresh check\n    reconnectAfterValidation = true;\n\n    // Try again after delay\n    wsReconnectTimeout = setTimeout(() => {\n      console.log(\"Attempting to reconnect WebSocket after validation failure\");\n      setupWebSocket();\n    }, WS_RECONNECT_DELAY);\n    return;\n  }\n\n  // Reset reconnect flag since validation succeeded\n  reconnectAfterValidation = false;\n\n  const wsUrl = `ws://${settings.serverHost}:${settings.serverPort}/extension-ws`;\n  console.log(`Connecting to WebSocket at ${wsUrl}`);\n\n  try {\n    ws = new WebSocket(wsUrl);\n\n    ws.onopen = () => {\n      console.log(`Chrome Extension: WebSocket connected to ${wsUrl}`);\n\n      // Start heartbeat to keep connection alive\n      heartbeatInterval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);\n\n      // Notify that connection is successful\n      chrome.runtime.sendMessage({\n        type: \"WEBSOCKET_CONNECTED\",\n        serverHost: settings.serverHost,\n        serverPort: settings.serverPort,\n      });\n\n      // Send the current URL to the server right after connection\n      // This ensures the server has the URL even if no navigation occurs\n      chrome.runtime.sendMessage(\n        {\n          type: \"GET_CURRENT_URL\",\n          tabId: chrome.devtools.inspectedWindow.tabId,\n        },\n        (response) => {\n          if (chrome.runtime.lastError) {\n            console.error(\n              \"Chrome Extension: Error getting URL from background on connection:\",\n              chrome.runtime.lastError\n            );\n\n            // If normal method fails, try fallback to chrome.tabs API directly\n            tryFallbackGetUrl();\n            return;\n          }\n\n          if (response && response.url) {\n            console.log(\n              \"Chrome Extension: Sending initial URL to server:\",\n              response.url\n            );\n\n            // Send the URL to the server via the background script\n            chrome.runtime.sendMessage({\n              type: \"UPDATE_SERVER_URL\",\n              tabId: chrome.devtools.inspectedWindow.tabId,\n              url: response.url,\n              source: \"initial_connection\",\n            });\n          } else {\n            // If response exists but no URL, try fallback\n            tryFallbackGetUrl();\n          }\n        }\n      );\n\n      // Fallback method to get URL directly\n      function tryFallbackGetUrl() {\n        console.log(\"Chrome Extension: Trying fallback method to get URL\");\n\n        // Try to get the URL directly using the tabs API\n        chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {\n          if (chrome.runtime.lastError) {\n            console.error(\n              \"Chrome Extension: Fallback URL retrieval failed:\",\n              chrome.runtime.lastError\n            );\n            return;\n          }\n\n          if (tabs && tabs.length > 0 && tabs[0].url) {\n            console.log(\n              \"Chrome Extension: Got URL via fallback method:\",\n              tabs[0].url\n            );\n\n            // Send the URL to the server\n            chrome.runtime.sendMessage({\n              type: \"UPDATE_SERVER_URL\",\n              tabId: chrome.devtools.inspectedWindow.tabId,\n              url: tabs[0].url,\n              source: \"fallback_method\",\n            });\n          } else {\n            console.warn(\n              \"Chrome Extension: Could not retrieve URL through fallback method\"\n            );\n          }\n        });\n      }\n    };\n\n    ws.onerror = (error) => {\n      console.error(`Chrome Extension: WebSocket error for ${wsUrl}:`, error);\n    };\n\n    ws.onclose = (event) => {\n      console.log(`Chrome Extension: WebSocket closed for ${wsUrl}:`, event);\n\n      // Stop heartbeat\n      if (heartbeatInterval) {\n        clearInterval(heartbeatInterval);\n        heartbeatInterval = null;\n      }\n\n      // Don't reconnect if this was an intentional closure\n      if (intentionalClosure) {\n        console.log(\n          \"Chrome Extension: Intentional WebSocket closure, not reconnecting\"\n        );\n        return;\n      }\n\n      // Only attempt to reconnect if the closure wasn't intentional\n      // Code 1000 (Normal Closure) and 1001 (Going Away) are normal closures\n      // Code 1005 often happens with clean closures in Chrome\n      const isAbnormalClosure = !(event.code === 1000 || event.code === 1001);\n\n      // Check if this was an abnormal closure or if we need to reconnect after validation\n      if (isAbnormalClosure || reconnectAfterValidation) {\n        console.log(\n          `Chrome Extension: Will attempt to reconnect WebSocket (closure code: ${event.code})`\n        );\n\n        // Try to reconnect after delay\n        wsReconnectTimeout = setTimeout(() => {\n          console.log(\n            `Chrome Extension: Attempting to reconnect WebSocket to ${wsUrl}`\n          );\n          setupWebSocket();\n        }, WS_RECONNECT_DELAY);\n      } else {\n        console.log(\n          `Chrome Extension: Normal WebSocket closure, not reconnecting automatically`\n        );\n      }\n    };\n\n    ws.onmessage = async (event) => {\n      try {\n        const message = JSON.parse(event.data);\n\n        // Don't log heartbeat responses to reduce noise\n        if (message.type !== \"heartbeat-response\") {\n          console.log(\"Chrome Extension: Received WebSocket message:\", message);\n\n          if (message.type === \"server-shutdown\") {\n            console.log(\"Chrome Extension: Received server shutdown signal\");\n            // Clear any reconnection attempts\n            if (wsReconnectTimeout) {\n              clearTimeout(wsReconnectTimeout);\n              wsReconnectTimeout = null;\n            }\n            // Close the connection gracefully\n            ws.close(1000, \"Server shutting down\");\n            return;\n          }\n        }\n\n        if (message.type === \"heartbeat-response\") {\n          // Just a heartbeat response, no action needed\n          // Uncomment the next line for debug purposes only\n          // console.log(\"Chrome Extension: Received heartbeat response\");\n        } else if (message.type === \"take-screenshot\") {\n          console.log(\"Chrome Extension: Taking screenshot...\");\n          // Capture screenshot of the current tab\n          chrome.tabs.captureVisibleTab(null, { format: \"png\" }, (dataUrl) => {\n            if (chrome.runtime.lastError) {\n              console.error(\n                \"Chrome Extension: Screenshot capture failed:\",\n                chrome.runtime.lastError\n              );\n              ws.send(\n                JSON.stringify({\n                  type: \"screenshot-error\",\n                  error: chrome.runtime.lastError.message,\n                  requestId: message.requestId,\n                })\n              );\n              return;\n            }\n\n            console.log(\"Chrome Extension: Screenshot captured successfully\");\n            // Just send the screenshot data, let the server handle paths\n            const response = {\n              type: \"screenshot-data\",\n              data: dataUrl,\n              requestId: message.requestId,\n              // Only include path if it's configured in settings\n              ...(settings.screenshotPath && { path: settings.screenshotPath }),\n              // Include auto-paste setting\n              autoPaste: settings.allowAutoPaste,\n            };\n\n            console.log(\"Chrome Extension: Sending screenshot data response\", {\n              ...response,\n              data: \"[base64 data]\",\n            });\n\n            ws.send(JSON.stringify(response));\n          });\n        } else if (message.type === \"get-current-url\") {\n          console.log(\"Chrome Extension: Received request for current URL\");\n\n          // Get the current URL from the background script instead of inspectedWindow.eval\n          let retryCount = 0;\n          const maxRetries = 2;\n\n          const requestCurrentUrl = () => {\n            chrome.runtime.sendMessage(\n              {\n                type: \"GET_CURRENT_URL\",\n                tabId: chrome.devtools.inspectedWindow.tabId,\n              },\n              (response) => {\n                if (chrome.runtime.lastError) {\n                  console.error(\n                    \"Chrome Extension: Error getting URL from background:\",\n                    chrome.runtime.lastError\n                  );\n\n                  // Retry logic\n                  if (retryCount < maxRetries) {\n                    retryCount++;\n                    console.log(\n                      `Retrying URL request (${retryCount}/${maxRetries})...`\n                    );\n                    setTimeout(requestCurrentUrl, 500); // Wait 500ms before retrying\n                    return;\n                  }\n\n                  ws.send(\n                    JSON.stringify({\n                      type: \"current-url-response\",\n                      url: null,\n                      tabId: chrome.devtools.inspectedWindow.tabId,\n                      error:\n                        \"Failed to get URL from background: \" +\n                        chrome.runtime.lastError.message,\n                      requestId: message.requestId,\n                    })\n                  );\n                  return;\n                }\n\n                if (response && response.success && response.url) {\n                  console.log(\n                    \"Chrome Extension: Got URL from background:\",\n                    response.url\n                  );\n                  ws.send(\n                    JSON.stringify({\n                      type: \"current-url-response\",\n                      url: response.url,\n                      tabId: chrome.devtools.inspectedWindow.tabId,\n                      requestId: message.requestId,\n                    })\n                  );\n                } else {\n                  console.error(\n                    \"Chrome Extension: Invalid URL response from background:\",\n                    response\n                  );\n\n                  // Last resort - try to get URL directly from the tab\n                  chrome.tabs.query(\n                    { active: true, currentWindow: true },\n                    (tabs) => {\n                      const url = tabs && tabs[0] && tabs[0].url;\n                      console.log(\n                        \"Chrome Extension: Got URL directly from tab:\",\n                        url\n                      );\n\n                      ws.send(\n                        JSON.stringify({\n                          type: \"current-url-response\",\n                          url: url || null,\n                          tabId: chrome.devtools.inspectedWindow.tabId,\n                          error:\n                            response?.error ||\n                            \"Failed to get URL from background\",\n                          requestId: message.requestId,\n                        })\n                      );\n                    }\n                  );\n                }\n              }\n            );\n          };\n\n          requestCurrentUrl();\n        }\n      } catch (error) {\n        console.error(\n          \"Chrome Extension: Error processing WebSocket message:\",\n          error\n        );\n      }\n    };\n  } catch (error) {\n    console.error(\"Error creating WebSocket:\", error);\n    // Try again after delay\n    wsReconnectTimeout = setTimeout(setupWebSocket, WS_RECONNECT_DELAY);\n  }\n}\n\n// Initialize WebSocket connection when DevTools opens\nsetupWebSocket();\n\n// Clean up WebSocket when DevTools closes\nwindow.addEventListener(\"unload\", () => {\n  if (ws) {\n    ws.close();\n  }\n  if (wsReconnectTimeout) {\n    clearTimeout(wsReconnectTimeout);\n  }\n});\n"
  },
  {
    "path": "chrome-extension/manifest.json",
    "content": "{\n    \"name\": \"BrowserTools MCP\",\n    \"version\": \"1.2.0\",\n    \"description\": \"MCP tool for AI code editors to capture data from a browser such as console logs, network requests, screenshots and more\",\n    \"manifest_version\": 3,\n    \"devtools_page\": \"devtools.html\",\n    \"permissions\": [\n      \"activeTab\",\n      \"debugger\",\n      \"storage\",\n      \"tabs\",\n      \"tabCapture\",\n      \"windows\"\n    ],\n    \"host_permissions\": [\n      \"<all_urls>\"\n    ],\n    \"background\": {\n      \"service_worker\": \"background.js\"\n    }\n}\n"
  },
  {
    "path": "chrome-extension/panel.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n    <style>\n        body {\n            padding: 16px;\n            font-family: system-ui, -apple-system, sans-serif;\n            background-color: #282828;\n            color: #fff;\n        }\n        .endpoint-list {\n            margin: 16px 0;\n        }\n        .endpoint-item {\n            display: flex;\n            gap: 8px;\n            margin-bottom: 8px;\n            align-items: center;\n        }\n        .endpoint-form {\n            display: flex;\n            gap: 8px;\n            margin-bottom: 16px;\n            align-items: center;\n        }\n        button {\n            padding: 4px 8px;\n        }\n        input {\n            padding: 4px;\n        }\n        .status-indicator {\n            width: 8px;\n            height: 8px;\n            border-radius: 50%;\n            display: inline-block;\n        }\n        .status-connected {\n            background: #4caf50;\n        }\n        .status-disconnected {\n            background: #f44336;\n        }\n        .form-group {\n            margin-bottom: 16px;\n        }\n        .form-group label {\n            display: block;\n            margin-bottom: 4px;\n        }\n        .checkbox-group {\n            margin-bottom: 8px;\n        }\n        .checkbox-group-2 {\n            margin-bottom: 6px;\n        }\n        input[type=\"number\"],\n        input[type=\"text\"] {\n            padding: 4px;\n            width: 200px;\n        }\n        .settings-section {\n            border: 1px solid #ccc;\n            padding: 16px;\n            margin-bottom: 16px;\n            border-radius: 4px;\n        }\n        .settings-header {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            cursor: pointer;\n            user-select: none;\n        }\n        .settings-header h3 {\n            margin: 0;\n        }\n        .settings-content {\n            display: none;\n            margin-top: 16px;\n        }\n        .settings-content.visible {\n            display: block;\n        }\n        .chevron {\n            width: 20px;\n            height: 20px;\n            transition: transform 0.3s ease;\n        }\n        .chevron.open {\n            transform: rotate(180deg);\n        }\n        .quick-actions {\n            display: flex;\n            gap: 8px;\n            margin-bottom: 16px;\n        }\n        .action-button {\n            background-color: #4a4a4a;\n            color: white;\n            border: none;\n            padding: 8px 16px;\n            border-radius: 4px;\n            cursor: pointer;\n            transition: background-color 0.2s;\n        }\n        .action-button:hover {\n            background-color: #5a5a5a;\n        }\n        .action-button.danger {\n            background-color: #f44336;\n        }\n        .action-button.danger:hover {\n            background-color: #d32f2f;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"settings-section\">\n        <h3>Quick Actions</h3>\n        <div class=\"quick-actions\">\n            <button id=\"capture-screenshot\" class=\"action-button\">\n                Capture Screenshot\n            </button>\n            <button id=\"wipe-logs\" class=\"action-button danger\">\n                Wipe All Logs\n            </button>\n        </div>\n        <div class=\"checkbox-group-2\" style=\"margin-top: 10px; display: flex; align-items: center;\">\n            <label>\n                <input type=\"checkbox\" id=\"allow-auto-paste\">\n                Allow Auto-paste to Cursor\n            </label>\n        </div>\n    </div>\n\n    <div class=\"settings-section\">\n        <h3>Screenshot Settings</h3>\n        <div class=\"form-group\">\n            <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>\n            <input type=\"text\" id=\"screenshot-path\" placeholder=\"/path/to/screenshots\">\n        </div>\n    </div>\n\n    <div class=\"settings-section\">\n        <h3>Server Connection Settings</h3>\n        <div class=\"form-group\">\n            <label for=\"server-host\">Server Host</label>\n            <input type=\"text\" id=\"server-host\" placeholder=\"localhost or IP address\">\n        </div>\n        <div class=\"form-group\">\n            <label for=\"server-port\">Server Port</label>\n            <input type=\"number\" id=\"server-port\" min=\"1\" max=\"65535\" value=\"3025\">\n        </div>\n        <div class=\"quick-actions\">\n            <button id=\"discover-server\" class=\"action-button\">\n                Auto-Discover Server\n            </button>\n            <button id=\"test-connection\" class=\"action-button\">\n                Test Connection\n            </button>\n        </div>\n        <div id=\"connection-status\" style=\"margin-top: 8px; display: none;\">\n            <span id=\"status-icon\" class=\"status-indicator\"></span>\n            <span id=\"status-text\"></span>\n        </div>\n    </div>\n\n    <div class=\"settings-section\">\n        <div class=\"settings-header\" id=\"advanced-settings-header\">\n            <h3>Advanced Settings</h3>\n            <svg class=\"chevron\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n                <polyline points=\"6 9 12 15 18 9\"></polyline>\n            </svg>\n        </div>\n        \n        <div class=\"settings-content\" id=\"advanced-settings-content\">\n            <div class=\"form-group\">\n                <label for=\"log-limit\">Log Limit (number of logs)</label>\n                <input type=\"number\" id=\"log-limit\" min=\"1\" value=\"50\">\n            </div>\n\n            <div class=\"form-group\">\n                <label for=\"query-limit\">Query Limit (characters)</label>\n                <input type=\"number\" id=\"query-limit\" min=\"1\" value=\"30000\">\n            </div>\n\n            <div class=\"form-group\">\n                <label for=\"string-size-limit\">String Size Limit (characters)</label>\n                <input type=\"number\" id=\"string-size-limit\" min=\"1\" value=\"500\">\n            </div>\n\n            <div class=\"form-group\">\n                <label for=\"max-log-size\">Max Log Size (characters)</label>\n                <input type=\"number\" id=\"max-log-size\" min=\"1000\" value=\"20000\">\n            </div>\n\n            <div class=\"checkbox-group\">\n                <label>\n                    <input type=\"checkbox\" id=\"show-request-headers\">\n                    Include Request Headers\n                </label>\n            </div>\n\n            <div class=\"checkbox-group\">\n                <label>\n                    <input type=\"checkbox\" id=\"show-response-headers\">\n                    Include Response Headers\n                </label>\n            </div>\n        </div>\n    </div>\n\n    <script src=\"panel.js\"></script>\n</body>\n</html> "
  },
  {
    "path": "chrome-extension/panel.js",
    "content": "// Store settings\nlet settings = {\n  logLimit: 50,\n  queryLimit: 30000,\n  stringSizeLimit: 500,\n  showRequestHeaders: false,\n  showResponseHeaders: false,\n  maxLogSize: 20000,\n  screenshotPath: \"\",\n  // Add server connection settings\n  serverHost: \"localhost\",\n  serverPort: 3025,\n  allowAutoPaste: false, // Default auto-paste setting\n};\n\n// Track connection status\nlet serverConnected = false;\nlet reconnectAttemptTimeout = null;\n// Add a flag to track ongoing discovery operations\nlet isDiscoveryInProgress = false;\n// Add an AbortController to cancel fetch operations\nlet discoveryController = null;\n\n// Load saved settings on startup\nchrome.storage.local.get([\"browserConnectorSettings\"], (result) => {\n  if (result.browserConnectorSettings) {\n    settings = { ...settings, ...result.browserConnectorSettings };\n    updateUIFromSettings();\n  }\n\n  // Create connection status banner at the top\n  createConnectionBanner();\n\n  // Automatically discover server on panel load with quiet mode enabled\n  discoverServer(true);\n});\n\n// Add listener for connection status updates from background script (page refresh events)\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  if (message.type === \"CONNECTION_STATUS_UPDATE\") {\n    console.log(\n      `Received connection status update: ${\n        message.isConnected ? \"Connected\" : \"Disconnected\"\n      }`\n    );\n\n    // Update UI based on connection status\n    if (message.isConnected) {\n      // If already connected, just maintain the current state\n      if (!serverConnected) {\n        // Connection was re-established, update UI\n        serverConnected = true;\n        updateConnectionBanner(true, {\n          name: \"Browser Tools Server\",\n          version: \"reconnected\",\n          host: settings.serverHost,\n          port: settings.serverPort,\n        });\n      }\n    } else {\n      // Connection lost, update UI to show disconnected\n      serverConnected = false;\n      updateConnectionBanner(false, null);\n    }\n  }\n\n  if (message.type === \"INITIATE_AUTO_DISCOVERY\") {\n    console.log(\n      `Initiating auto-discovery after page refresh (reason: ${message.reason})`\n    );\n\n    // For page refreshes or if forceRestart is set to true, always cancel any ongoing discovery and restart\n    if (message.reason === \"page_refresh\" || message.forceRestart === true) {\n      // Cancel any ongoing discovery operation\n      cancelOngoingDiscovery();\n\n      // Update UI to indicate we're starting a fresh scan\n      if (connectionStatusDiv) {\n        connectionStatusDiv.style.display = \"block\";\n        if (statusIcon) statusIcon.className = \"status-indicator\";\n        if (statusText)\n          statusText.textContent =\n            \"Page refreshed. Restarting server discovery...\";\n      }\n\n      // Always update the connection banner when a page refresh occurs\n      updateConnectionBanner(false, null);\n\n      // Start a new discovery process with quiet mode\n      console.log(\"Starting fresh discovery after page refresh\");\n      discoverServer(true);\n    }\n    // For other types of auto-discovery requests, only start if not already in progress\n    else if (!isDiscoveryInProgress) {\n      // Use quiet mode for auto-discovery to minimize UI changes\n      discoverServer(true);\n    }\n  }\n\n  // Handle successful server validation\n  if (message.type === \"SERVER_VALIDATION_SUCCESS\") {\n    console.log(\n      `Server validation successful: ${message.serverHost}:${message.serverPort}`\n    );\n\n    // Update the connection status banner\n    serverConnected = true;\n    updateConnectionBanner(true, message.serverInfo);\n\n    // If we were showing the connection status dialog, we can hide it now\n    if (connectionStatusDiv && connectionStatusDiv.style.display === \"block\") {\n      connectionStatusDiv.style.display = \"none\";\n    }\n  }\n\n  // Handle failed server validation\n  if (message.type === \"SERVER_VALIDATION_FAILED\") {\n    console.log(\n      `Server validation failed: ${message.reason} - ${message.serverHost}:${message.serverPort}`\n    );\n\n    // Update the connection status\n    serverConnected = false;\n    updateConnectionBanner(false, null);\n\n    // Start auto-discovery if this was a page refresh validation\n    if (\n      message.reason === \"connection_error\" ||\n      message.reason === \"http_error\"\n    ) {\n      // If we're not already trying to discover the server, start the process\n      if (!isDiscoveryInProgress) {\n        console.log(\"Starting auto-discovery after validation failure\");\n        discoverServer(true);\n      }\n    }\n  }\n\n  // Handle successful WebSocket connection\n  if (message.type === \"WEBSOCKET_CONNECTED\") {\n    console.log(\n      `WebSocket connected to ${message.serverHost}:${message.serverPort}`\n    );\n\n    // Update connection status if it wasn't already connected\n    if (!serverConnected) {\n      serverConnected = true;\n      updateConnectionBanner(true, {\n        name: \"Browser Tools Server\",\n        version: \"connected via WebSocket\",\n        host: message.serverHost,\n        port: message.serverPort,\n      });\n    }\n  }\n});\n\n// Create connection status banner\nfunction createConnectionBanner() {\n  // Check if banner already exists\n  if (document.getElementById(\"connection-banner\")) {\n    return;\n  }\n\n  // Create the banner\n  const banner = document.createElement(\"div\");\n  banner.id = \"connection-banner\";\n  banner.style.cssText = `\n    padding: 6px 0px; \n    margin-bottom: 4px;\n    width: 40%; \n    display: flex; \n    flex-direction: column;\n    align-items: flex-start; \n    background-color:rgba(0,0,0,0);\n    border-radius: 11px;\n    font-size: 11px;\n    font-weight: 500;\n    color: #ffffff;\n  `;\n\n  // Create reconnect button (now placed at the top)\n  const reconnectButton = document.createElement(\"button\");\n  reconnectButton.id = \"banner-reconnect-btn\";\n  reconnectButton.textContent = \"Reconnect\";\n  reconnectButton.style.cssText = `\n    background-color: #333333;\n    color: #ffffff;\n    border: 1px solid #444444;\n    border-radius: 3px;\n    padding: 2px 8px;\n    font-size: 10px;\n    cursor: pointer;\n    margin-bottom: 6px;\n    align-self: flex-start;\n    display: none;\n    transition: background-color 0.2s;\n  `;\n  reconnectButton.addEventListener(\"mouseover\", () => {\n    reconnectButton.style.backgroundColor = \"#444444\";\n  });\n  reconnectButton.addEventListener(\"mouseout\", () => {\n    reconnectButton.style.backgroundColor = \"#333333\";\n  });\n  reconnectButton.addEventListener(\"click\", () => {\n    // Hide the button while reconnecting\n    reconnectButton.style.display = \"none\";\n    reconnectButton.textContent = \"Reconnecting...\";\n\n    // Update UI to show searching state\n    updateConnectionBanner(false, null);\n\n    // Try to discover server\n    discoverServer(false);\n  });\n\n  // Create a container for the status indicator and text\n  const statusContainer = document.createElement(\"div\");\n  statusContainer.style.cssText = `\n    display: flex;\n    align-items: center;\n    width: 100%;\n  `;\n\n  // Create status indicator\n  const indicator = document.createElement(\"div\");\n  indicator.id = \"banner-status-indicator\";\n  indicator.style.cssText = `\n    width: 6px; \n    height: 6px; \n    position: relative;\n    top: 1px;\n    border-radius: 50%; \n    background-color: #ccc; \n    margin-right: 8px; \n    flex-shrink: 0;\n    transition: background-color 0.3s ease;\n  `;\n\n  // Create status text\n  const statusText = document.createElement(\"div\");\n  statusText.id = \"banner-status-text\";\n  statusText.textContent = \"Searching for server...\";\n  statusText.style.cssText =\n    \"flex-grow: 1; font-weight: 400; letter-spacing: 0.1px; font-size: 11px;\";\n\n  // Add elements to statusContainer\n  statusContainer.appendChild(indicator);\n  statusContainer.appendChild(statusText);\n\n  // Add elements to banner - reconnect button first, then status container\n  banner.appendChild(reconnectButton);\n  banner.appendChild(statusContainer);\n\n  // Add banner to the beginning of the document body\n  // This ensures it's the very first element\n  document.body.prepend(banner);\n\n  // Set initial state\n  updateConnectionBanner(false, null);\n}\n\n// Update the connection banner with current status\nfunction updateConnectionBanner(connected, serverInfo) {\n  const indicator = document.getElementById(\"banner-status-indicator\");\n  const statusText = document.getElementById(\"banner-status-text\");\n  const banner = document.getElementById(\"connection-banner\");\n  const reconnectButton = document.getElementById(\"banner-reconnect-btn\");\n\n  if (!indicator || !statusText || !banner || !reconnectButton) return;\n\n  if (connected && serverInfo) {\n    // Connected state with server info\n    indicator.style.backgroundColor = \"#4CAF50\"; // Green indicator\n    statusText.style.color = \"#ffffff\"; // White text for contrast on black\n    statusText.textContent = `Connected to ${serverInfo.name} v${serverInfo.version} at ${settings.serverHost}:${settings.serverPort}`;\n\n    // Hide reconnect button when connected\n    reconnectButton.style.display = \"none\";\n  } else if (connected) {\n    // Connected without server info\n    indicator.style.backgroundColor = \"#4CAF50\"; // Green indicator\n    statusText.style.color = \"#ffffff\"; // White text for contrast on black\n    statusText.textContent = `Connected to server at ${settings.serverHost}:${settings.serverPort}`;\n\n    // Hide reconnect button when connected\n    reconnectButton.style.display = \"none\";\n  } else {\n    // Disconnected state\n    indicator.style.backgroundColor = \"#F44336\"; // Red indicator\n    statusText.style.color = \"#ffffff\"; // White text for contrast on black\n\n    // Only show \"searching\" message if discovery is in progress\n    if (isDiscoveryInProgress) {\n      statusText.textContent = \"Not connected to server. Searching...\";\n      // Hide reconnect button while actively searching\n      reconnectButton.style.display = \"none\";\n    } else {\n      statusText.textContent = \"Not connected to server.\";\n      // Show reconnect button above status message when disconnected and not searching\n      reconnectButton.style.display = \"block\";\n      reconnectButton.textContent = \"Reconnect\";\n    }\n  }\n}\n\n// Initialize UI elements\nconst logLimitInput = document.getElementById(\"log-limit\");\nconst queryLimitInput = document.getElementById(\"query-limit\");\nconst stringSizeLimitInput = document.getElementById(\"string-size-limit\");\nconst showRequestHeadersCheckbox = document.getElementById(\n  \"show-request-headers\"\n);\nconst showResponseHeadersCheckbox = document.getElementById(\n  \"show-response-headers\"\n);\nconst maxLogSizeInput = document.getElementById(\"max-log-size\");\nconst screenshotPathInput = document.getElementById(\"screenshot-path\");\nconst captureScreenshotButton = document.getElementById(\"capture-screenshot\");\n\n// Server connection UI elements\nconst serverHostInput = document.getElementById(\"server-host\");\nconst serverPortInput = document.getElementById(\"server-port\");\nconst discoverServerButton = document.getElementById(\"discover-server\");\nconst testConnectionButton = document.getElementById(\"test-connection\");\nconst connectionStatusDiv = document.getElementById(\"connection-status\");\nconst statusIcon = document.getElementById(\"status-icon\");\nconst statusText = document.getElementById(\"status-text\");\n\n// Initialize collapsible advanced settings\nconst advancedSettingsHeader = document.getElementById(\n  \"advanced-settings-header\"\n);\nconst advancedSettingsContent = document.getElementById(\n  \"advanced-settings-content\"\n);\nconst chevronIcon = advancedSettingsHeader.querySelector(\".chevron\");\n\nadvancedSettingsHeader.addEventListener(\"click\", () => {\n  advancedSettingsContent.classList.toggle(\"visible\");\n  chevronIcon.classList.toggle(\"open\");\n});\n\n// Get all inputs by ID\nconst allowAutoPasteCheckbox = document.getElementById(\"allow-auto-paste\");\n\n// Update UI from settings\nfunction updateUIFromSettings() {\n  logLimitInput.value = settings.logLimit;\n  queryLimitInput.value = settings.queryLimit;\n  stringSizeLimitInput.value = settings.stringSizeLimit;\n  showRequestHeadersCheckbox.checked = settings.showRequestHeaders;\n  showResponseHeadersCheckbox.checked = settings.showResponseHeaders;\n  maxLogSizeInput.value = settings.maxLogSize;\n  screenshotPathInput.value = settings.screenshotPath;\n  serverHostInput.value = settings.serverHost;\n  serverPortInput.value = settings.serverPort;\n  allowAutoPasteCheckbox.checked = settings.allowAutoPaste;\n}\n\n// Save settings\nfunction saveSettings() {\n  chrome.storage.local.set({ browserConnectorSettings: settings });\n  // Notify devtools.js about settings change\n  chrome.runtime.sendMessage({\n    type: \"SETTINGS_UPDATED\",\n    settings,\n  });\n}\n\n// Add event listeners for all inputs\nlogLimitInput.addEventListener(\"change\", (e) => {\n  settings.logLimit = parseInt(e.target.value, 10);\n  saveSettings();\n});\n\nqueryLimitInput.addEventListener(\"change\", (e) => {\n  settings.queryLimit = parseInt(e.target.value, 10);\n  saveSettings();\n});\n\nstringSizeLimitInput.addEventListener(\"change\", (e) => {\n  settings.stringSizeLimit = parseInt(e.target.value, 10);\n  saveSettings();\n});\n\nshowRequestHeadersCheckbox.addEventListener(\"change\", (e) => {\n  settings.showRequestHeaders = e.target.checked;\n  saveSettings();\n});\n\nshowResponseHeadersCheckbox.addEventListener(\"change\", (e) => {\n  settings.showResponseHeaders = e.target.checked;\n  saveSettings();\n});\n\nmaxLogSizeInput.addEventListener(\"change\", (e) => {\n  settings.maxLogSize = parseInt(e.target.value, 10);\n  saveSettings();\n});\n\nscreenshotPathInput.addEventListener(\"change\", (e) => {\n  settings.screenshotPath = e.target.value;\n  saveSettings();\n});\n\n// Add event listeners for server settings\nserverHostInput.addEventListener(\"change\", (e) => {\n  settings.serverHost = e.target.value;\n  saveSettings();\n  // Automatically test connection when host is changed\n  testConnection(settings.serverHost, settings.serverPort);\n});\n\nserverPortInput.addEventListener(\"change\", (e) => {\n  settings.serverPort = parseInt(e.target.value, 10);\n  saveSettings();\n  // Automatically test connection when port is changed\n  testConnection(settings.serverHost, settings.serverPort);\n});\n\n// Add event listener for auto-paste checkbox\nallowAutoPasteCheckbox.addEventListener(\"change\", (e) => {\n  settings.allowAutoPaste = e.target.checked;\n  saveSettings();\n});\n\n// Function to cancel any ongoing discovery operations\nfunction cancelOngoingDiscovery() {\n  if (isDiscoveryInProgress) {\n    console.log(\"Cancelling ongoing discovery operation\");\n\n    // Abort any fetch requests in progress\n    if (discoveryController) {\n      try {\n        discoveryController.abort();\n      } catch (error) {\n        console.error(\"Error aborting discovery controller:\", error);\n      }\n      discoveryController = null;\n    }\n\n    // Reset the discovery status\n    isDiscoveryInProgress = false;\n\n    // Update UI to indicate the operation was cancelled\n    if (\n      statusText &&\n      connectionStatusDiv &&\n      connectionStatusDiv.style.display === \"block\"\n    ) {\n      statusText.textContent = \"Server discovery operation cancelled\";\n    }\n\n    // Clear any pending network timeouts that might be part of the discovery process\n    clearTimeout(reconnectAttemptTimeout);\n    reconnectAttemptTimeout = null;\n\n    console.log(\"Discovery operation cancelled successfully\");\n  }\n}\n\n// Test server connection\ntestConnectionButton.addEventListener(\"click\", async () => {\n  // Cancel any ongoing discovery operations before testing\n  cancelOngoingDiscovery();\n  await testConnection(settings.serverHost, settings.serverPort);\n});\n\n// Function to test server connection\nasync function testConnection(host, port) {\n  // Cancel any ongoing discovery operations\n  cancelOngoingDiscovery();\n\n  connectionStatusDiv.style.display = \"block\";\n  statusIcon.className = \"status-indicator\";\n  statusText.textContent = \"Testing connection...\";\n\n  try {\n    // Use the identity endpoint instead of .port for more reliable validation\n    const response = await fetch(`http://${host}:${port}/.identity`, {\n      signal: AbortSignal.timeout(5000), // 5 second timeout\n    });\n\n    if (response.ok) {\n      const identity = await response.json();\n\n      // Verify this is actually our server by checking the signature\n      if (identity.signature !== \"mcp-browser-connector-24x7\") {\n        statusIcon.className = \"status-indicator status-disconnected\";\n        statusText.textContent = `Connection failed: Found a server at ${host}:${port} but it's not the Browser Tools server`;\n        serverConnected = false;\n        updateConnectionBanner(false, null);\n        scheduleReconnectAttempt();\n        return false;\n      }\n\n      statusIcon.className = \"status-indicator status-connected\";\n      statusText.textContent = `Connected successfully to ${identity.name} v${identity.version} at ${host}:${port}`;\n      serverConnected = true;\n      updateConnectionBanner(true, identity);\n\n      // Clear any scheduled reconnect attempts\n      if (reconnectAttemptTimeout) {\n        clearTimeout(reconnectAttemptTimeout);\n        reconnectAttemptTimeout = null;\n      }\n\n      // Update settings if different port was discovered\n      if (parseInt(identity.port, 10) !== port) {\n        console.log(`Detected different port: ${identity.port}`);\n        settings.serverPort = parseInt(identity.port, 10);\n        serverPortInput.value = settings.serverPort;\n        saveSettings();\n      }\n\n      return true;\n    } else {\n      statusIcon.className = \"status-indicator status-disconnected\";\n      statusText.textContent = `Connection failed: Server returned ${response.status}`;\n      serverConnected = false;\n\n      // Make sure isDiscoveryInProgress is false so the reconnect button will show\n      isDiscoveryInProgress = false;\n\n      // Now update the connection banner to show the reconnect button\n      updateConnectionBanner(false, null);\n      scheduleReconnectAttempt();\n      return false;\n    }\n  } catch (error) {\n    statusIcon.className = \"status-indicator status-disconnected\";\n    statusText.textContent = `Connection failed: ${error.message}`;\n    serverConnected = false;\n\n    // Make sure isDiscoveryInProgress is false so the reconnect button will show\n    isDiscoveryInProgress = false;\n\n    // Now update the connection banner to show the reconnect button\n    updateConnectionBanner(false, null);\n    scheduleReconnectAttempt();\n    return false;\n  }\n}\n\n// Schedule a reconnect attempt if server isn't found\nfunction scheduleReconnectAttempt() {\n  // Clear any existing reconnect timeout\n  if (reconnectAttemptTimeout) {\n    clearTimeout(reconnectAttemptTimeout);\n  }\n\n  // Schedule a reconnect attempt in 30 seconds\n  reconnectAttemptTimeout = setTimeout(() => {\n    console.log(\"Attempting to reconnect to server...\");\n    // Only show minimal UI during auto-reconnect\n    discoverServer(true);\n  }, 30000); // 30 seconds\n}\n\n// Helper function to try connecting to a server\nasync function tryServerConnection(host, port) {\n  try {\n    // Check if the discovery process was cancelled\n    if (!isDiscoveryInProgress) {\n      return false;\n    }\n\n    // Create a local timeout that won't abort the entire discovery process\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => {\n      controller.abort();\n    }, 500); // 500ms timeout for each connection attempt\n\n    try {\n      // Use identity endpoint for validation\n      const response = await fetch(`http://${host}:${port}/.identity`, {\n        // Use a local controller for this specific request timeout\n        // but also respect the global discovery cancellation\n        signal: discoveryController\n          ? AbortSignal.any([controller.signal, discoveryController.signal])\n          : controller.signal,\n      });\n\n      clearTimeout(timeoutId);\n\n      // Check again if discovery was cancelled during the fetch\n      if (!isDiscoveryInProgress) {\n        return false;\n      }\n\n      if (response.ok) {\n        const identity = await response.json();\n\n        // Verify this is actually our server by checking the signature\n        if (identity.signature !== \"mcp-browser-connector-24x7\") {\n          console.log(\n            `Found a server at ${host}:${port} but it's not the Browser Tools server`\n          );\n          return false;\n        }\n\n        console.log(`Successfully found server at ${host}:${port}`);\n\n        // Update settings with discovered server\n        settings.serverHost = host;\n        settings.serverPort = parseInt(identity.port, 10);\n        serverHostInput.value = settings.serverHost;\n        serverPortInput.value = settings.serverPort;\n        saveSettings();\n\n        statusIcon.className = \"status-indicator status-connected\";\n        statusText.textContent = `Discovered ${identity.name} v${identity.version} at ${host}:${identity.port}`;\n\n        // Update connection banner with server info\n        updateConnectionBanner(true, identity);\n\n        // Update connection status\n        serverConnected = true;\n\n        // Clear any scheduled reconnect attempts\n        if (reconnectAttemptTimeout) {\n          clearTimeout(reconnectAttemptTimeout);\n          reconnectAttemptTimeout = null;\n        }\n\n        // End the discovery process\n        isDiscoveryInProgress = false;\n\n        // Successfully found server\n        return true;\n      }\n\n      return false;\n    } finally {\n      clearTimeout(timeoutId);\n    }\n  } catch (error) {\n    // Ignore connection errors during discovery\n    // But check if it was an abort (cancellation)\n    if (error.name === \"AbortError\") {\n      // Check if this was due to the global discovery cancellation\n      if (discoveryController && discoveryController.signal.aborted) {\n        console.log(\"Connection attempt aborted by global cancellation\");\n        return \"aborted\";\n      }\n      // Otherwise it was just a timeout for this specific connection attempt\n      return false;\n    }\n    console.log(`Connection error for ${host}:${port}: ${error.message}`);\n    return false;\n  }\n}\n\n// Server discovery function (extracted to be reusable)\nasync function discoverServer(quietMode = false) {\n  // Cancel any ongoing discovery operations before starting a new one\n  cancelOngoingDiscovery();\n\n  // Create a new AbortController for this discovery process\n  discoveryController = new AbortController();\n  isDiscoveryInProgress = true;\n\n  // In quiet mode, we don't show the connection status until we either succeed or fail completely\n  if (!quietMode) {\n    connectionStatusDiv.style.display = \"block\";\n    statusIcon.className = \"status-indicator\";\n    statusText.textContent = \"Discovering server...\";\n  }\n\n  // Always update the connection banner\n  updateConnectionBanner(false, null);\n\n  try {\n    console.log(\"Starting server discovery process\");\n\n    // Add an early cancellation listener that will respond to page navigation/refresh\n    discoveryController.signal.addEventListener(\"abort\", () => {\n      console.log(\"Discovery aborted via AbortController signal\");\n      isDiscoveryInProgress = false;\n    });\n\n    // Common IPs to try (in order of likelihood)\n    const hosts = [\"localhost\", \"127.0.0.1\"];\n\n    // Add the current configured host if it's not already in the list\n    if (\n      !hosts.includes(settings.serverHost) &&\n      settings.serverHost !== \"0.0.0.0\"\n    ) {\n      hosts.unshift(settings.serverHost); // Put at the beginning for priority\n    }\n\n    // Add common local network IPs\n    const commonLocalIps = [\"192.168.0.\", \"192.168.1.\", \"10.0.0.\", \"10.0.1.\"];\n    for (const prefix of commonLocalIps) {\n      for (let i = 1; i <= 5; i++) {\n        // Reduced from 10 to 5 for efficiency\n        hosts.push(`${prefix}${i}`);\n      }\n    }\n\n    // Build port list in a smart order:\n    // 1. Start with current configured port\n    // 2. Add default port (3025)\n    // 3. Add sequential ports around the default (for fallback detection)\n    const ports = [];\n\n    // Current configured port gets highest priority\n    const configuredPort = parseInt(settings.serverPort, 10);\n    ports.push(configuredPort);\n\n    // Add default port if it's not the same as configured\n    if (configuredPort !== 3025) {\n      ports.push(3025);\n    }\n\n    // Add sequential fallback ports (from default up to default+10)\n    for (let p = 3026; p <= 3035; p++) {\n      if (p !== configuredPort) {\n        // Avoid duplicates\n        ports.push(p);\n      }\n    }\n\n    // Remove duplicates\n    const uniquePorts = [...new Set(ports)];\n    console.log(\"Will check ports:\", uniquePorts);\n\n    // Create a progress indicator\n    let progress = 0;\n    let totalChecked = 0;\n\n    // Phase 1: Try the most likely combinations first (current host:port and localhost variants)\n    console.log(\"Starting Phase 1: Quick check of high-priority hosts/ports\");\n    const priorityHosts = hosts.slice(0, 2); // First two hosts are highest priority\n    for (const host of priorityHosts) {\n      // Check if discovery was cancelled\n      if (!isDiscoveryInProgress) {\n        console.log(\"Discovery process was cancelled during Phase 1\");\n        return false;\n      }\n\n      // Try configured port first\n      totalChecked++;\n      if (!quietMode) {\n        statusText.textContent = `Checking ${host}:${uniquePorts[0]}...`;\n      }\n      console.log(`Checking ${host}:${uniquePorts[0]}...`);\n      const result = await tryServerConnection(host, uniquePorts[0]);\n\n      // Check for cancellation or success\n      if (result === \"aborted\" || !isDiscoveryInProgress) {\n        console.log(\"Discovery process was cancelled\");\n        return false;\n      } else if (result === true) {\n        console.log(\"Server found in priority check\");\n        if (quietMode) {\n          // In quiet mode, only show the connection banner but hide the status box\n          connectionStatusDiv.style.display = \"none\";\n        }\n        return true; // Successfully found server\n      }\n\n      // Then try default port if different\n      if (uniquePorts.length > 1) {\n        // Check if discovery was cancelled\n        if (!isDiscoveryInProgress) {\n          console.log(\"Discovery process was cancelled\");\n          return false;\n        }\n\n        totalChecked++;\n        if (!quietMode) {\n          statusText.textContent = `Checking ${host}:${uniquePorts[1]}...`;\n        }\n        console.log(`Checking ${host}:${uniquePorts[1]}...`);\n        const result = await tryServerConnection(host, uniquePorts[1]);\n\n        // Check for cancellation or success\n        if (result === \"aborted\" || !isDiscoveryInProgress) {\n          console.log(\"Discovery process was cancelled\");\n          return false;\n        } else if (result === true) {\n          console.log(\"Server found in priority check\");\n          if (quietMode) {\n            // In quiet mode, only show the connection banner but hide the status box\n            connectionStatusDiv.style.display = \"none\";\n          }\n          return true; // Successfully found server\n        }\n      }\n    }\n\n    // If we're in quiet mode and the quick checks failed, show the status now\n    // as we move into more intensive scanning\n    if (quietMode) {\n      connectionStatusDiv.style.display = \"block\";\n      statusIcon.className = \"status-indicator\";\n      statusText.textContent = \"Searching for server...\";\n    }\n\n    // Phase 2: Systematic scan of all combinations\n    const totalAttempts = hosts.length * uniquePorts.length;\n    console.log(\n      `Starting Phase 2: Full scan (${totalAttempts} total combinations)`\n    );\n    statusText.textContent = `Quick check failed. Starting full scan (${totalChecked}/${totalAttempts})...`;\n\n    // First, scan through all ports on localhost/127.0.0.1 to find fallback ports quickly\n    const localHosts = [\"localhost\", \"127.0.0.1\"];\n    for (const host of localHosts) {\n      // Skip the first two ports on localhost if we already checked them in Phase 1\n      const portsToCheck = uniquePorts.slice(\n        localHosts.includes(host) && priorityHosts.includes(host) ? 2 : 0\n      );\n\n      for (const port of portsToCheck) {\n        // Check if discovery was cancelled\n        if (!isDiscoveryInProgress) {\n          console.log(\"Discovery process was cancelled during local port scan\");\n          return false;\n        }\n\n        // Update progress\n        progress++;\n        totalChecked++;\n        statusText.textContent = `Scanning local ports... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;\n        console.log(`Checking ${host}:${port}...`);\n\n        const result = await tryServerConnection(host, port);\n\n        // Check for cancellation or success\n        if (result === \"aborted\" || !isDiscoveryInProgress) {\n          console.log(\"Discovery process was cancelled\");\n          return false;\n        } else if (result === true) {\n          console.log(`Server found at ${host}:${port}`);\n          return true; // Successfully found server\n        }\n      }\n    }\n\n    // Then scan all the remaining host/port combinations\n    for (const host of hosts) {\n      // Skip hosts we already checked\n      if (localHosts.includes(host)) {\n        continue;\n      }\n\n      for (const port of uniquePorts) {\n        // Check if discovery was cancelled\n        if (!isDiscoveryInProgress) {\n          console.log(\"Discovery process was cancelled during remote scan\");\n          return false;\n        }\n\n        // Update progress\n        progress++;\n        totalChecked++;\n        statusText.textContent = `Scanning remote hosts... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;\n        console.log(`Checking ${host}:${port}...`);\n\n        const result = await tryServerConnection(host, port);\n\n        // Check for cancellation or success\n        if (result === \"aborted\" || !isDiscoveryInProgress) {\n          console.log(\"Discovery process was cancelled\");\n          return false;\n        } else if (result === true) {\n          console.log(`Server found at ${host}:${port}`);\n          return true; // Successfully found server\n        }\n      }\n    }\n\n    console.log(\n      `Discovery process completed, checked ${totalChecked} combinations, no server found`\n    );\n    // If we get here, no server was found\n    statusIcon.className = \"status-indicator status-disconnected\";\n    statusText.textContent =\n      \"No server found. Please check server is running and try again.\";\n\n    serverConnected = false;\n\n    // End the discovery process first before updating the banner\n    isDiscoveryInProgress = false;\n\n    // Update the connection banner to show the reconnect button\n    updateConnectionBanner(false, null);\n\n    // Schedule a reconnect attempt\n    scheduleReconnectAttempt();\n\n    return false;\n  } catch (error) {\n    console.error(\"Error during server discovery:\", error);\n    statusIcon.className = \"status-indicator status-disconnected\";\n    statusText.textContent = `Error discovering server: ${error.message}`;\n\n    serverConnected = false;\n\n    // End the discovery process first before updating the banner\n    isDiscoveryInProgress = false;\n\n    // Update the connection banner to show the reconnect button\n    updateConnectionBanner(false, null);\n\n    // Schedule a reconnect attempt\n    scheduleReconnectAttempt();\n\n    return false;\n  } finally {\n    console.log(\"Discovery process finished\");\n    // Always clean up, even if there was an error\n    if (discoveryController) {\n      discoveryController = null;\n    }\n  }\n}\n\n// Bind discover server button to the extracted function\ndiscoverServerButton.addEventListener(\"click\", () => discoverServer(false));\n\n// Screenshot capture functionality\ncaptureScreenshotButton.addEventListener(\"click\", () => {\n  captureScreenshotButton.textContent = \"Capturing...\";\n\n  // Send message to background script to capture screenshot\n  chrome.runtime.sendMessage(\n    {\n      type: \"CAPTURE_SCREENSHOT\",\n      tabId: chrome.devtools.inspectedWindow.tabId,\n      screenshotPath: settings.screenshotPath,\n    },\n    (response) => {\n      console.log(\"Screenshot capture response:\", response);\n      if (!response) {\n        captureScreenshotButton.textContent = \"Failed to capture!\";\n        console.error(\"Screenshot capture failed: No response received\");\n      } else if (!response.success) {\n        captureScreenshotButton.textContent = \"Failed to capture!\";\n        console.error(\"Screenshot capture failed:\", response.error);\n      } else {\n        captureScreenshotButton.textContent = `Captured: ${response.title}`;\n        console.log(\"Screenshot captured successfully:\", response.path);\n      }\n      setTimeout(() => {\n        captureScreenshotButton.textContent = \"Capture Screenshot\";\n      }, 2000);\n    }\n  );\n});\n\n// Add wipe logs functionality\nconst wipeLogsButton = document.getElementById(\"wipe-logs\");\nwipeLogsButton.addEventListener(\"click\", () => {\n  const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/wipelogs`;\n  console.log(`Sending wipe request to ${serverUrl}`);\n\n  fetch(serverUrl, {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/json\" },\n  })\n    .then((response) => response.json())\n    .then((result) => {\n      console.log(\"Logs wiped successfully:\", result.message);\n      wipeLogsButton.textContent = \"Logs Wiped!\";\n      setTimeout(() => {\n        wipeLogsButton.textContent = \"Wipe All Logs\";\n      }, 2000);\n    })\n    .catch((error) => {\n      console.error(\"Failed to wipe logs:\", error);\n      wipeLogsButton.textContent = \"Failed to Wipe Logs\";\n      setTimeout(() => {\n        wipeLogsButton.textContent = \"Wipe All Logs\";\n      }, 2000);\n    });\n});\n"
  },
  {
    "path": "docs/mcp-docs.md",
    "content": "## Resources\n\nExpose data and content from your servers to LLMs\n\nResources 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.\n\nResources 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:\n\nClaude Desktop currently requires users to explicitly select resources before they can be used\nOther clients might automatically select resources based on heuristics\nSome implementations may even allow the AI model itself to determine which resources to use\nServer 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.\n\n​\nOverview\nResources represent any kind of data that an MCP server wants to make available to clients. This can include:\n\nFile contents\nDatabase records\nAPI responses\nLive system data\nScreenshots and images\nLog files\nAnd more\nEach resource is identified by a unique URI and can contain either text or binary data.\n\n​\nResource URIs\nResources are identified using URIs that follow this format:\n\n[protocol]://[host]/[path]\nFor example:\n\nfile:///home/user/documents/report.pdf\npostgres://database/customers/schema\nscreen://localhost/display1\nThe protocol and path structure is defined by the MCP server implementation. Servers can define their own custom URI schemes.\n\n​\nResource types\nResources can contain two types of content:\n\n​\nText resources\nText resources contain UTF-8 encoded text data. These are suitable for:\n\nSource code\nConfiguration files\nLog files\nJSON/XML data\nPlain text\n​\nBinary resources\nBinary resources contain raw binary data encoded in base64. These are suitable for:\n\nImages\nPDFs\nAudio files\nVideo files\nOther non-text formats\n​\nResource discovery\nClients can discover available resources through two main methods:\n\n​\nDirect resources\nServers expose a list of concrete resources via the resources/list endpoint. Each resource includes:\n\n{\nuri: string; // Unique identifier for the resource\nname: string; // Human-readable name\ndescription?: string; // Optional description\nmimeType?: string; // Optional MIME type\n}\n​\nResource templates\nFor dynamic resources, servers can expose URI templates that clients can use to construct valid resource URIs:\n\n{\nuriTemplate: string; // URI template following RFC 6570\nname: string; // Human-readable name for this type\ndescription?: string; // Optional description\nmimeType?: string; // Optional MIME type for all matching resources\n}\n​\nReading resources\nTo read a resource, clients make a resources/read request with the resource URI.\n\nThe server responds with a list of resource contents:\n\n{\ncontents: [\n{\nuri: string; // The URI of the resource\nmimeType?: string; // Optional MIME type\n\n      // One of:\n      text?: string;      // For text resources\n      blob?: string;      // For binary resources (base64 encoded)\n    }\n\n]\n}\nServers 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.\n\n​\nResource updates\nMCP supports real-time updates for resources through two mechanisms:\n\n​\nList changes\nServers can notify clients when their list of available resources changes via the notifications/resources/list_changed notification.\n\n​\nContent changes\nClients can subscribe to updates for specific resources:\n\nClient sends resources/subscribe with resource URI\nServer sends notifications/resources/updated when the resource changes\nClient can fetch latest content with resources/read\nClient can unsubscribe with resources/unsubscribe\n​\nExample implementation\nHere’s a simple example of implementing resource support in an MCP server:\n\n## Prompts\n\nCreate reusable prompt templates and workflows\n\nPrompts 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.\n\nPrompts 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.\n\n​\nOverview\nPrompts in MCP are predefined templates that can:\n\nAccept dynamic arguments\nInclude context from resources\nChain multiple interactions\nGuide specific workflows\nSurface as UI elements (like slash commands)\n​\nPrompt structure\nEach prompt is defined with:\n\n{\nname: string; // Unique identifier for the prompt\ndescription?: string; // Human-readable description\narguments?: [ // Optional list of arguments\n{\nname: string; // Argument identifier\ndescription?: string; // Argument description\nrequired?: boolean; // Whether argument is required\n}\n]\n}\n​\nDiscovering prompts\nClients can discover available prompts through the prompts/list endpoint:\n\n// Request\n{\nmethod: \"prompts/list\"\n}\n\n// Response\n{\nprompts: [\n{\nname: \"analyze-code\",\ndescription: \"Analyze code for potential improvements\",\narguments: [\n{\nname: \"language\",\ndescription: \"Programming language\",\nrequired: true\n}\n]\n}\n]\n}\n​\nUsing prompts\nTo use a prompt, clients make a prompts/get request:\n\n// Request\n{\nmethod: \"prompts/get\",\nparams: {\nname: \"analyze-code\",\narguments: {\nlanguage: \"python\"\n}\n}\n}\n\n// Response\n{\ndescription: \"Analyze Python code for potential improvements\",\nmessages: [\n{\nrole: \"user\",\ncontent: {\ntype: \"text\",\ntext: \"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`\"\n}\n}\n]\n}\n​\nDynamic prompts\nPrompts can be dynamic and include:\n\n​\nEmbedded resource context\n\n{\n\"name\": \"analyze-project\",\n\"description\": \"Analyze project logs and code\",\n\"arguments\": [\n{\n\"name\": \"timeframe\",\n\"description\": \"Time period to analyze logs\",\n\"required\": true\n},\n{\n\"name\": \"fileUri\",\n\"description\": \"URI of code file to review\",\n\"required\": true\n}\n]\n}\nWhen handling the prompts/get request:\n\n{\n\"messages\": [\n{\n\"role\": \"user\",\n\"content\": {\n\"type\": \"text\",\n\"text\": \"Analyze these system logs and the code file for any issues:\"\n}\n},\n{\n\"role\": \"user\",\n\"content\": {\n\"type\": \"resource\",\n\"resource\": {\n\"uri\": \"logs://recent?timeframe=1h\",\n\"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\",\n\"mimeType\": \"text/plain\"\n}\n}\n},\n{\n\"role\": \"user\",\n\"content\": {\n\"type\": \"resource\",\n\"resource\": {\n\"uri\": \"file:///path/to/code.py\",\n\"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\",\n\"mimeType\": \"text/x-python\"\n}\n}\n}\n]\n}\n​\nMulti-step workflows\n\nconst debugWorkflow = {\nname: \"debug-error\",\nasync getMessages(error: string) {\nreturn [\n{\nrole: \"user\",\ncontent: {\ntype: \"text\",\ntext: `Here's an error I'm seeing: ${error}`\n}\n},\n{\nrole: \"assistant\",\ncontent: {\ntype: \"text\",\ntext: \"I'll help analyze this error. What have you tried so far?\"\n}\n},\n{\nrole: \"user\",\ncontent: {\ntype: \"text\",\ntext: \"I've tried restarting the service, but the error persists.\"\n}\n}\n];\n}\n};\n​\nExample implementation\nHere’s a complete example of implementing prompts in an MCP server:\n\nTypeScript\nPython\n\nimport { Server } from \"@modelcontextprotocol/sdk/server\";\nimport {\nListPromptsRequestSchema,\nGetPromptRequestSchema\n} from \"@modelcontextprotocol/sdk/types\";\n\nconst PROMPTS = {\n\"git-commit\": {\nname: \"git-commit\",\ndescription: \"Generate a Git commit message\",\narguments: [\n{\nname: \"changes\",\ndescription: \"Git diff or description of changes\",\nrequired: true\n}\n]\n},\n\"explain-code\": {\nname: \"explain-code\",\ndescription: \"Explain how code works\",\narguments: [\n{\nname: \"code\",\ndescription: \"Code to explain\",\nrequired: true\n},\n{\nname: \"language\",\ndescription: \"Programming language\",\nrequired: false\n}\n]\n}\n};\n\nconst server = new Server({\nname: \"example-prompts-server\",\nversion: \"1.0.0\"\n}, {\ncapabilities: {\nprompts: {}\n}\n});\n\n// List available prompts\nserver.setRequestHandler(ListPromptsRequestSchema, async () => {\nreturn {\nprompts: Object.values(PROMPTS)\n};\n});\n\n// Get specific prompt\nserver.setRequestHandler(GetPromptRequestSchema, async (request) => {\nconst prompt = PROMPTS[request.params.name];\nif (!prompt) {\nthrow new Error(`Prompt not found: ${request.params.name}`);\n}\n\nif (request.params.name === \"git-commit\") {\nreturn {\nmessages: [\n{\nrole: \"user\",\ncontent: {\ntype: \"text\",\ntext: `Generate a concise but descriptive commit message for these changes:\\n\\n${request.params.arguments?.changes}`\n}\n}\n]\n};\n}\n\nif (request.params.name === \"explain-code\") {\nconst language = request.params.arguments?.language || \"Unknown\";\nreturn {\nmessages: [\n{\nrole: \"user\",\ncontent: {\ntype: \"text\",\ntext: `Explain how this ${language} code works:\\n\\n${request.params.arguments?.code}`\n}\n}\n]\n};\n}\n\nthrow new Error(\"Prompt implementation not found\");\n});\n​\nBest practices\nWhen implementing prompts:\n\nUse clear, descriptive prompt names\nProvide detailed descriptions for prompts and arguments\nValidate all required arguments\nHandle missing arguments gracefully\nConsider versioning for prompt templates\nCache dynamic content when appropriate\nImplement error handling\nDocument expected argument formats\nConsider prompt composability\nTest prompts with various inputs\n​\nUI integration\nPrompts can be surfaced in client UIs as:\n\nSlash commands\nQuick actions\nContext menu items\nCommand palette entries\nGuided workflows\nInteractive forms\n​\nUpdates and changes\nServers can notify clients about prompt changes:\n\nServer capability: prompts.listChanged\nNotification: notifications/prompts/list_changed\nClient re-fetches prompt list\n​\nSecurity considerations\nWhen implementing prompts:\n\nValidate all arguments\nSanitize user input\nConsider rate limiting\nImplement access controls\nAudit prompt usage\nHandle sensitive data appropriately\nValidate generated content\nImplement timeouts\nConsider prompt injection risks\nDocument security requirements\n\n## Tools\n\nTools\nEnable LLMs to perform actions through your server\n\nTools 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.\n\nTools 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).\n\n​\nOverview\nTools 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:\n\nDiscovery: Clients can list available tools through the tools/list endpoint\nInvocation: Tools are called using the tools/call endpoint, where servers perform the requested operation and return results\nFlexibility: Tools can range from simple calculations to complex API interactions\nLike 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.\n\n​\nTool definition structure\nEach tool is defined with the following structure:\n\n{\nname: string; // Unique identifier for the tool\ndescription?: string; // Human-readable description\ninputSchema: { // JSON Schema for the tool's parameters\ntype: \"object\",\nproperties: { ... } // Tool-specific parameters\n}\n}\n​\nImplementing tools\nHere’s an example of implementing a basic tool in an MCP server:\n\nTypeScript\nPython\n\nconst server = new Server({\nname: \"example-server\",\nversion: \"1.0.0\"\n}, {\ncapabilities: {\ntools: {}\n}\n});\n\n// Define available tools\nserver.setRequestHandler(ListToolsRequestSchema, async () => {\nreturn {\ntools: [{\nname: \"calculate_sum\",\ndescription: \"Add two numbers together\",\ninputSchema: {\ntype: \"object\",\nproperties: {\na: { type: \"number\" },\nb: { type: \"number\" }\n},\nrequired: [\"a\", \"b\"]\n}\n}]\n};\n});\n\n// Handle tool execution\nserver.setRequestHandler(CallToolRequestSchema, async (request) => {\nif (request.params.name === \"calculate_sum\") {\nconst { a, b } = request.params.arguments;\nreturn {\ncontent: [\n{\ntype: \"text\",\ntext: String(a + b)\n}\n]\n};\n}\nthrow new Error(\"Tool not found\");\n});\n​\nExample tool patterns\nHere are some examples of types of tools that a server could provide:\n\n​\nSystem operations\nTools that interact with the local system:\n\n{\nname: \"execute_command\",\ndescription: \"Run a shell command\",\ninputSchema: {\ntype: \"object\",\nproperties: {\ncommand: { type: \"string\" },\nargs: { type: \"array\", items: { type: \"string\" } }\n}\n}\n}\n​\nAPI integrations\nTools that wrap external APIs:\n\n{\nname: \"github_create_issue\",\ndescription: \"Create a GitHub issue\",\ninputSchema: {\ntype: \"object\",\nproperties: {\ntitle: { type: \"string\" },\nbody: { type: \"string\" },\nlabels: { type: \"array\", items: { type: \"string\" } }\n}\n}\n}\n​\nData processing\nTools that transform or analyze data:\n\n{\nname: \"analyze_csv\",\ndescription: \"Analyze a CSV file\",\ninputSchema: {\ntype: \"object\",\nproperties: {\nfilepath: { type: \"string\" },\noperations: {\ntype: \"array\",\nitems: {\nenum: [\"sum\", \"average\", \"count\"]\n}\n}\n}\n}\n}\n​\nBest practices\nWhen implementing tools:\n\nProvide clear, descriptive names and descriptions\nUse detailed JSON Schema definitions for parameters\nInclude examples in tool descriptions to demonstrate how the model should use them\nImplement proper error handling and validation\nUse progress reporting for long operations\nKeep tool operations focused and atomic\nDocument expected return value structures\nImplement proper timeouts\nConsider rate limiting for resource-intensive operations\nLog tool usage for debugging and monitoring\n​\nSecurity considerations\nWhen exposing tools:\n\n​\nInput validation\nValidate all parameters against the schema\nSanitize file paths and system commands\nValidate URLs and external identifiers\nCheck parameter sizes and ranges\nPrevent command injection\n​\nAccess control\nImplement authentication where needed\nUse appropriate authorization checks\nAudit tool usage\nRate limit requests\nMonitor for abuse\n​\nError handling\nDon’t expose internal errors to clients\nLog security-relevant errors\nHandle timeouts appropriately\nClean up resources after errors\nValidate return values\n​\nTool discovery and updates\nMCP supports dynamic tool discovery:\n\nClients can list available tools at any time\nServers can notify clients when tools change using notifications/tools/list_changed\nTools can be added or removed during runtime\nTool definitions can be updated (though this should be done carefully)\n​\nError handling\nTool 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:\n\nSet isError to true in the result\nInclude error details in the content array\nHere’s an example of proper error handling for tools:\n\nTypeScript\nPython\n\ntry {\n// Tool operation\nconst result = performOperation();\nreturn {\ncontent: [\n{\ntype: \"text\",\ntext: `Operation successful: ${result}`\n}\n]\n};\n} catch (error) {\nreturn {\nisError: true,\ncontent: [\n{\ntype: \"text\",\ntext: `Error: ${error.message}`\n}\n]\n};\n}\nThis approach allows the LLM to see that an error occurred and potentially take corrective action or request human intervention.\n\n​\nTesting tools\nA comprehensive testing strategy for MCP tools should cover:\n\nFunctional testing: Verify tools execute correctly with valid inputs and handle invalid inputs appropriately\nIntegration testing: Test tool interaction with external systems using both real and mocked dependencies\nSecurity testing: Validate authentication, authorization, input sanitization, and rate limiting\nPerformance testing: Check behavior under load, timeout handling, and resource cleanup\nError handling: Ensure tools properly report errors through the MCP protocol and clean up resources\n\n## Sampling\n\nSampling\nLet your servers request completions from LLMs\n\nSampling is a powerful MCP feature that allows servers to request LLM completions through the client, enabling sophisticated agentic behaviors while maintaining security and privacy.\n\nThis feature of MCP is not yet supported in the Claude Desktop client.\n\n​\nHow sampling works\nThe sampling flow follows these steps:\n\nServer sends a sampling/createMessage request to the client\nClient reviews the request and can modify it\nClient samples from an LLM\nClient reviews the completion\nClient returns the result to the server\nThis human-in-the-loop design ensures users maintain control over what the LLM sees and generates.\n\n​\nMessage format\nSampling requests use a standardized message format:\n\n{\nmessages: [\n{\nrole: \"user\" | \"assistant\",\ncontent: {\ntype: \"text\" | \"image\",\n\n        // For text:\n        text?: string,\n\n        // For images:\n        data?: string,             // base64 encoded\n        mimeType?: string\n      }\n    }\n\n],\nmodelPreferences?: {\nhints?: [{\nname?: string // Suggested model name/family\n}],\ncostPriority?: number, // 0-1, importance of minimizing cost\nspeedPriority?: number, // 0-1, importance of low latency\nintelligencePriority?: number // 0-1, importance of capabilities\n},\nsystemPrompt?: string,\nincludeContext?: \"none\" | \"thisServer\" | \"allServers\",\ntemperature?: number,\nmaxTokens: number,\nstopSequences?: string[],\nmetadata?: Record<string, unknown>\n}\n​\nRequest parameters\n​\nMessages\nThe messages array contains the conversation history to send to the LLM. Each message has:\n\nrole: Either “user” or “assistant”\ncontent: The message content, which can be:\nText content with a text field\nImage content with data (base64) and mimeType fields\n​\nModel preferences\nThe modelPreferences object allows servers to specify their model selection preferences:\n\nhints: Array of model name suggestions that clients can use to select an appropriate model:\n\nname: String that can match full or partial model names (e.g. “claude-3”, “sonnet”)\nClients may map hints to equivalent models from different providers\nMultiple hints are evaluated in preference order\nPriority values (0-1 normalized):\n\ncostPriority: Importance of minimizing costs\nspeedPriority: Importance of low latency response\nintelligencePriority: Importance of advanced model capabilities\nClients make the final model selection based on these preferences and their available models.\n\n​\nSystem prompt\nAn optional systemPrompt field allows servers to request a specific system prompt. The client may modify or ignore this.\n\n​\nContext inclusion\nThe includeContext parameter specifies what MCP context to include:\n\n\"none\": No additional context\n\"thisServer\": Include context from the requesting server\n\"allServers\": Include context from all connected MCP servers\nThe client controls what context is actually included.\n\n​\nSampling parameters\nFine-tune the LLM sampling with:\n\ntemperature: Controls randomness (0.0 to 1.0)\nmaxTokens: Maximum tokens to generate\nstopSequences: Array of sequences that stop generation\nmetadata: Additional provider-specific parameters\n​\nResponse format\nThe client returns a completion result:\n\n{\nmodel: string, // Name of the model used\nstopReason?: \"endTurn\" | \"stopSequence\" | \"maxTokens\" | string,\nrole: \"user\" | \"assistant\",\ncontent: {\ntype: \"text\" | \"image\",\ntext?: string,\ndata?: string,\nmimeType?: string\n}\n}\n​\nExample request\nHere’s an example of requesting sampling from a client:\n\n{\n\"method\": \"sampling/createMessage\",\n\"params\": {\n\"messages\": [\n{\n\"role\": \"user\",\n\"content\": {\n\"type\": \"text\",\n\"text\": \"What files are in the current directory?\"\n}\n}\n],\n\"systemPrompt\": \"You are a helpful file system assistant.\",\n\"includeContext\": \"thisServer\",\n\"maxTokens\": 100\n}\n}\n​\nBest practices\nWhen implementing sampling:\n\nAlways provide clear, well-structured prompts\nHandle both text and image content appropriately\nSet reasonable token limits\nInclude relevant context through includeContext\nValidate responses before using them\nHandle errors gracefully\nConsider rate limiting sampling requests\nDocument expected sampling behavior\nTest with various model parameters\nMonitor sampling costs\n​\nHuman in the loop controls\nSampling is designed with human oversight in mind:\n\n​\nFor prompts\nClients should show users the proposed prompt\nUsers should be able to modify or reject prompts\nSystem prompts can be filtered or modified\nContext inclusion is controlled by the client\n​\nFor completions\nClients should show users the completion\nUsers should be able to modify or reject completions\nClients can filter or modify completions\nUsers control which model is used\n​\nSecurity considerations\nWhen implementing sampling:\n\nValidate all message content\nSanitize sensitive information\nImplement appropriate rate limits\nMonitor sampling usage\nEncrypt data in transit\nHandle user data privacy\nAudit sampling requests\nControl cost exposure\nImplement timeouts\nHandle model errors gracefully\n​\nCommon patterns\n​\nAgentic workflows\nSampling enables agentic patterns like:\n\nReading and analyzing resources\nMaking decisions based on context\nGenerating structured data\nHandling multi-step tasks\nProviding interactive assistance\n​\nContext management\nBest practices for context:\n\nRequest minimal necessary context\nStructure context clearly\nHandle context size limits\nUpdate context as needed\nClean up stale context\n​\nError handling\nRobust error handling should:\n\nCatch sampling failures\nHandle timeout errors\nManage rate limits\nValidate responses\nProvide fallback behaviors\nLog errors appropriately\n​\nLimitations\nBe aware of these limitations:\n\nSampling depends on client capabilities\nUsers control sampling behavior\nContext size has limits\nRate limits may apply\nCosts should be considered\nModel availability varies\nResponse times vary\nNot all content types supported\n\n## Roots\n\nRoots\nUnderstanding roots in MCP\n\nRoots 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.\n\n​\nWhat are Roots?\nA 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.\n\nFor example, roots could be:\n\nfile:///home/user/projects/myapp\nhttps://api.example.com/v1\n​\nWhy Use Roots?\nRoots serve several important purposes:\n\nGuidance: They inform servers about relevant resources and locations\nClarity: Roots make it clear which resources are part of your workspace\nOrganization: Multiple roots let you work with different resources simultaneously\n​\nHow Roots Work\nWhen a client supports roots, it:\n\nDeclares the roots capability during connection\nProvides a list of suggested roots to the server\nNotifies the server when roots change (if supported)\nWhile roots are informational and not strictly enforcing, servers should:\n\nRespect the provided roots\nUse root URIs to locate and access resources\nPrioritize operations within root boundaries\n​\nCommon Use Cases\nRoots are commonly used to define:\n\nProject directories\nRepository locations\nAPI endpoints\nConfiguration locations\nResource boundaries\n​\nBest Practices\nWhen working with roots:\n\nOnly suggest necessary resources\nUse clear, descriptive names for roots\nMonitor root accessibility\nHandle root changes gracefully\n​\nExample\nHere’s how a typical MCP client might expose roots:\n\n{\n\"roots\": [\n{\n\"uri\": \"file:///home/user/projects/frontend\",\n\"name\": \"Frontend Repository\"\n},\n{\n\"uri\": \"https://api.example.com/v1\",\n\"name\": \"API Endpoint\"\n}\n]\n}\nThis configuration suggests the server focus on both a local repository and an API endpoint while keeping them logically separated.\n\n## Transports\n\nTransports\nLearn about MCP’s communication mechanisms\n\nTransports 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.\n\n​\nMessage Format\nMCP 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.\n\nThere are three types of JSON-RPC messages used:\n\n​\nRequests\n\n{\njsonrpc: \"2.0\",\nid: number | string,\nmethod: string,\nparams?: object\n}\n​\nResponses\n\n{\njsonrpc: \"2.0\",\nid: number | string,\nresult?: object,\nerror?: {\ncode: number,\nmessage: string,\ndata?: unknown\n}\n}\n​\nNotifications\n\n{\njsonrpc: \"2.0\",\nmethod: string,\nparams?: object\n}\n​\nBuilt-in Transport Types\nMCP includes two standard transport implementations:\n\n​\nStandard Input/Output (stdio)\nThe stdio transport enables communication through standard input and output streams. This is particularly useful for local integrations and command-line tools.\n\nUse stdio when:\n\nBuilding command-line tools\nImplementing local integrations\nNeeding simple process communication\nWorking with shell scripts\nTypeScript (Server)\nTypeScript (Client)\nPython (Server)\nPython (Client)\n\nconst server = new Server({\nname: \"example-server\",\nversion: \"1.0.0\"\n}, {\ncapabilities: {}\n});\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n​\nServer-Sent Events (SSE)\nSSE transport enables server-to-client streaming with HTTP POST requests for client-to-server communication.\n\nUse SSE when:\n\nOnly server-to-client streaming is needed\nWorking with restricted networks\nImplementing simple updates\nTypeScript (Server)\nTypeScript (Client)\nPython (Server)\nPython (Client)\n\nimport express from \"express\";\n\nconst app = express();\n\nconst server = new Server({\nname: \"example-server\",\nversion: \"1.0.0\"\n}, {\ncapabilities: {}\n});\n\nlet transport: SSEServerTransport | null = null;\n\napp.get(\"/sse\", (req, res) => {\ntransport = new SSEServerTransport(\"/messages\", res);\nserver.connect(transport);\n});\n\napp.post(\"/messages\", (req, res) => {\nif (transport) {\ntransport.handlePostMessage(req, res);\n}\n});\n\napp.listen(3000);\n​\nCustom Transports\nMCP makes it easy to implement custom transports for specific needs. Any transport implementation just needs to conform to the Transport interface:\n\nYou can implement custom transports for:\n\nCustom network protocols\nSpecialized communication channels\nIntegration with existing systems\nPerformance optimization\nTypeScript\nPython\n\ninterface Transport {\n// Start processing messages\nstart(): Promise<void>;\n\n// Send a JSON-RPC message\nsend(message: JSONRPCMessage): Promise<void>;\n\n// Close the connection\nclose(): Promise<void>;\n\n// Callbacks\nonclose?: () => void;\nonerror?: (error: Error) => void;\nonmessage?: (message: JSONRPCMessage) => void;\n}\n​\nError Handling\nTransport implementations should handle various error scenarios:\n\nConnection errors\nMessage parsing errors\nProtocol errors\nNetwork timeouts\nResource cleanup\nExample error handling:\n\nTypeScript\nPython\n\nclass ExampleTransport implements Transport {\nasync start() {\ntry {\n// Connection logic\n} catch (error) {\nthis.onerror?.(new Error(`Failed to connect: ${error}`));\nthrow error;\n}\n}\n\nasync send(message: JSONRPCMessage) {\ntry {\n// Sending logic\n} catch (error) {\nthis.onerror?.(new Error(`Failed to send message: ${error}`));\nthrow error;\n}\n}\n}\n​\nBest Practices\nWhen implementing or using MCP transport:\n\nHandle connection lifecycle properly\nImplement proper error handling\nClean up resources on connection close\nUse appropriate timeouts\nValidate messages before sending\nLog transport events for debugging\nImplement reconnection logic when appropriate\nHandle backpressure in message queues\nMonitor connection health\nImplement proper security measures\n​\nSecurity Considerations\nWhen implementing transport:\n\n​\nAuthentication and Authorization\nImplement proper authentication mechanisms\nValidate client credentials\nUse secure token handling\nImplement authorization checks\n​\nData Security\nUse TLS for network transport\nEncrypt sensitive data\nValidate message integrity\nImplement message size limits\nSanitize input data\n​\nNetwork Security\nImplement rate limiting\nUse appropriate timeouts\nHandle denial of service scenarios\nMonitor for unusual patterns\nImplement proper firewall rules\n​\nDebugging Transport\nTips for debugging transport issues:\n\nEnable debug logging\nMonitor message flow\nCheck connection states\nValidate message formats\nTest error scenarios\nUse network analysis tools\nImplement health checks\nMonitor resource usage\nTest edge cases\nUse proper error tracking\n"
  },
  {
    "path": "docs/mcp.md",
    "content": "# MCP TypeScript SDK ![NPM Version](mdc:https:/img.shields.io/npm/v/%40modelcontextprotocol%2Fsdk) ![MIT licensed](mdc:https:/img.shields.io/npm/l/%40modelcontextprotocol%2Fsdk)\n\n## Table of Contents\n\n- [Overview](mdc:#overview)\n- [Installation](mdc:#installation)\n- [Quickstart](mdc:#quickstart)\n- [What is MCP?](mdc:#what-is-mcp)\n- [Core Concepts](mdc:#core-concepts)\n  - [Server](mdc:#server)\n  - [Resources](mdc:#resources)\n  - [Tools](mdc:#tools)\n  - [Prompts](mdc:#prompts)\n- [Running Your Server](mdc:#running-your-server)\n  - [stdio](mdc:#stdio)\n  - [HTTP with SSE](mdc:#http-with-sse)\n  - [Testing and Debugging](mdc:#testing-and-debugging)\n- [Examples](mdc:#examples)\n  - [Echo Server](mdc:#echo-server)\n  - [SQLite Explorer](mdc:#sqlite-explorer)\n- [Advanced Usage](mdc:#advanced-usage)\n  - [Low-Level Server](mdc:#low-level-server)\n  - [Writing MCP Clients](mdc:#writing-mcp-clients)\n  - [Server Capabilities](mdc:#server-capabilities)\n\n## Overview\n\nThe 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:\n\n- Build MCP clients that can connect to any MCP server\n- Create MCP servers that expose resources, prompts and tools\n- Use standard transports like stdio and SSE\n- Handle all MCP protocol messages and lifecycle events\n\n## Installation\n\n```bash\nnpm install @modelcontextprotocol/sdk\n```\n\n## Quick Start\n\nLet's create a simple MCP server that exposes a calculator tool and some data:\n\n```typescript\nimport {\n  McpServer,\n  ResourceTemplate,\n} from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\n// Create an MCP server\nconst server = new McpServer({\n  name: \"Demo\",\n  version: \"1.0.0\",\n});\n\n// Add an addition tool\nserver.tool(\"add\", { a: z.number(), b: z.number() }, async ({ a, b }) => ({\n  content: [{ type: \"text\", text: String(a + b) }],\n}));\n\n// Add a dynamic greeting resource\nserver.resource(\n  \"greeting\",\n  new ResourceTemplate(\"greeting://{name}\", { list: undefined }),\n  async (uri, { name }) => ({\n    contents: [\n      {\n        uri: uri.href,\n        text: `Hello, ${name}!`,\n      },\n    ],\n  })\n);\n\n// Start receiving messages on stdin and sending messages on stdout\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\n## What is MCP?\n\nThe [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:\n\n- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)\n- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)\n- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)\n- And more!\n\n## Core Concepts\n\n### Server\n\nThe McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:\n\n```typescript\nconst server = new McpServer({\n  name: \"My App\",\n  version: \"1.0.0\",\n});\n```\n\n### Resources\n\nResources 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:\n\n```typescript\n// Static resource\nserver.resource(\"config\", \"config://app\", async (uri) => ({\n  contents: [\n    {\n      uri: uri.href,\n      text: \"App configuration here\",\n    },\n  ],\n}));\n\n// Dynamic resource with parameters\nserver.resource(\n  \"user-profile\",\n  new ResourceTemplate(\"users://{userId}/profile\", { list: undefined }),\n  async (uri, { userId }) => ({\n    contents: [\n      {\n        uri: uri.href,\n        text: `Profile data for user ${userId}`,\n      },\n    ],\n  })\n);\n```\n\n### Tools\n\nTools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:\n\n```typescript\n// Simple tool with parameters\nserver.tool(\n  \"calculate-bmi\",\n  {\n    weightKg: z.number(),\n    heightM: z.number(),\n  },\n  async ({ weightKg, heightM }) => ({\n    content: [\n      {\n        type: \"text\",\n        text: String(weightKg / (heightM * heightM)),\n      },\n    ],\n  })\n);\n\n// Async tool with external API call\nserver.tool(\"fetch-weather\", { city: z.string() }, async ({ city }) => {\n  const response = await fetch(`https://api.weather.com/${city}`);\n  const data = await response.text();\n  return {\n    content: [{ type: \"text\", text: data }],\n  };\n});\n```\n\n### Prompts\n\nPrompts are reusable templates that help LLMs interact with your server effectively:\n\n```typescript\nserver.prompt(\"review-code\", { code: z.string() }, ({ code }) => ({\n  messages: [\n    {\n      role: \"user\",\n      content: {\n        type: \"text\",\n        text: `Please review this code:\\n\\n${code}`,\n      },\n    },\n  ],\n}));\n```\n\n## Running Your Server\n\nMCP 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:\n\n### stdio\n\nFor command-line tools and direct integrations:\n\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nconst server = new McpServer({\n  name: \"example-server\",\n  version: \"1.0.0\",\n});\n\n// ... set up server resources, tools, and prompts ...\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\n### HTTP with SSE\n\nFor 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:\n\n```typescript\nimport express from \"express\";\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { SSEServerTransport } from \"@modelcontextprotocol/sdk/server/sse.js\";\n\nconst server = new McpServer({\n  name: \"example-server\",\n  version: \"1.0.0\",\n});\n\n// ... set up server resources, tools, and prompts ...\n\nconst app = express();\n\napp.get(\"/sse\", async (req, res) => {\n  const transport = new SSEServerTransport(\"/messages\", res);\n  await server.connect(transport);\n});\n\napp.post(\"/messages\", async (req, res) => {\n  // Note: to support multiple simultaneous connections, these messages will\n  // need to be routed to a specific matching transport. (This logic isn't\n  // implemented here, for simplicity.)\n  await transport.handlePostMessage(req, res);\n});\n\napp.listen(3001);\n```\n\n### Testing and Debugging\n\nTo test your server, you can use the [MCP Inspector](mdc:https:/github.com/modelcontextprotocol/inspector). See its README for more information.\n\n## Examples\n\n### Echo Server\n\nA simple server demonstrating resources, tools, and prompts:\n\n```typescript\nimport {\n  McpServer,\n  ResourceTemplate,\n} from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { z } from \"zod\";\n\nconst server = new McpServer({\n  name: \"Echo\",\n  version: \"1.0.0\",\n});\n\nserver.resource(\n  \"echo\",\n  new ResourceTemplate(\"echo://{message}\", { list: undefined }),\n  async (uri, { message }) => ({\n    contents: [\n      {\n        uri: uri.href,\n        text: `Resource echo: ${message}`,\n      },\n    ],\n  })\n);\n\nserver.tool(\"echo\", { message: z.string() }, async ({ message }) => ({\n  content: [{ type: \"text\", text: `Tool echo: ${message}` }],\n}));\n\nserver.prompt(\"echo\", { message: z.string() }, ({ message }) => ({\n  messages: [\n    {\n      role: \"user\",\n      content: {\n        type: \"text\",\n        text: `Please process this message: ${message}`,\n      },\n    },\n  ],\n}));\n```\n\n### SQLite Explorer\n\nA more complex example showing database integration:\n\n```typescript\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport sqlite3 from \"sqlite3\";\nimport { promisify } from \"util\";\nimport { z } from \"zod\";\n\nconst server = new McpServer({\n  name: \"SQLite Explorer\",\n  version: \"1.0.0\",\n});\n\n// Helper to create DB connection\nconst getDb = () => {\n  const db = new sqlite3.Database(\"database.db\");\n  return {\n    all: promisify<string, any[]>(db.all.bind(db)),\n    close: promisify(db.close.bind(db)),\n  };\n};\n\nserver.resource(\"schema\", \"schema://main\", async (uri) => {\n  const db = getDb();\n  try {\n    const tables = await db.all(\n      \"SELECT sql FROM sqlite_master WHERE type='table'\"\n    );\n    return {\n      contents: [\n        {\n          uri: uri.href,\n          text: tables.map((t: { sql: string }) => t.sql).join(\"\\n\"),\n        },\n      ],\n    };\n  } finally {\n    await db.close();\n  }\n});\n\nserver.tool(\"query\", { sql: z.string() }, async ({ sql }) => {\n  const db = getDb();\n  try {\n    const results = await db.all(sql);\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: JSON.stringify(results, null, 2),\n        },\n      ],\n    };\n  } catch (err: unknown) {\n    const error = err as Error;\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Error: ${error.message}`,\n        },\n      ],\n      isError: true,\n    };\n  } finally {\n    await db.close();\n  }\n});\n```\n\n## Advanced Usage\n\n### Low-Level Server\n\nFor more control, you can use the low-level Server class directly:\n\n```typescript\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n  ListPromptsRequestSchema,\n  GetPromptRequestSchema,\n} from \"@modelcontextprotocol/sdk/types.js\";\n\nconst server = new Server(\n  {\n    name: \"example-server\",\n    version: \"1.0.0\",\n  },\n  {\n    capabilities: {\n      prompts: {},\n    },\n  }\n);\n\nserver.setRequestHandler(ListPromptsRequestSchema, async () => {\n  return {\n    prompts: [\n      {\n        name: \"example-prompt\",\n        description: \"An example prompt template\",\n        arguments: [\n          {\n            name: \"arg1\",\n            description: \"Example argument\",\n            required: true,\n          },\n        ],\n      },\n    ],\n  };\n});\n\nserver.setRequestHandler(GetPromptRequestSchema, async (request) => {\n  if (request.params.name !== \"example-prompt\") {\n    throw new Error(\"Unknown prompt\");\n  }\n  return {\n    description: \"Example prompt\",\n    messages: [\n      {\n        role: \"user\",\n        content: {\n          type: \"text\",\n          text: \"Example prompt text\",\n        },\n      },\n    ],\n  };\n});\n\nconst transport = new StdioServerTransport();\nawait server.connect(transport);\n```\n\n### Writing MCP Clients\n\nThe SDK provides a high-level client interface:\n\n```typescript\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\n\nconst transport = new StdioClientTransport({\n  command: \"node\",\n  args: [\"server.js\"],\n});\n\nconst client = new Client(\n  {\n    name: \"example-client\",\n    version: \"1.0.0\",\n  },\n  {\n    capabilities: {\n      prompts: {},\n      resources: {},\n      tools: {},\n    },\n  }\n);\n\nawait client.connect(transport);\n\n// List prompts\nconst prompts = await client.listPrompts();\n\n// Get a prompt\nconst prompt = await client.getPrompt(\"example-prompt\", {\n  arg1: \"value\",\n});\n\n// List resources\nconst resources = await client.listResources();\n\n// Read a resource\nconst resource = await client.readResource(\"file:///example.txt\");\n\n// Call a tool\nconst result = await client.callTool({\n  name: \"example-tool\",\n  arguments: {\n    arg1: \"value\",\n  },\n});\n```\n\n## Documentation\n\n- [Model Context Protocol documentation](mdc:https:/modelcontextprotocol.io)\n- [MCP Specification](mdc:https:/spec.modelcontextprotocol.io)\n- [Example Servers](mdc:https:/github.com/modelcontextprotocol/servers)\n\n## Contributing\n\nIssues and pull requests are welcome on GitHub at https://github.com/modelcontextprotocol/typescript-sdk.\n\n## License\n\nThis project is licensed under the MIT License—see the [LICENSE](mdc:LICENSE) file for details.\n"
  }
]