main d298068137c3 cached
26 files
337.4 KB
77.1k tokens
110 symbols
1 requests
Download .txt
Showing preview only (350K chars total). Download the full file or copy to clipboard to get everything.
Repository: AgentDeskAI/browser-tools-mcp
Branch: main
Commit: d298068137c3
Files: 26
Total size: 337.4 KB

Directory structure:
gitextract_uipce7r9/

├── .gitignore
├── LICENSE
├── README.md
├── browser-tools-mcp/
│   ├── README.md
│   ├── mcp-server.ts
│   ├── package.json
│   └── tsconfig.json
├── browser-tools-server/
│   ├── README.md
│   ├── browser-connector.ts
│   ├── lighthouse/
│   │   ├── accessibility.ts
│   │   ├── best-practices.ts
│   │   ├── index.ts
│   │   ├── performance.ts
│   │   ├── seo.ts
│   │   └── types.ts
│   ├── package.json
│   ├── puppeteer-service.ts
│   └── tsconfig.json
├── chrome-extension/
│   ├── background.js
│   ├── devtools.html
│   ├── devtools.js
│   ├── manifest.json
│   ├── panel.html
│   └── panel.js
└── docs/
    ├── mcp-docs.md
    └── mcp.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
node_modules
dist
.port
.DS_Store


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 AgentDesk LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
THIS PROJECT IS NO LONGER ACTIVE PLEASE USE A DIFFERENT SOLUTION FOR THIS. 

# BrowserTools MCP


> Make your AI tools 10x more aware and capable of interacting with your browser

This application is a powerful browser monitoring and interaction tool that enables AI-powered applications via Anthropic's Model Context Protocol (MCP) to capture and analyze browser data through a Chrome extension.

Read our [docs](https://browsertools.agentdesk.ai/) for the full installation, quickstart and contribution guides.

## Roadmap

Check out our project roadmap here: [Github Roadmap / Project Board](https://github.com/orgs/AgentDeskAI/projects/1/views/1)

## Updates

v1.2.0 is out! Here's a quick breakdown of the update:
- 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!)
- Integrated a suite of SEO, performance, accessibility, and best practice analysis tools via Lighthouse
- Implemented a NextJS specific prompt used to improve SEO for a NextJS application
- Added Debugger Mode as a tool which executes all debugging tools in a particular sequence, along with a prompt to improve reasoning
- Added Audit Mode as a tool to execute all auditing tools in a particular sequence
- Resolved Windows connectivity issues
- Improved networking between BrowserTools server, extension and MCP server with host/port auto-discovery, auto-reconnect, and graceful shutdown mechanisms
- Added ability to more easily exit out of the Browser Tools server with Ctrl+C

## Quickstart Guide

There are three components to run this MCP tool:

1. 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)
2. Install the MCP server from this command within your IDE: `npx @agentdeskai/browser-tools-mcp@latest`
3. Open a new terminal and run this command: `npx @agentdeskai/browser-tools-server@latest`

* Different IDEs have different configs but this command is generally a good starting point; please reference your IDEs docs for the proper config setup

IMPORTANT TIP - there are two servers you need to install. There's...
- browser-tools-server (local nodejs server that's a middleware for gathering logs)
and
- browser-tools-mcp (MCP server that you install into your IDE that communicates w/ the extension + browser-tools-server)

`npx @agentdeskai/browser-tools-mcp@latest` is what you put into your IDE
`npx @agentdeskai/browser-tools-server@latest` is what you run in a new terminal window

After those three steps, open up your chrome dev tools and then the BrowserToolsMCP panel.

If you're still having issues try these steps:
- Quit / close down your browser. Not just the window but all of Chrome itself. 
- Restart the local node server (browser-tools-server)
- Make sure you only have ONE instance of chrome dev tools panel open

After 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!

If you have any questions or issues, feel free to open an issue ticket! And if you have any ideas to make this better, feel free to reach out or open an issue ticket with an enhancement tag or reach out to me at [@tedx_ai on x](https://x.com/tedx_ai)

## Full Update Notes:

Coding agents like Cursor can run these audits against the current page seamlessly. By leveraging Puppeteer and the Lighthouse npm library, BrowserTools MCP can now:

- Evaluate pages for WCAG compliance
- Identify performance bottlenecks
- Flag on-page SEO issues
- Check adherence to web development best practices
- Review NextJS specific issues with SEO

...all without leaving your IDE 🎉

---

## 🔑 Key Additions

| Audit Type         | Description                                                                                                                              |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Accessibility**  | WCAG-compliant checks for color contrast, missing alt text, keyboard navigation traps, ARIA attributes, and more.                        |
| **Performance**    | Lighthouse-driven analysis of render-blocking resources, excessive DOM size, unoptimized images, and other factors affecting page speed. |
| **SEO**            | Evaluates on-page SEO factors (like metadata, headings, and link structure) and suggests improvements for better search visibility.      |
| **Best Practices** | Checks for general best practices in web development.                                                                                    |
| **NextJS Audit**   | Injects a prompt used to perform a NextJS audit.                                                                                         |
| **Audit Mode**     | Runs all auditing tools in a sequence.                                                                                                   |
| **Debugger Mode**  | Runs all debugging tools in a sequence.                                                                                                  |

---

## 🛠️ Using Audit Tools

### ✅ **Before You Start**

Ensure you have:

- An **active tab** in your browser
- The **BrowserTools extension enabled**

### ▶️ **Running Audits**

**Headless Browser Automation**:  
 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.

The headless browser instance remains active for **60 seconds** after the last audit call to efficiently handle consecutive audit requests.

**Structured Results**:  
 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.

The MCP server provides tools to run audits on the current page. Here are example queries you can use to trigger them:

#### Accessibility Audit (`runAccessibilityAudit`)

Ensures the page meets accessibility standards like WCAG.

> **Example Queries:**
>
> - "Are there any accessibility issues on this page?"
> - "Run an accessibility audit."
> - "Check if this page meets WCAG standards."

#### Performance Audit (`runPerformanceAudit`)

Identifies performance bottlenecks and loading issues.

> **Example Queries:**
>
> - "Why is this page loading so slowly?"
> - "Check the performance of this page."
> - "Run a performance audit."

#### SEO Audit (`runSEOAudit`)

Evaluates how well the page is optimized for search engines.

> **Example Queries:**
>
> - "How can I improve SEO for this page?"
> - "Run an SEO audit."
> - "Check SEO on this page."

#### Best Practices Audit (`runBestPracticesAudit`)

Checks for general best practices in web development.

> **Example Queries:**
>
> - "Run a best practices audit."
> - "Check best practices on this page."
> - "Are there any best practices issues on this page?"

#### Audit Mode (`runAuditMode`)

Runs all audits in a particular sequence. Will run a NextJS audit if the framework is detected.

> **Example Queries:**
>
> - "Run audit mode."
> - "Enter audit mode."

#### NextJS Audits (`runNextJSAudit`)

Checks for best practices and SEO improvements for NextJS applications

> **Example Queries:**
>
> - "Run a NextJS audit."
> - "Run a NextJS audit, I'm using app router."
> - "Run a NextJS audit, I'm using page router."

#### Debugger Mode (`runDebuggerMode`)

Runs all debugging tools in a particular sequence

> **Example Queries:**
>
> - "Enter debugger mode."

## Architecture

There are three core components all used to capture and analyze browser data:

1. **Chrome Extension**: A browser extension that captures screenshots, console logs, network activity and DOM elements.
2. **Node Server**: An intermediary server that facilitates communication between the Chrome extension and any instance of an MCP server.
3. **MCP Server**: A Model Context Protocol server that provides standardized tools for AI clients to interact with the browser.

```
┌─────────────┐     ┌──────────────┐     ┌───────────────┐     ┌─────────────┐
│  MCP Client │ ──► │  MCP Server  │ ──► │  Node Server  │ ──► │   Chrome    │
│  (e.g.      │ ◄── │  (Protocol   │ ◄── │ (Middleware)  │ ◄── │  Extension  │
│   Cursor)   │     │   Handler)   │     │               │     │             │
└─────────────┘     └──────────────┘     └───────────────┘     └─────────────┘
```

Model Context Protocol (MCP) is a capability supported by Anthropic AI models that
allow you to create custom tools for any compatible client. MCP clients like Claude
Desktop, Cursor, Cline or Zed can run an MCP server which "teaches" these clients
about a new tool that they can use.

These tools can call out to external APIs but in our case, **all logs are stored locally** on your machine and NEVER sent out to any third-party service or API. BrowserTools MCP runs a local instance of a NodeJS API server which communicates with the BrowserTools Chrome Extension.

All consumers of the BrowserTools MCP Server interface with the same NodeJS API and Chrome extension.

#### Chrome Extension

- Monitors XHR requests/responses and console logs
- Tracks selected DOM elements
- Sends all logs and current element to the BrowserTools Connector
- Connects to Websocket server to capture/send screenshots
- Allows user to configure token/truncation limits + screenshot folder path

#### Node Server

- Acts as middleware between the Chrome extension and MCP server
- Receives logs and currently selected element from Chrome extension
- Processes requests from MCP server to capture logs, screenshot or current element
- Sends Websocket command to the Chrome extension for capturing a screenshot
- Intelligently truncates strings and # of duplicate objects in logs to avoid token limits
- Removes cookies and sensitive headers to avoid sending to LLMs in MCP clients

#### MCP Server

- Implements the Model Context Protocol
- Provides standardized tools for AI clients
- Compatible with various MCP clients (Cursor, Cline, Zed, Claude Desktop, etc.)

## Installation

Installation steps can be found in our documentation:

- [BrowserTools MCP Docs](https://browsertools.agentdesk.ai/)

## Usage

Once installed and configured, the system allows any compatible MCP client to:

- Monitor browser console output
- Capture network traffic
- Take screenshots
- Analyze selected elements
- Wipe logs stored in our MCP server
- Run accessibility, performance, SEO, and best practices audits

## Compatibility

- Works with any MCP-compatible client
- Primarily designed for Cursor IDE integration
- Supports other AI editors and MCP clients


================================================
FILE: browser-tools-mcp/README.md
================================================
# Browser Tools MCP Server

A Model Context Protocol (MCP) server that provides AI-powered browser tools integration. This server works in conjunction with the Browser Tools Server to provide AI capabilities for browser debugging and analysis.

## Features

- MCP protocol implementation
- Browser console log access
- Network request analysis
- Screenshot capture capabilities
- Element selection and inspection
- Real-time browser state monitoring
- Accessibility, performance, SEO, and best practices audits

## Prerequisites

- Node.js 14 or higher
- Browser Tools Server running
- Chrome or Chromium browser installed (required for audit functionality)

## Installation

```bash
npx @agentdeskai/browser-tools-mcp
```

Or install globally:

```bash
npm install -g @agentdeskai/browser-tools-mcp
```

## Usage

1. First, make sure the Browser Tools Server is running:

```bash
npx @agentdeskai/browser-tools-server
```

2. Then start the MCP server:

```bash
npx @agentdeskai/browser-tools-mcp
```

3. The MCP server will connect to the Browser Tools Server and provide the following capabilities:

- Console log retrieval
- Network request monitoring
- Screenshot capture
- Element selection
- Browser state analysis
- Accessibility and performance audits

## MCP Functions

The server provides the following MCP functions:

- `mcp_getConsoleLogs` - Retrieve browser console logs
- `mcp_getConsoleErrors` - Get browser console errors
- `mcp_getNetworkErrors` - Get network error logs
- `mcp_getNetworkSuccess` - Get successful network requests
- `mcp_getNetworkLogs` - Get all network logs
- `mcp_getSelectedElement` - Get the currently selected DOM element
- `mcp_runAccessibilityAudit` - Run a WCAG-compliant accessibility audit
- `mcp_runPerformanceAudit` - Run a performance audit
- `mcp_runSEOAudit` - Run an SEO audit
- `mcp_runBestPracticesAudit` - Run a best practices audit

## Integration

This server is designed to work with AI tools and platforms that support the Model Context Protocol (MCP). It provides a standardized interface for AI models to interact with browser state and debugging information.

## License

MIT


================================================
FILE: browser-tools-mcp/mcp-server.ts
================================================
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import path from "path";
import fs from "fs";

// Create the MCP server
const server = new McpServer({
  name: "Browser Tools MCP",
  version: "1.2.0",
});

// Track the discovered server connection
let discoveredHost = "127.0.0.1";
let discoveredPort = 3025;
let serverDiscovered = false;

// Function to get the default port from environment variable or default
function getDefaultServerPort(): number {
  // Check environment variable first
  if (process.env.BROWSER_TOOLS_PORT) {
    const envPort = parseInt(process.env.BROWSER_TOOLS_PORT, 10);
    if (!isNaN(envPort) && envPort > 0) {
      return envPort;
    }
  }

  // Try to read from .port file
  try {
    const portFilePath = path.join(__dirname, ".port");
    if (fs.existsSync(portFilePath)) {
      const port = parseInt(fs.readFileSync(portFilePath, "utf8").trim(), 10);
      if (!isNaN(port) && port > 0) {
        return port;
      }
    }
  } catch (err) {
    console.error("Error reading port file:", err);
  }

  // Default port if no configuration found
  return 3025;
}

// Function to get default server host from environment variable or default
function getDefaultServerHost(): string {
  // Check environment variable first
  if (process.env.BROWSER_TOOLS_HOST) {
    return process.env.BROWSER_TOOLS_HOST;
  }

  // Default to localhost
  return "127.0.0.1";
}

// Server discovery function - similar to what you have in the Chrome extension
async function discoverServer(): Promise<boolean> {
  console.log("Starting server discovery process");

  // Common hosts to try
  const hosts = [getDefaultServerHost(), "127.0.0.1", "localhost"];

  // Ports to try (start with default, then try others)
  const defaultPort = getDefaultServerPort();
  const ports = [defaultPort];

  // Add additional ports (fallback range)
  for (let p = 3025; p <= 3035; p++) {
    if (p !== defaultPort) {
      ports.push(p);
    }
  }

  console.log(`Will try hosts: ${hosts.join(", ")}`);
  console.log(`Will try ports: ${ports.join(", ")}`);

  // Try to find the server
  for (const host of hosts) {
    for (const port of ports) {
      try {
        console.log(`Checking ${host}:${port}...`);

        // Use the identity endpoint for validation
        const response = await fetch(`http://${host}:${port}/.identity`, {
          signal: AbortSignal.timeout(1000), // 1 second timeout
        });

        if (response.ok) {
          const identity = await response.json();

          // Verify this is actually our server by checking the signature
          if (identity.signature === "mcp-browser-connector-24x7") {
            console.log(`Successfully found server at ${host}:${port}`);

            // Save the discovered connection
            discoveredHost = host;
            discoveredPort = port;
            serverDiscovered = true;

            return true;
          }
        }
      } catch (error: any) {
        // Ignore connection errors during discovery
        console.error(`Error checking ${host}:${port}: ${error.message}`);
      }
    }
  }

  console.error("No server found during discovery");
  return false;
}

// Wrapper function to ensure server connection before making requests
async function withServerConnection<T>(
  apiCall: () => Promise<T>
): Promise<T | any> {
  // Attempt to discover server if not already discovered
  if (!serverDiscovered) {
    const discovered = await discoverServer();
    if (!discovered) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to discover browser connector server. Please ensure it's running.",
          },
        ],
        isError: true,
      };
    }
  }

  // Now make the actual API call with discovered host/port
  try {
    return await apiCall();
  } catch (error: any) {
    // If the request fails, try rediscovering the server once
    console.error(
      `API call failed: ${error.message}. Attempting rediscovery...`
    );
    serverDiscovered = false;

    if (await discoverServer()) {
      console.error("Rediscovery successful. Retrying API call...");
      try {
        // Retry the API call with the newly discovered connection
        return await apiCall();
      } catch (retryError: any) {
        console.error(`Retry failed: ${retryError.message}`);
        return {
          content: [
            {
              type: "text",
              text: `Error after reconnection attempt: ${retryError.message}`,
            },
          ],
          isError: true,
        };
      }
    } else {
      console.error("Rediscovery failed. Could not reconnect to server.");
      return {
        content: [
          {
            type: "text",
            text: `Failed to reconnect to server: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
}

// We'll define our tools that retrieve data from the browser connector
server.tool("getConsoleLogs", "Check our browser logs", async () => {
  return await withServerConnection(async () => {
    const response = await fetch(
      `http://${discoveredHost}:${discoveredPort}/console-logs`
    );
    const json = await response.json();
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(json, null, 2),
        },
      ],
    };
  });
});

server.tool(
  "getConsoleErrors",
  "Check our browsers console errors",
  async () => {
    return await withServerConnection(async () => {
      const response = await fetch(
        `http://${discoveredHost}:${discoveredPort}/console-errors`
      );
      const json = await response.json();
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(json, null, 2),
          },
        ],
      };
    });
  }
);

server.tool("getNetworkErrors", "Check our network ERROR logs", async () => {
  return await withServerConnection(async () => {
    const response = await fetch(
      `http://${discoveredHost}:${discoveredPort}/network-errors`
    );
    const json = await response.json();
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(json, null, 2),
        },
      ],
      isError: true,
    };
  });
});

server.tool("getNetworkLogs", "Check ALL our network logs", async () => {
  return await withServerConnection(async () => {
    const response = await fetch(
      `http://${discoveredHost}:${discoveredPort}/network-success`
    );
    const json = await response.json();
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(json, null, 2),
        },
      ],
    };
  });
});

server.tool(
  "takeScreenshot",
  "Take a screenshot of the current browser tab",
  async () => {
    return await withServerConnection(async () => {
      try {
        const response = await fetch(
          `http://${discoveredHost}:${discoveredPort}/capture-screenshot`,
          {
            method: "POST",
          }
        );

        const result = await response.json();

        if (response.ok) {
          return {
            content: [
              {
                type: "text",
                text: "Successfully saved screenshot",
              },
            ],
          };
        } else {
          return {
            content: [
              {
                type: "text",
                text: `Error taking screenshot: ${result.error}`,
              },
            ],
          };
        }
      } catch (error: any) {
        const errorMessage =
          error instanceof Error ? error.message : String(error);
        return {
          content: [
            {
              type: "text",
              text: `Failed to take screenshot: ${errorMessage}`,
            },
          ],
        };
      }
    });
  }
);

server.tool(
  "getSelectedElement",
  "Get the selected element from the browser",
  async () => {
    return await withServerConnection(async () => {
      const response = await fetch(
        `http://${discoveredHost}:${discoveredPort}/selected-element`
      );
      const json = await response.json();
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(json, null, 2),
          },
        ],
      };
    });
  }
);

server.tool("wipeLogs", "Wipe all browser logs from memory", async () => {
  return await withServerConnection(async () => {
    const response = await fetch(
      `http://${discoveredHost}:${discoveredPort}/wipelogs`,
      {
        method: "POST",
      }
    );
    const json = await response.json();
    return {
      content: [
        {
          type: "text",
          text: json.message,
        },
      ],
    };
  });
});

// Define audit categories as enum to match the server's AuditCategory enum
enum AuditCategory {
  ACCESSIBILITY = "accessibility",
  PERFORMANCE = "performance",
  SEO = "seo",
  BEST_PRACTICES = "best-practices",
  PWA = "pwa",
}

// Add tool for accessibility audits, launches a headless browser instance
server.tool(
  "runAccessibilityAudit",
  "Run an accessibility audit on the current page",
  {},
  async () => {
    return await withServerConnection(async () => {
      try {
        // Simplified approach - let the browser connector handle the current tab and URL
        console.log(
          `Sending POST request to http://${discoveredHost}:${discoveredPort}/accessibility-audit`
        );
        const response = await fetch(
          `http://${discoveredHost}:${discoveredPort}/accessibility-audit`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify({
              category: AuditCategory.ACCESSIBILITY,
              source: "mcp_tool",
              timestamp: Date.now(),
            }),
          }
        );

        // Log the response status
        console.log(`Accessibility audit response status: ${response.status}`);

        if (!response.ok) {
          const errorText = await response.text();
          console.error(`Accessibility audit error: ${errorText}`);
          throw new Error(`Server returned ${response.status}: ${errorText}`);
        }

        const json = await response.json();

        // flatten it by merging metadata with the report contents
        if (json.report) {
          const { metadata, report } = json;
          const flattened = {
            ...metadata,
            ...report,
          };

          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(flattened, null, 2),
              },
            ],
          };
        } else {
          // Return as-is if it's not in the new format
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(json, null, 2),
              },
            ],
          };
        }
      } catch (error) {
        const errorMessage =
          error instanceof Error ? error.message : String(error);
        console.error("Error in accessibility audit:", errorMessage);
        return {
          content: [
            {
              type: "text",
              text: `Failed to run accessibility audit: ${errorMessage}`,
            },
          ],
        };
      }
    });
  }
);

// Add tool for performance audits, launches a headless browser instance
server.tool(
  "runPerformanceAudit",
  "Run a performance audit on the current page",
  {},
  async () => {
    return await withServerConnection(async () => {
      try {
        // Simplified approach - let the browser connector handle the current tab and URL
        console.log(
          `Sending POST request to http://${discoveredHost}:${discoveredPort}/performance-audit`
        );
        const response = await fetch(
          `http://${discoveredHost}:${discoveredPort}/performance-audit`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify({
              category: AuditCategory.PERFORMANCE,
              source: "mcp_tool",
              timestamp: Date.now(),
            }),
          }
        );

        // Log the response status
        console.log(`Performance audit response status: ${response.status}`);

        if (!response.ok) {
          const errorText = await response.text();
          console.error(`Performance audit error: ${errorText}`);
          throw new Error(`Server returned ${response.status}: ${errorText}`);
        }

        const json = await response.json();

        // flatten it by merging metadata with the report contents
        if (json.report) {
          const { metadata, report } = json;
          const flattened = {
            ...metadata,
            ...report,
          };

          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(flattened, null, 2),
              },
            ],
          };
        } else {
          // Return as-is if it's not in the new format
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(json, null, 2),
              },
            ],
          };
        }
      } catch (error) {
        const errorMessage =
          error instanceof Error ? error.message : String(error);
        console.error("Error in performance audit:", errorMessage);
        return {
          content: [
            {
              type: "text",
              text: `Failed to run performance audit: ${errorMessage}`,
            },
          ],
        };
      }
    });
  }
);

// Add tool for SEO audits, launches a headless browser instance
server.tool(
  "runSEOAudit",
  "Run an SEO audit on the current page",
  {},
  async () => {
    return await withServerConnection(async () => {
      try {
        console.log(
          `Sending POST request to http://${discoveredHost}:${discoveredPort}/seo-audit`
        );
        const response = await fetch(
          `http://${discoveredHost}:${discoveredPort}/seo-audit`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify({
              category: AuditCategory.SEO,
              source: "mcp_tool",
              timestamp: Date.now(),
            }),
          }
        );

        // Log the response status
        console.log(`SEO audit response status: ${response.status}`);

        if (!response.ok) {
          const errorText = await response.text();
          console.error(`SEO audit error: ${errorText}`);
          throw new Error(`Server returned ${response.status}: ${errorText}`);
        }

        const json = await response.json();

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(json, null, 2),
            },
          ],
        };
      } catch (error) {
        const errorMessage =
          error instanceof Error ? error.message : String(error);
        console.error("Error in SEO audit:", errorMessage);
        return {
          content: [
            {
              type: "text",
              text: `Failed to run SEO audit: ${errorMessage}`,
            },
          ],
        };
      }
    });
  }
);

server.tool("runNextJSAudit", {}, async () => ({
  content: [
    {
      type: "text",
      text: `
      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.

      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. 

      When no more areas of improvement are found, return "No more areas of improvement found, your NextJS application is optimized for SEO!".

      Start by analyzing each of the following aspects of our codebase:
      1. Meta tags - provides information about your website to search engines and social media platforms.

        Pages should provide the following standard meta tags:

        title
        description
        keywords
        robots
        viewport
        charSet
        Open Graph meta tags:

        og:site_name
        og:locale
        og:title
        og:description
        og:type
        og:url
        og:image
        og:image:alt
        og:image:type
        og:image:width
        og:image:height
        Article meta tags (actually it's also OpenGraph):

        article:published_time
        article:modified_time
        article:author
        Twitter meta tags:

        twitter:card
        twitter:site
        twitter:creator
        twitter:title
        twitter:description
        twitter:image

        For applications using the pages router, set up metatags like this in pages/[slug].tsx:
          import Head from "next/head";

          export default function Page() {
            return (
              <Head>
                <title>
                  Next.js SEO: The Complete Checklist to Boost Your Site Ranking
                </title>
                <meta
                  name="description"
                  content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
                />
                <meta
                  name="keywords"
                  content="nextjs seo complete checklist, nextjs seo tutorial"
                />
                <meta name="robots" content="index, follow" />
                <meta name="googlebot" content="index, follow" />
                <meta name="viewport" content="width=device-width, initial-scale=1.0" />
                <meta charSet="utf-8" />
                <meta property="og:site_name" content="Blog | Minh Vu" />
                <meta property="og:locale" content="en_US" />
                <meta
                  property="og:title"
                  content="Next.js SEO: The Complete Checklist to Boost Your Site Ranking"
                />
                <meta
                  property="og:description"
                  content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
                />
                <meta property="og:type" content="website" />
                <meta property="og:url" content="https://dminhvu.com/nextjs-seo" />
                <meta
                  property="og:image"
                  content="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png"
                />
                <meta property="og:image:alt" content="Next.js SEO" />
                <meta property="og:image:type" content="image/png" />
                <meta property="og:image:width" content="1200" />
                <meta property="og:image:height" content="630" />
                <meta
                  property="article:published_time"
                  content="2024-01-11T11:35:00+07:00"
                />
                <meta
                  property="article:modified_time"
                  content="2024-01-11T11:35:00+07:00"
                />
                <meta
                  property="article:author"
                  content="https://www.linkedin.com/in/dminhvu02"
                />
                <meta name="twitter:card" content="summary_large_image" />
                <meta name="twitter:site" content="@dminhvu02" />
                <meta name="twitter:creator" content="@dminhvu02" />
                <meta
                  name="twitter:title"
                  content="Next.js SEO: The Complete Checklist to Boost Your Site Ranking"
                />
                <meta
                  name="twitter:description"
                  content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
                />
                <meta
                  name="twitter:image"
                  content="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png"
                />
              </Head>
            );
          }

        For applications using the app router, set up metatags like this in layout.tsx:
          import type { Viewport, Metadata } from "next";

          export const viewport: Viewport = {
            width: "device-width",
            initialScale: 1,
            themeColor: "#ffffff"
          };
          
          export const metadata: Metadata = {
            metadataBase: new URL("https://dminhvu.com"),
            openGraph: {
              siteName: "Blog | Minh Vu",
              type: "website",
              locale: "en_US"
            },
            robots: {
              index: true,
              follow: true,
              "max-image-preview": "large",
              "max-snippet": -1,
              "max-video-preview": -1,
              googleBot: "index, follow"
            },
            alternates: {
              types: {
                "application/rss+xml": "https://dminhvu.com/rss.xml"
              }
            },
            applicationName: "Blog | Minh Vu",
            appleWebApp: {
              title: "Blog | Minh Vu",
              statusBarStyle: "default",
              capable: true
            },
            verification: {
              google: "YOUR_DATA",
              yandex: ["YOUR_DATA"],
              other: {
                "msvalidate.01": ["YOUR_DATA"],
                "facebook-domain-verification": ["YOUR_DATA"]
              }
            },
            icons: {
              icon: [
                {
                  url: "/favicon.ico",
                  type: "image/x-icon"
                },
                {
                  url: "/favicon-16x16.png",
                  sizes: "16x16",
                  type: "image/png"
                }
                // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
              ],
              shortcut: [
                {
                  url: "/favicon.ico",
                  type: "image/x-icon"
                }
              ],
              apple: [
                {
                  url: "/apple-icon-57x57.png",
                  sizes: "57x57",
                  type: "image/png"
                },
                {
                  url: "/apple-icon-60x60.png",
                  sizes: "60x60",
                  type: "image/png"
                }
                // 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
              ]
            }
          };
        And like this for any page.tsx file:
          import { Metadata } from "next";

          export const metadata: Metadata = {
            title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
            description:
              "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
            keywords: [
              "elastic",
              "python",
              "javascript",
              "react",
              "machine learning",
              "data science"
            ],
            openGraph: {
              url: "https://dminhvu.com",
              type: "website",
              title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
              description:
                "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
              images: [
                {
                  url: "https://dminhvu.com/images/home/thumbnail.png",
                  width: 1200,
                  height: 630,
                  alt: "dminhvu"
                }
              ]
            },
            twitter: {
              card: "summary_large_image",
              title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
              description:
                "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
              creator: "@dminhvu02",
              site: "@dminhvu02",
              images: [
                {
                  url: "https://dminhvu.com/images/home/thumbnail.png",
                  width: 1200,
                  height: 630,
                  alt: "dminhvu"
                }
              ]
            },
            alternates: {
              canonical: "https://dminhvu.com"
            }
          };

          Note that the charSet and viewport are automatically added by Next.js App Router, so you don't need to define them.

        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:

        import type { Metadata, ResolvingMetadata } from "next";

        type Params = {
          slug: string;
        };
        
        type Props = {
          params: Params;
          searchParams: { [key: string]: string | string[] | undefined };
        };
        
        export async function generateMetadata(
          { params, searchParams }: Props,
          parent: ResolvingMetadata
        ): Promise<Metadata> {
          const { slug } = params;
        
          const post: Post = await fetch("YOUR_ENDPOINT", {
            method: "GET",
            next: {
              revalidate: 60 * 60 * 24
            }
          }).then((res) => res.json());
        
          return {
            title: "{post.title} | dminhvu",
            authors: [
              {
                name: post.author || "Minh Vu"
              }
            ],
            description: post.description,
            keywords: post.keywords,
            openGraph: {
              title: "{post.title} | dminhvu",
              description: post.description,
              type: "article",
              url: "https://dminhvu.com/{post.slug}",
              publishedTime: post.created_at,
              modifiedTime: post.modified_at,
              authors: ["https://dminhvu.com/about"],
              tags: post.categories,
              images: [
                {
                  url: "https://ik.imagekit.io/dminhvu/assets/{post.slug}/thumbnail.png?tr=f-png",
                  width: 1024,
                  height: 576,
                  alt: post.title,
                  type: "image/png"
                }
              ]
            },
            twitter: {
              card: "summary_large_image",
              site: "@dminhvu02",
              creator: "@dminhvu02",
              title: "{post.title} | dminhvu",
              description: post.description,
              images: [
                {
                  url: "https://ik.imagekit.io/dminhvu/assets/{post.slug}/thumbnail.png?tr=f-png",
                  width: 1024,
                  height: 576,
                  alt: post.title
                }
              ]
            },
            alternates: {
              canonical: "https://dminhvu.com/{post.slug}"
            }
          };
        }

        
      2. JSON-LD Schema

      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.

      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:
      export default async function Page({ params }) {
        const { id } = await params
        const product = await getProduct(id)
      
        const jsonLd = {
          '@context': 'https://schema.org',
          '@type': 'Product',
          name: product.name,
          image: product.image,
          description: product.description,
        }
      
        return (
          <section>
            {/* Add JSON-LD to your page */}
            <script
              type="application/ld+json"
              dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
            />
            {/* ... */}
          </section>
        )
      }
      
      You can type your JSON-LD with TypeScript using community packages like schema-dts:


      import { Product, WithContext } from 'schema-dts'
      
      const jsonLd: WithContext<Product> = {
        '@context': 'https://schema.org',
        '@type': 'Product',
        name: 'Next.js Sticker',
        image: 'https://nextjs.org/imgs/sticker.png',
        description: 'Dynamic at the speed of static.',
      }
      3. Sitemap
      Your website should provide a sitemap so that search engines can easily crawl and index your pages.

        Generate Sitemap for Next.js Pages Router
        For Next.js Pages Router, you can use next-sitemap to generate a sitemap for your Next.js website after building.

        For example, running the following command will install next-sitemap and generate a sitemap for this blog:


        npm install next-sitemap
        npx next-sitemap
        A sitemap will be generated at public/sitemap.xml:

        public/sitemap.xml

        <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">
        <url>
          <loc>https://dminhvu.com</loc>
            <lastmod>2024-01-11T02:03:09.613Z</lastmod>
            <changefreq>daily</changefreq>
          <priority>0.7</priority>
        </url>
        <!-- other pages -->
        </urlset>
        Please visit the next-sitemap page for more information.

        Generate Sitemap for Next.js App Router
        For Next.js App Router, you can define the sitemap.ts file at app/sitemap.ts:

        app/sitemap.ts

        import {
          getAllCategories,
          getAllPostSlugsWithModifyTime
        } from "@/utils/getData";
        import { MetadataRoute } from "next";
        
        export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
          const defaultPages = [
            {
              url: "https://dminhvu.com",
              lastModified: new Date(),
              changeFrequency: "daily",
              priority: 1
            },
            {
              url: "https://dminhvu.com/about",
              lastModified: new Date(),
              changeFrequency: "monthly",
              priority: 0.9
            },
            {
              url: "https://dminhvu.com/contact",
              lastModified: new Date(),
              changeFrequency: "monthly",
              priority: 0.9
            }
            // other pages
          ];
        
          const postSlugs = await getAllPostSlugsWithModifyTime();
          const categorySlugs = await getAllCategories();
        
          const sitemap = [
            ...defaultPages,
            ...postSlugs.map((e: any) => ({
              url: "https://dminhvu.com/{e.slug}",
              lastModified: e.modified_at,
              changeFrequency: "daily",
              priority: 0.8
            })),
            ...categorySlugs.map((e: any) => ({
              url: "https://dminhvu.com/category/{e}",
              lastModified: new Date(),
              changeFrequency: "daily",
              priority: 0.7
            }))
          ];
        
          return sitemap;
        }
        With this sitemap.ts file created, you can access the sitemap at https://dminhvu.com/sitemap.xml.


        <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
          <url>
            <loc>https://dminhvu.com</loc>
            <lastmod>2024-01-11T02:03:09.613Z</lastmod>
            <changefreq>daily</changefreq>
            <priority>0.7</priority>
          </url>
          <!-- other pages -->
        </urlset>
      4. robots.txt
      A robots.txt file should be added to tell search engines which pages to crawl and which pages to ignore.

        robots.txt for Next.js Pages Router
        For Next.js Pages Router, you can create a robots.txt file at public/robots.txt:

        public/robots.txt

        User-agent: *
        Disallow:
        Sitemap: https://dminhvu.com/sitemap.xml
        You can prevent the search engine from crawling a page (usually search result pages, noindex pages, etc.) by adding the following line:

        public/robots.txt

        User-agent: *
        Disallow: /search?q=
        Disallow: /admin
        robots.txt for Next.js App Router
        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:

        app/robots.ts

        import { MetadataRoute } from "next";
        
        export default function robots(): MetadataRoute.Robots {
          return {
            rules: {
              userAgent: "*",
              allow: ["/"],
              disallow: ["/search?q=", "/admin/"]
            },
            sitemap: ["https://dminhvu.com/sitemap.xml"]
          };
        }
        With this robots.ts file created, you can access the robots.txt file at https://dminhvu.com/robots.txt.


        User-agent: *
        Allow: /
        Disallow: /search?q=
        Disallow: /admin
        
        Sitemap: https://dminhvu.com/sitemap.xml
      5. Link tags
      Link Tags for Next.js Pages Router
      For example, the current page has the following link tags if I use the Pages Router:

      pages/_app.tsx

      import Head from "next/head";
      
      export default function Page() {
        return (
          <Head>
            {/* other parts */}
            <link
              rel="alternate"
              type="application/rss+xml"
              href="https://dminhvu.com/rss.xml"
            />
            <link rel="icon" href="/favicon.ico" type="image/x-icon" />
            <link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png" />
            <link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png" />
            {/* 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 */}
            <link
              rel="icon"
              type="image/png"
              href="/favicon-16x16.png"
              sizes="16x16"
            />
            {/* add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png */}
          </Head>
        );
      }
      pages/[slug].tsx

      import Head from "next/head";
      
      export default function Page() {
        return (
          <Head>
            {/* other parts */}
            <link rel="canonical" href="https://dminhvu.com/nextjs-seo" />
          </Head>
        );
      }
      Link Tags for Next.js App Router
      For Next.js App Router, the link tags can be defined using the export const metadata or generateMetadata similar to the meta tags section.

      The code below is exactly the same as the meta tags for Next.js App Router section above.

      app/layout.tsx

      export const metadata: Metadata = {
        // other parts
        alternates: {
          types: {
            "application/rss+xml": "https://dminhvu.com/rss.xml"
          }
        },
        icons: {
          icon: [
            {
              url: "/favicon.ico",
              type: "image/x-icon"
            },
            {
              url: "/favicon-16x16.png",
              sizes: "16x16",
              type: "image/png"
            }
            // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
          ],
          shortcut: [
            {
              url: "/favicon.ico",
              type: "image/x-icon"
            }
          ],
          apple: [
            {
              url: "/apple-icon-57x57.png",
              sizes: "57x57",
              type: "image/png"
            },
            {
              url: "/apple-icon-60x60.png",
              sizes: "60x60",
              type: "image/png"
            }
            // 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
          ]
        }
      };
      app/page.tsx

      export const metadata: Metadata = {
        // other parts
        alternates: {
          canonical: "https://dminhvu.com"
        }
      };
      6. Script optimization
      Script Optimization for General Scripts
      Next.js provides a built-in component called <Script> to add external scripts to your website.

      For example, you can add Google Analytics to your website by adding the following script tag:

      pages/_app.tsx

      import Head from "next/head";
      import Script from "next/script";
      
      export default function Page() {
        return (
          <Head>
            {/* other parts */}
            {process.env.NODE_ENV === "production" && (
              <>
                <Script async strategy="afterInteractive" id="analytics">
                  {'
                    window.dataLayer = window.dataLayer || [];
                    function gtag(){dataLayer.push(arguments);}
                    gtag('js', new Date());
                    gtag('config', 'G-XXXXXXXXXX');
                  '}
                </Script>
              </>
            )}
          </Head>
        );
      }
      Script Optimization for Common Third-Party Integrations
      Next.js App Router introduces a new library called @next/third-parties for:

      Google Tag Manager
      Google Analytics
      Google Maps Embed
      YouTube Embed
      To use the @next/third-parties library, you need to install it:


      npm install @next/third-parties
      Then, you can add the following code to your app/layout.tsx:

      app/layout.tsx

      import { GoogleTagManager } from "@next/third-parties/google";
      import { GoogleAnalytics } from "@next/third-parties/google";
      import Head from "next/head";
      
      export default function Page() {
        return (
          <html lang="en" className="scroll-smooth" suppressHydrationWarning>
            {process.env.NODE_ENV === "production" && (
              <>
                <GoogleAnalytics gaId="G-XXXXXXXXXX" />
                {/* other scripts */}
              </>
            )}
            {/* other parts */}
          </html>
        );
      }
      Please note that you don't need to include both GoogleTagManager and GoogleAnalytics if you only use one of them.
      7. Image optimization
      Image Optimization
      This part can be applied to both Pages Router and App Router.

      Image optimization is also an important part of SEO as it helps your website load faster.

      Faster image rendering speed will contribute to the Google PageSpeed score, which can improve user experience and SEO.

      You can use next/image to optimize images in your Next.js website.

      For example, the following code will optimize this post thumbnail:


      import Image from "next/image";
      
      export default function Page() {
        return (
          <Image
            src="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-webp"
            alt="Next.js SEO"
            width={1200}
            height={630}
          />
        );
      }
      Remember to use a CDN to serve your media (images, videos, etc.) to improve the loading speed.

      For the image format, use WebP if possible because it has a smaller size than PNG and JPEG.

      Given the provided procedures, begin by analyzing all of our Next.js pages.
      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.
      Once you've performed this comprehensive analysis, return back a report on what we can do to improve our application.
      Do not actually make the code changes yet, just return a comprehensive plan that you will ask for approval for.
      If feedback is provided, adjust the plan accordingly and ask for approval again.
      If the user approves of the plan, go ahead and proceed to implement all the necessary code changes to completely optimize our application.
    `,
    },
  ],
}));

server.tool(
  "runDebuggerMode",
  "Run debugger mode to debug an issue in our application",
  async () => ({
    content: [
      {
        type: "text",
        text: `
      Please follow this exact sequence to debug an issue in our application:
  
  1. Reflect on 5-7 different possible sources of the problem
  2. Distill those down to 1-2 most likely sources
  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
  4. Use the "getConsoleLogs", "getConsoleErrors", "getNetworkLogs" & "getNetworkErrors" tools to obtain any newly added web browser logs
  5. Obtain the server logs as well if accessible - otherwise, ask me to copy/paste them into the chat
  6. Deeply reflect on what could be wrong + produce a comprehensive analysis of the issue
  7. Suggest additional logs if the issue persists or if the source is not yet clear
  8. Once a fix is implemented, ask for approval to remove the previously added logs

  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.
`,
      },
    ],
  })
);

server.tool(
  "runAuditMode",
  "Run audit mode to optimize our application for SEO, accessibility and performance",
  async () => ({
    content: [
      {
        type: "text",
        text: `
      I want you to enter "Audit Mode". Use the following MCP tools one after the other in this exact sequence:
      
      1. runAccessibilityAudit
      2. runPerformanceAudit
      3. runBestPracticesAudit
      4. runSEOAudit
      5. runNextJSAudit (only if our application is ACTUALLY using NextJS)

      After running all of these tools, return back a comprehensive analysis of the audit results.

      Do NOT use runNextJSAudit tool unless you see that our application is ACTUALLY using NextJS.

      DO NOT use the takeScreenshot tool EVER during audit mode. ONLY use it if I specifically ask you to take a screenshot of something.

      DO NOT check console or network logs to get started - your main priority is to run the audits in the sequence defined above.
      
      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.

      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.

      Keep repeating / iterating through this process with the four tools until our application is as optimized as possible for SEO, accessibility and performance.

`,
      },
    ],
  })
);

// Add tool for Best Practices audits, launches a headless browser instance
server.tool(
  "runBestPracticesAudit",
  "Run a best practices audit on the current page",
  {},
  async () => {
    return await withServerConnection(async () => {
      try {
        console.log(
          `Sending POST request to http://${discoveredHost}:${discoveredPort}/best-practices-audit`
        );
        const response = await fetch(
          `http://${discoveredHost}:${discoveredPort}/best-practices-audit`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              Accept: "application/json",
            },
            body: JSON.stringify({
              source: "mcp_tool",
              timestamp: Date.now(),
            }),
          }
        );

        // Check for errors
        if (!response.ok) {
          const errorText = await response.text();
          throw new Error(`Server returned ${response.status}: ${errorText}`);
        }

        const json = await response.json();

        // flatten it by merging metadata with the report contents
        if (json.report) {
          const { metadata, report } = json;
          const flattened = {
            ...metadata,
            ...report,
          };

          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(flattened, null, 2),
              },
            ],
          };
        } else {
          // Return as-is if it's not in the new format
          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(json, null, 2),
              },
            ],
          };
        }
      } catch (error) {
        const errorMessage =
          error instanceof Error ? error.message : String(error);
        console.error("Error in Best Practices audit:", errorMessage);
        return {
          content: [
            {
              type: "text",
              text: `Failed to run Best Practices audit: ${errorMessage}`,
            },
          ],
        };
      }
    });
  }
);

// Start receiving messages on stdio
(async () => {
  try {
    // Attempt initial server discovery
    console.error("Attempting initial server discovery on startup...");
    await discoverServer();
    if (serverDiscovered) {
      console.error(
        `Successfully discovered server at ${discoveredHost}:${discoveredPort}`
      );
    } else {
      console.error(
        "Initial server discovery failed. Will try again when tools are used."
      );
    }

    const transport = new StdioServerTransport();

    // Ensure stdout is only used for JSON messages
    const originalStdoutWrite = process.stdout.write.bind(process.stdout);
    process.stdout.write = (chunk: any, encoding?: any, callback?: any) => {
      // Only allow JSON messages to pass through
      if (typeof chunk === "string" && !chunk.startsWith("{")) {
        return true; // Silently skip non-JSON messages
      }
      return originalStdoutWrite(chunk, encoding, callback);
    };

    await server.connect(transport);
  } catch (error) {
    console.error("Failed to initialize MCP server:", error);
    process.exit(1);
  }
})();


================================================
FILE: browser-tools-mcp/package.json
================================================
{
  "name": "@agentdeskai/browser-tools-mcp",
  "version": "1.2.0",
  "description": "MCP (Model Context Protocol) server for browser tools integration",
  "main": "dist/mcp-server.js",
  "bin": {
    "browser-tools-mcp": "dist/mcp-server.js"
  },
  "scripts": {
    "inspect": "tsc && npx @modelcontextprotocol/inspector node -- dist/mcp-server.js",
    "inspect-live": "npx @modelcontextprotocol/inspector npx -- @agentdeskai/browser-tools-mcp",
    "build": "tsc",
    "start": "tsc && node dist/mcp-server.js",
    "prepublishOnly": "npm run build",
    "update": "npm run build && npm version patch && npm publish"
  },
  "keywords": [
    "mcp",
    "model-context-protocol",
    "browser",
    "tools",
    "debugging",
    "ai",
    "chrome",
    "extension"
  ],
  "author": "AgentDesk AI",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.4.1",
    "body-parser": "^1.20.3",
    "cors": "^2.8.5",
    "express": "^4.21.2",
    "llm-cost": "^1.0.5",
    "node-fetch": "^2.7.0",
    "ws": "^8.18.0"
  },
  "devDependencies": {
    "@types/ws": "^8.5.14",
    "@types/body-parser": "^1.19.5",
    "@types/cors": "^2.8.17",
    "@types/express": "^5.0.0",
    "@types/node": "^22.13.1",
    "@types/node-fetch": "^2.6.11",
    "typescript": "^5.7.3"
  }
}


================================================
FILE: browser-tools-mcp/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": ".",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["*.ts"],
  "exclude": ["node_modules", "dist"]
} 

================================================
FILE: browser-tools-server/README.md
================================================
# Browser Tools Server

A powerful browser tools server for capturing and managing browser events, logs, and screenshots. This server works in conjunction with the Browser Tools Chrome Extension to provide comprehensive browser debugging capabilities.

## Features

- Console log capture
- Network request monitoring
- Screenshot capture
- Element selection tracking
- WebSocket real-time communication
- Configurable log limits and settings
- Lighthouse-powered accessibility, performance, SEO, and best practices audits

## Installation

```bash
npx @agentdeskai/browser-tools-server
```

Or install globally:

```bash
npm install -g @agentdeskai/browser-tools-server
```

## Usage

1. Start the server:

```bash
npx @agentdeskai/browser-tools-server
```

2. The server will start on port 3025 by default

3. Install and enable the Browser Tools Chrome Extension

4. The server exposes the following endpoints:

- `/console-logs` - Get console logs
- `/console-errors` - Get console errors
- `/network-errors` - Get network error logs
- `/network-success` - Get successful network requests
- `/all-xhr` - Get all network requests
- `/screenshot` - Capture screenshots
- `/selected-element` - Get currently selected DOM element
- `/accessibility-audit` - Run accessibility audit on current page
- `/performance-audit` - Run performance audit on current page
- `/seo-audit` - Run SEO audit on current page

## API Documentation

### GET Endpoints

- `GET /console-logs` - Returns recent console logs
- `GET /console-errors` - Returns recent console errors
- `GET /network-errors` - Returns recent network errors
- `GET /network-success` - Returns recent successful network requests
- `GET /all-xhr` - Returns all recent network requests
- `GET /selected-element` - Returns the currently selected DOM element

### POST Endpoints

- `POST /extension-log` - Receive logs from the extension
- `POST /screenshot` - Capture and save screenshots
- `POST /selected-element` - Update the selected element
- `POST /wipelogs` - Clear all stored logs
- `POST /accessibility-audit` - Run a WCAG-compliant accessibility audit on the current page
- `POST /performance-audit` - Run a performance audit on the current page
- `POST /seo-audit` - Run a SEO audit on the current page

# Audit Functionality

The server provides Lighthouse-powered audit capabilities through four AI-optimized endpoints. These audits have been specifically tailored for AI consumption, with structured data, clear categorization, and smart prioritization.

## Smart Limit Implementation

All audit tools implement a "smart limit" approach to provide the most relevant information based on impact severity:

- **Critical issues**: No limit (all issues are shown)
- **Serious issues**: Up to 15 items per issue
- **Moderate issues**: Up to 10 items per issue
- **Minor issues**: Up to 3 items per issue

This ensures that the most important issues are always included in the response, while less important ones are limited to maintain a manageable response size for AI processing.

## Common Audit Response Structure

All audit responses follow a similar structure:

```json
{
  "metadata": {
    "url": "https://example.com",
    "timestamp": "2025-03-06T16:28:30.930Z",
    "device": "desktop",
    "lighthouseVersion": "11.7.1"
  },
  "report": {
    "score": 88,
    "audit_counts": {
      "failed": 2,
      "passed": 17,
      "manual": 10,
      "informative": 0,
      "not_applicable": 42
    }
    // Audit-specific content
    // ...
  }
}
```

## Accessibility Audit (`/accessibility-audit`)

The accessibility audit evaluates web pages against WCAG standards, identifying issues that affect users with disabilities.

### Response Format

```json
{
  "metadata": {
    "url": "https://example.com",
    "timestamp": "2025-03-06T16:28:30.930Z",
    "device": "desktop",
    "lighthouseVersion": "11.7.1"
  },
  "report": {
    "score": 88,
    "audit_counts": {
      "failed": 2,
      "passed": 17,
      "manual": 10,
      "informative": 0,
      "not_applicable": 42
    },
    "issues": [
      {
        "id": "meta-viewport",
        "title": "`[user-scalable=\"no\"]` is used in the `<meta name=\"viewport\">` element or the `[maximum-scale]` attribute is less than 5.",
        "impact": "critical",
        "category": "a11y-best-practices",
        "elements": [
          {
            "selector": "head > meta",
            "snippet": "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0\">",
            "label": "head > meta",
            "issue_description": "Fix any of the following: user-scalable on <meta> tag disables zooming on mobile devices"
          }
        ],
        "score": 0
      }
    ],
    "categories": {
      "a11y-navigation": { "score": 0, "issues_count": 0 },
      "a11y-aria": { "score": 0, "issues_count": 1 },
      "a11y-best-practices": { "score": 0, "issues_count": 1 }
    },
    "critical_elements": [
      {
        "selector": "head > meta",
        "snippet": "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0\">",
        "label": "head > meta",
        "issue_description": "Fix any of the following: user-scalable on <meta> tag disables zooming on mobile devices"
      }
    ],
    "prioritized_recommendations": [
      "Fix ARIA attributes and roles",
      "Fix 1 issues in a11y-best-practices"
    ]
  }
}
```

### Key Features

- **Issues Categorized by Impact**: Critical, serious, moderate, and minor
- **Element-Specific Information**: Selectors, snippets, and labels for affected elements
- **Issue Categories**: ARIA, navigation, color contrast, forms, keyboard access, etc.
- **Critical Elements List**: Quick access to the most serious issues
- **Prioritized Recommendations**: Actionable advice in order of importance

## Performance Audit (`/performance-audit`)

The performance audit analyzes page load speed, Core Web Vitals, and optimization opportunities.

### Response Format

```json
{
  "metadata": {
    "url": "https://example.com",
    "timestamp": "2025-03-06T16:27:44.900Z",
    "device": "desktop",
    "lighthouseVersion": "11.7.1"
  },
  "report": {
    "score": 60,
    "audit_counts": {
      "failed": 11,
      "passed": 21,
      "manual": 0,
      "informative": 20,
      "not_applicable": 8
    },
    "metrics": [
      {
        "id": "lcp",
        "score": 0,
        "value_ms": 14149,
        "passes_core_web_vital": false,
        "element_selector": "div.heading > span",
        "element_type": "text",
        "element_content": "Welcome to Example"
      },
      {
        "id": "fcp",
        "score": 0.53,
        "value_ms": 1542,
        "passes_core_web_vital": false
      },
      {
        "id": "si",
        "score": 0,
        "value_ms": 6883
      },
      {
        "id": "tti",
        "score": 0,
        "value_ms": 14746
      },
      {
        "id": "cls",
        "score": 1,
        "value_ms": 0.001,
        "passes_core_web_vital": true
      },
      {
        "id": "tbt",
        "score": 1,
        "value_ms": 43,
        "passes_core_web_vital": true
      }
    ],
    "opportunities": [
      {
        "id": "render_blocking_resources",
        "savings_ms": 1270,
        "severity": "serious",
        "resources": [
          {
            "url": "styles.css",
            "savings_ms": 781
          }
        ]
      }
    ],
    "page_stats": {
      "total_size_kb": 2190,
      "total_requests": 108,
      "resource_counts": {
        "js": 86,
        "css": 1,
        "img": 3,
        "font": 3,
        "other": 15
      },
      "third_party_size_kb": 2110,
      "main_thread_blocking_time_ms": 693
    },
    "prioritized_recommendations": ["Improve Largest Contentful Paint (LCP)"]
  }
}
```

### Key Features

- **Core Web Vitals Analysis**: LCP, FCP, CLS, TBT with pass/fail status
- **Element Information for LCP**: Identifies what's causing the largest contentful paint
- **Optimization Opportunities**: Specific actions to improve performance with estimated time savings
- **Resource Breakdown**: By type, size, and origin (first vs. third party)
- **Main Thread Analysis**: Blocking time metrics to identify JavaScript performance issues
- **Resource-Specific Recommendations**: For each optimization opportunity

## SEO Audit (`/seo-audit`)

The SEO audit checks search engine optimization best practices and identifies issues that could affect search ranking.

### Response Format

```json
{
  "metadata": {
    "url": "https://example.com",
    "timestamp": "2025-03-06T16:29:12.455Z",
    "device": "desktop",
    "lighthouseVersion": "11.7.1"
  },
  "report": {
    "score": 91,
    "audit_counts": {
      "failed": 1,
      "passed": 10,
      "manual": 1,
      "informative": 0,
      "not_applicable": 3
    },
    "issues": [
      {
        "id": "is-crawlable",
        "title": "Page is blocked from indexing",
        "impact": "critical",
        "category": "crawlability",
        "score": 0
      }
    ],
    "categories": {
      "content": { "score": 0, "issues_count": 0 },
      "mobile": { "score": 0, "issues_count": 0 },
      "crawlability": { "score": 0, "issues_count": 1 },
      "other": { "score": 0, "issues_count": 0 }
    },
    "prioritized_recommendations": [
      "Fix crawlability issues (1 issues): robots.txt, sitemaps, and redirects"
    ]
  }
}
```

### Key Features

- **Issues Categorized by Impact**: Critical, serious, moderate, and minor
- **SEO Categories**: Content, mobile friendliness, crawlability
- **Issue Details**: Information about what's causing each SEO problem
- **Prioritized Recommendations**: Actionable advice in order of importance

## Best Practices Audit (`/best-practices-audit`)

The best practices audit evaluates adherence to web development best practices related to security, trust, user experience, and browser compatibility.

### Response Format

```json
{
  "metadata": {
    "url": "https://example.com",
    "timestamp": "2025-03-06T17:01:38.029Z",
    "device": "desktop",
    "lighthouseVersion": "11.7.1"
  },
  "report": {
    "score": 74,
    "audit_counts": {
      "failed": 4,
      "passed": 10,
      "manual": 0,
      "informative": 2,
      "not_applicable": 1
    },
    "issues": [
      {
        "id": "deprecations",
        "title": "Uses deprecated APIs",
        "impact": "critical",
        "category": "security",
        "score": 0,
        "details": [
          {
            "value": "UnloadHandler"
          }
        ]
      },
      {
        "id": "errors-in-console",
        "title": "Browser errors were logged to the console",
        "impact": "serious",
        "category": "user-experience",
        "score": 0,
        "details": [
          {
            "source": "console.error",
            "description": "ReferenceError: variable is not defined"
          }
        ]
      }
    ],
    "categories": {
      "security": { "score": 75, "issues_count": 1 },
      "trust": { "score": 100, "issues_count": 0 },
      "user-experience": { "score": 50, "issues_count": 1 },
      "browser-compat": { "score": 100, "issues_count": 0 },
      "other": { "score": 75, "issues_count": 2 }
    },
    "prioritized_recommendations": [
      "Address 1 security issues: vulnerabilities, CSP, deprecations",
      "Improve 1 user experience issues: console errors, user interactions"
    ]
  }
}
```

### Key Features

- **Issues Categorized by Impact**: Critical, serious, moderate, and minor
- **Best Practice Categories**: Security, trust, user experience, browser compatibility
- **Detailed Issue Information**: Specific problems affecting best practices compliance
- **Security Focus**: Special attention to security vulnerabilities and deprecated APIs
- **Prioritized Recommendations**: Actionable advice in order of importance

## License

MIT

# Puppeteer Service

A comprehensive browser automation service built on Puppeteer to provide reliable cross-platform browser control capabilities.

## Features

- **Cross-Platform Browser Support**:

  - Windows, macOS, and Linux support
  - Chrome, Edge, Brave, and Firefox detection
  - Fallback strategy for finding browser executables

- **Smart Browser Management**:

  - Singleton browser instance with automatic cleanup
  - Connection retry mechanisms
  - Temporary user data directories with cleanup

- **Rich Configuration Options**:
  - Custom browser paths
  - Network condition emulation
  - Device emulation (mobile, tablet, desktop)
  - Resource blocking
  - Cookies and headers customization
  - Locale and timezone emulation


================================================
FILE: browser-tools-server/browser-connector.ts
================================================
#!/usr/bin/env node

import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import { tokenizeAndEstimateCost } from "llm-cost";
import { WebSocketServer, WebSocket } from "ws";
import fs from "fs";
import path from "path";
import { IncomingMessage } from "http";
import { Socket } from "net";
import os from "os";
import { exec } from "child_process";
import {
  runPerformanceAudit,
  runAccessibilityAudit,
  runSEOAudit,
  AuditCategory,
  LighthouseReport,
} from "./lighthouse/index.js";
import * as net from "net";
import { runBestPracticesAudit } from "./lighthouse/best-practices.js";

/**
 * Converts a file path to the appropriate format for the current platform
 * Handles Windows, WSL, macOS and Linux path formats
 *
 * @param inputPath - The path to convert
 * @returns The converted path appropriate for the current platform
 */
function convertPathForCurrentPlatform(inputPath: string): string {
  const platform = os.platform();

  // If no path provided, return as is
  if (!inputPath) return inputPath;

  console.log(`Converting path "${inputPath}" for platform: ${platform}`);

  // Windows-specific conversion
  if (platform === "win32") {
    // Convert forward slashes to backslashes
    return inputPath.replace(/\//g, "\\");
  }

  // Linux/Mac-specific conversion
  if (platform === "linux" || platform === "darwin") {
    // Check if this is a Windows UNC path (starts with \\)
    if (inputPath.startsWith("\\\\") || inputPath.includes("\\")) {
      // Check if this is a WSL path (contains wsl.localhost or wsl$)
      if (inputPath.includes("wsl.localhost") || inputPath.includes("wsl$")) {
        // Extract the path after the distribution name
        // Handle both \\wsl.localhost\Ubuntu\path and \\wsl$\Ubuntu\path formats
        const parts = inputPath.split("\\").filter((part) => part.length > 0);
        console.log("Path parts:", parts);

        // Find the index after the distribution name
        const distNames = [
          "Ubuntu",
          "Debian",
          "kali",
          "openSUSE",
          "SLES",
          "Fedora",
        ];

        // Find the distribution name in the path
        let distIndex = -1;
        for (const dist of distNames) {
          const index = parts.findIndex(
            (part) => part === dist || part.toLowerCase() === dist.toLowerCase()
          );
          if (index !== -1) {
            distIndex = index;
            break;
          }
        }

        if (distIndex !== -1 && distIndex + 1 < parts.length) {
          // Reconstruct the path as a native Linux path
          const linuxPath = "/" + parts.slice(distIndex + 1).join("/");
          console.log(
            `Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`
          );
          return linuxPath;
        }

        // If we couldn't find a distribution name but it's clearly a WSL path,
        // try to extract everything after wsl.localhost or wsl$
        const wslIndex = parts.findIndex(
          (part) =>
            part === "wsl.localhost" ||
            part === "wsl$" ||
            part.toLowerCase() === "wsl.localhost" ||
            part.toLowerCase() === "wsl$"
        );

        if (wslIndex !== -1 && wslIndex + 2 < parts.length) {
          // Skip the WSL prefix and distribution name
          const linuxPath = "/" + parts.slice(wslIndex + 2).join("/");
          console.log(
            `Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`
          );
          return linuxPath;
        }
      }

      // For non-WSL Windows paths, just normalize the slashes
      const normalizedPath = inputPath
        .replace(/\\\\/g, "/")
        .replace(/\\/g, "/");
      console.log(
        `Converted Windows UNC path "${inputPath}" to "${normalizedPath}"`
      );
      return normalizedPath;
    }

    // Handle Windows drive letters (e.g., C:\path\to\file)
    if (/^[A-Z]:\\/i.test(inputPath)) {
      // Convert Windows drive path to Linux/Mac compatible path
      const normalizedPath = inputPath
        .replace(/^[A-Z]:\\/i, "/")
        .replace(/\\/g, "/");
      console.log(
        `Converted Windows drive path "${inputPath}" to "${normalizedPath}"`
      );
      return normalizedPath;
    }
  }

  // Return the original path if no conversion was needed or possible
  return inputPath;
}

// Function to get default downloads folder
function getDefaultDownloadsFolder(): string {
  const homeDir = os.homedir();
  // Downloads folder is typically the same path on Windows, macOS, and Linux
  const downloadsPath = path.join(homeDir, "Downloads", "mcp-screenshots");
  return downloadsPath;
}

// We store logs in memory
const consoleLogs: any[] = [];
const consoleErrors: any[] = [];
const networkErrors: any[] = [];
const networkSuccess: any[] = [];
const allXhr: any[] = [];

// Store the current URL from the extension
let currentUrl: string = "";

// Store the current tab ID from the extension
let currentTabId: string | number | null = null;

// Add settings state
let currentSettings = {
  logLimit: 50,
  queryLimit: 30000,
  showRequestHeaders: false,
  showResponseHeaders: false,
  model: "claude-3-sonnet",
  stringSizeLimit: 500,
  maxLogSize: 20000,
  screenshotPath: getDefaultDownloadsFolder(),
  // Add server host configuration
  serverHost: process.env.SERVER_HOST || "0.0.0.0", // Default to all interfaces
};

// Add new storage for selected element
let selectedElement: any = null;

// Add new state for tracking screenshot requests
interface ScreenshotCallback {
  resolve: (value: {
    data: string;
    path?: string;
    autoPaste?: boolean;
  }) => void;
  reject: (reason: Error) => void;
}

const screenshotCallbacks = new Map<string, ScreenshotCallback>();

// Function to get available port starting with the given port
async function getAvailablePort(
  startPort: number,
  maxAttempts: number = 10
): Promise<number> {
  let currentPort = startPort;
  let attempts = 0;

  while (attempts < maxAttempts) {
    try {
      // Try to create a server on the current port
      // We'll use a raw Node.js net server for just testing port availability
      await new Promise<void>((resolve, reject) => {
        const testServer = net.createServer();

        // Handle errors (e.g., port in use)
        testServer.once("error", (err: any) => {
          if (err.code === "EADDRINUSE") {
            console.log(`Port ${currentPort} is in use, trying next port...`);
            currentPort++;
            attempts++;
            resolve(); // Continue to next iteration
          } else {
            reject(err); // Different error, propagate it
          }
        });

        // If we can listen, the port is available
        testServer.once("listening", () => {
          // Make sure to close the server to release the port
          testServer.close(() => {
            console.log(`Found available port: ${currentPort}`);
            resolve();
          });
        });

        // Try to listen on the current port
        testServer.listen(currentPort, currentSettings.serverHost);
      });

      // If we reach here without incrementing the port, it means the port is available
      return currentPort;
    } catch (error: any) {
      console.error(`Error checking port ${currentPort}:`, error);
      // For non-EADDRINUSE errors, try the next port
      currentPort++;
      attempts++;
    }
  }

  // If we've exhausted all attempts, throw an error
  throw new Error(
    `Could not find an available port after ${maxAttempts} attempts starting from ${startPort}`
  );
}

// Start with requested port and find an available one
const REQUESTED_PORT = parseInt(process.env.PORT || "3025", 10);
let PORT = REQUESTED_PORT;

// Create application and initialize middleware
const app = express();
app.use(cors());
// Increase JSON body parser limit to 50MB to handle large screenshots
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));

// Helper to recursively truncate strings in any data structure
function truncateStringsInData(data: any, maxLength: number): any {
  if (typeof data === "string") {
    return data.length > maxLength
      ? data.substring(0, maxLength) + "... (truncated)"
      : data;
  }

  if (Array.isArray(data)) {
    return data.map((item) => truncateStringsInData(item, maxLength));
  }

  if (typeof data === "object" && data !== null) {
    const result: any = {};
    for (const [key, value] of Object.entries(data)) {
      result[key] = truncateStringsInData(value, maxLength);
    }
    return result;
  }

  return data;
}

// Helper to safely parse and process JSON strings
function processJsonString(jsonString: string, maxLength: number): string {
  try {
    // Try to parse the string as JSON
    const parsed = JSON.parse(jsonString);
    // Process any strings within the parsed JSON
    const processed = truncateStringsInData(parsed, maxLength);
    // Stringify the processed data
    return JSON.stringify(processed);
  } catch (e) {
    // If it's not valid JSON, treat it as a regular string
    return truncateStringsInData(jsonString, maxLength);
  }
}

// Helper to process logs based on settings
function processLogsWithSettings(logs: any[]) {
  return logs.map((log) => {
    const processedLog = { ...log };

    if (log.type === "network-request") {
      // Handle headers visibility
      if (!currentSettings.showRequestHeaders) {
        delete processedLog.requestHeaders;
      }
      if (!currentSettings.showResponseHeaders) {
        delete processedLog.responseHeaders;
      }
    }

    return processedLog;
  });
}

// Helper to calculate size of a log entry
function calculateLogSize(log: any): number {
  return JSON.stringify(log).length;
}

// Helper to truncate logs based on character limit
function truncateLogsToQueryLimit(logs: any[]): any[] {
  if (logs.length === 0) return logs;

  // First process logs according to current settings
  const processedLogs = processLogsWithSettings(logs);

  let currentSize = 0;
  const result = [];

  for (const log of processedLogs) {
    const logSize = calculateLogSize(log);

    // Check if adding this log would exceed the limit
    if (currentSize + logSize > currentSettings.queryLimit) {
      console.log(
        `Reached query limit (${currentSize}/${currentSettings.queryLimit}), truncating logs`
      );
      break;
    }

    // Add log and update size
    result.push(log);
    currentSize += logSize;
    console.log(`Added log of size ${logSize}, total size now: ${currentSize}`);
  }

  return result;
}

// Endpoint for the extension to POST data
app.post("/extension-log", (req, res) => {
  console.log("\n=== Received Extension Log ===");
  console.log("Request body:", {
    dataType: req.body.data?.type,
    timestamp: req.body.data?.timestamp,
    hasSettings: !!req.body.settings,
  });

  const { data, settings } = req.body;

  // Update settings if provided
  if (settings) {
    console.log("Updating settings:", settings);
    currentSettings = {
      ...currentSettings,
      ...settings,
    };
  }

  if (!data) {
    console.log("Warning: No data received in log request");
    res.status(400).json({ status: "error", message: "No data provided" });
    return;
  }

  console.log(`Processing ${data.type} log entry`);

  switch (data.type) {
    case "page-navigated":
      // Handle page navigation event via HTTP POST
      // Note: This is also handled in the WebSocket message handler
      // as the extension may send navigation events through either channel
      console.log("Received page navigation event with URL:", data.url);
      currentUrl = data.url;

      // Also update the tab ID if provided
      if (data.tabId) {
        console.log("Updating tab ID from page navigation event:", data.tabId);
        currentTabId = data.tabId;
      }

      console.log("Updated current URL:", currentUrl);
      break;
    case "console-log":
      console.log("Adding console log:", {
        level: data.level,
        message:
          data.message?.substring(0, 100) +
          (data.message?.length > 100 ? "..." : ""),
        timestamp: data.timestamp,
      });
      consoleLogs.push(data);
      if (consoleLogs.length > currentSettings.logLimit) {
        console.log(
          `Console logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`
        );
        consoleLogs.shift();
      }
      break;
    case "console-error":
      console.log("Adding console error:", {
        level: data.level,
        message:
          data.message?.substring(0, 100) +
          (data.message?.length > 100 ? "..." : ""),
        timestamp: data.timestamp,
      });
      consoleErrors.push(data);
      if (consoleErrors.length > currentSettings.logLimit) {
        console.log(
          `Console errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`
        );
        consoleErrors.shift();
      }
      break;
    case "network-request":
      const logEntry = {
        url: data.url,
        method: data.method,
        status: data.status,
        timestamp: data.timestamp,
      };
      console.log("Adding network request:", logEntry);

      // Route network requests based on status code
      if (data.status >= 400) {
        networkErrors.push(data);
        if (networkErrors.length > currentSettings.logLimit) {
          console.log(
            `Network errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`
          );
          networkErrors.shift();
        }
      } else {
        networkSuccess.push(data);
        if (networkSuccess.length > currentSettings.logLimit) {
          console.log(
            `Network success logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`
          );
          networkSuccess.shift();
        }
      }
      break;
    case "selected-element":
      console.log("Updating selected element:", {
        tagName: data.element?.tagName,
        id: data.element?.id,
        className: data.element?.className,
      });
      selectedElement = data.element;
      break;
    default:
      console.log("Unknown log type:", data.type);
  }

  console.log("Current log counts:", {
    consoleLogs: consoleLogs.length,
    consoleErrors: consoleErrors.length,
    networkErrors: networkErrors.length,
    networkSuccess: networkSuccess.length,
  });
  console.log("=== End Extension Log ===\n");

  res.json({ status: "ok" });
});

// Update GET endpoints to use the new function
app.get("/console-logs", (req, res) => {
  const truncatedLogs = truncateLogsToQueryLimit(consoleLogs);
  res.json(truncatedLogs);
});

app.get("/console-errors", (req, res) => {
  const truncatedLogs = truncateLogsToQueryLimit(consoleErrors);
  res.json(truncatedLogs);
});

app.get("/network-errors", (req, res) => {
  const truncatedLogs = truncateLogsToQueryLimit(networkErrors);
  res.json(truncatedLogs);
});

app.get("/network-success", (req, res) => {
  const truncatedLogs = truncateLogsToQueryLimit(networkSuccess);
  res.json(truncatedLogs);
});

app.get("/all-xhr", (req, res) => {
  // Merge and sort network success and error logs by timestamp
  const mergedLogs = [...networkSuccess, ...networkErrors].sort(
    (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
  );
  const truncatedLogs = truncateLogsToQueryLimit(mergedLogs);
  res.json(truncatedLogs);
});

// Add new endpoint for selected element
app.post("/selected-element", (req, res) => {
  const { data } = req.body;
  selectedElement = data;
  res.json({ status: "ok" });
});

app.get("/selected-element", (req, res) => {
  res.json(selectedElement || { message: "No element selected" });
});

app.get("/.port", (req, res) => {
  res.send(PORT.toString());
});

// Add new identity endpoint with a unique signature
app.get("/.identity", (req, res) => {
  res.json({
    port: PORT,
    name: "browser-tools-server",
    version: "1.2.0",
    signature: "mcp-browser-connector-24x7",
  });
});

// Add function to clear all logs
function clearAllLogs() {
  console.log("Wiping all logs...");
  consoleLogs.length = 0;
  consoleErrors.length = 0;
  networkErrors.length = 0;
  networkSuccess.length = 0;
  allXhr.length = 0;
  selectedElement = null;
  console.log("All logs have been wiped");
}

// Add endpoint to wipe logs
app.post("/wipelogs", (req, res) => {
  clearAllLogs();
  res.json({ status: "ok", message: "All logs cleared successfully" });
});

// Add endpoint for the extension to report the current URL
app.post("/current-url", (req, res) => {
  console.log(
    "Received current URL update request:",
    JSON.stringify(req.body, null, 2)
  );

  if (req.body && req.body.url) {
    const oldUrl = currentUrl;
    currentUrl = req.body.url;

    // Update the current tab ID if provided
    if (req.body.tabId) {
      const oldTabId = currentTabId;
      currentTabId = req.body.tabId;
      console.log(`Updated current tab ID: ${oldTabId} -> ${currentTabId}`);
    }

    // Log the source of the update if provided
    const source = req.body.source || "unknown";
    const tabId = req.body.tabId || "unknown";
    const timestamp = req.body.timestamp
      ? new Date(req.body.timestamp).toISOString()
      : "unknown";

    console.log(
      `Updated current URL via dedicated endpoint: ${oldUrl} -> ${currentUrl}`
    );
    console.log(
      `URL update details: source=${source}, tabId=${tabId}, timestamp=${timestamp}`
    );

    res.json({
      status: "ok",
      url: currentUrl,
      tabId: currentTabId,
      previousUrl: oldUrl,
      updated: oldUrl !== currentUrl,
    });
  } else {
    console.log("No URL provided in current-url request");
    res.status(400).json({ status: "error", message: "No URL provided" });
  }
});

// Add endpoint to get the current URL
app.get("/current-url", (req, res) => {
  console.log("Current URL requested, returning:", currentUrl);
  res.json({ url: currentUrl });
});

interface ScreenshotMessage {
  type: "screenshot-data" | "screenshot-error";
  data?: string;
  path?: string;
  error?: string;
  autoPaste?: boolean;
}

export class BrowserConnector {
  private wss: WebSocketServer;
  private activeConnection: WebSocket | null = null;
  private app: express.Application;
  private server: any;
  private urlRequestCallbacks: Map<string, (url: string) => void> = new Map();

  constructor(app: express.Application, server: any) {
    this.app = app;
    this.server = server;

    // Initialize WebSocket server using the existing HTTP server
    this.wss = new WebSocketServer({
      noServer: true,
      path: "/extension-ws",
    });

    // Register the capture-screenshot endpoint
    this.app.post(
      "/capture-screenshot",
      async (req: express.Request, res: express.Response) => {
        console.log(
          "Browser Connector: Received request to /capture-screenshot endpoint"
        );
        console.log("Browser Connector: Request body:", req.body);
        console.log(
          "Browser Connector: Active WebSocket connection:",
          !!this.activeConnection
        );
        await this.captureScreenshot(req, res);
      }
    );

    // Set up accessibility audit endpoint
    this.setupAccessibilityAudit();

    // Set up performance audit endpoint
    this.setupPerformanceAudit();

    // Set up SEO audit endpoint
    this.setupSEOAudit();

    // Set up Best Practices audit endpoint
    this.setupBestPracticesAudit();

    // Handle upgrade requests for WebSocket
    this.server.on(
      "upgrade",
      (request: IncomingMessage, socket: Socket, head: Buffer) => {
        if (request.url === "/extension-ws") {
          this.wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {
            this.wss.emit("connection", ws, request);
          });
        }
      }
    );

    this.wss.on("connection", (ws: WebSocket) => {
      console.log("Chrome extension connected via WebSocket");
      this.activeConnection = ws;

      ws.on("message", (message: string | Buffer | ArrayBuffer | Buffer[]) => {
        try {
          const data = JSON.parse(message.toString());
          // Log message without the base64 data
          console.log("Received WebSocket message:", {
            ...data,
            data: data.data ? "[base64 data]" : undefined,
          });

          // Handle URL response
          if (data.type === "current-url-response" && data.url) {
            console.log("Received current URL from browser:", data.url);
            currentUrl = data.url;

            // Also update the tab ID if provided
            if (data.tabId) {
              console.log(
                "Updating tab ID from WebSocket message:",
                data.tabId
              );
              currentTabId = data.tabId;
            }

            // Call the callback if exists
            if (
              data.requestId &&
              this.urlRequestCallbacks.has(data.requestId)
            ) {
              const callback = this.urlRequestCallbacks.get(data.requestId);
              if (callback) callback(data.url);
              this.urlRequestCallbacks.delete(data.requestId);
            }
          }
          // Handle page navigation event via WebSocket
          // Note: This is intentionally duplicated from the HTTP handler in /extension-log
          // as the extension may send navigation events through either channel
          if (data.type === "page-navigated" && data.url) {
            console.log("Page navigated to:", data.url);
            currentUrl = data.url;

            // Also update the tab ID if provided
            if (data.tabId) {
              console.log(
                "Updating tab ID from page navigation event:",
                data.tabId
              );
              currentTabId = data.tabId;
            }
          }
          // Handle screenshot response
          if (data.type === "screenshot-data" && data.data) {
            console.log("Received screenshot data");
            console.log("Screenshot path from extension:", data.path);
            console.log("Auto-paste setting from extension:", data.autoPaste);
            // Get the most recent callback since we're not using requestId anymore
            const callbacks = Array.from(screenshotCallbacks.values());
            if (callbacks.length > 0) {
              const callback = callbacks[0];
              console.log("Found callback, resolving promise");
              // Pass both the data, path and autoPaste to the resolver
              callback.resolve({
                data: data.data,
                path: data.path,
                autoPaste: data.autoPaste,
              });
              screenshotCallbacks.clear(); // Clear all callbacks
            } else {
              console.log("No callbacks found for screenshot");
            }
          }
          // Handle screenshot error
          else if (data.type === "screenshot-error") {
            console.log("Received screenshot error:", data.error);
            const callbacks = Array.from(screenshotCallbacks.values());
            if (callbacks.length > 0) {
              const callback = callbacks[0];
              callback.reject(
                new Error(data.error || "Screenshot capture failed")
              );
              screenshotCallbacks.clear(); // Clear all callbacks
            }
          } else {
            console.log("Unhandled message type:", data.type);
          }
        } catch (error) {
          console.error("Error processing WebSocket message:", error);
        }
      });

      ws.on("close", () => {
        console.log("Chrome extension disconnected");
        if (this.activeConnection === ws) {
          this.activeConnection = null;
        }
      });
    });

    // Add screenshot endpoint
    this.app.post(
      "/screenshot",
      (req: express.Request, res: express.Response): void => {
        console.log(
          "Browser Connector: Received request to /screenshot endpoint"
        );
        console.log("Browser Connector: Request body:", req.body);
        try {
          console.log("Received screenshot capture request");
          const { data, path: outputPath } = req.body;

          if (!data) {
            console.log("Screenshot request missing data");
            res.status(400).json({ error: "Missing screenshot data" });
            return;
          }

          // Use provided path or default to downloads folder
          const targetPath = outputPath || getDefaultDownloadsFolder();
          console.log(`Using screenshot path: ${targetPath}`);

          // Remove the data:image/png;base64, prefix
          const base64Data = data.replace(/^data:image\/png;base64,/, "");

          // Create the full directory path if it doesn't exist
          fs.mkdirSync(targetPath, { recursive: true });
          console.log(`Created/verified directory: ${targetPath}`);

          // Generate a unique filename using timestamp
          const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
          const filename = `screenshot-${timestamp}.png`;
          const fullPath = path.join(targetPath, filename);
          console.log(`Saving screenshot to: ${fullPath}`);

          // Write the file
          fs.writeFileSync(fullPath, base64Data, "base64");
          console.log("Screenshot saved successfully");

          res.json({
            path: fullPath,
            filename: filename,
          });
        } catch (error: unknown) {
          console.error("Error saving screenshot:", error);
          if (error instanceof Error) {
            res.status(500).json({ error: error.message });
          } else {
            res.status(500).json({ error: "An unknown error occurred" });
          }
        }
      }
    );
  }

  private async handleScreenshot(req: express.Request, res: express.Response) {
    if (!this.activeConnection) {
      return res.status(503).json({ error: "Chrome extension not connected" });
    }

    try {
      const result = await new Promise((resolve, reject) => {
        // Set up one-time message handler for this screenshot request
        const messageHandler = (
          message: string | Buffer | ArrayBuffer | Buffer[]
        ) => {
          try {
            const response: ScreenshotMessage = JSON.parse(message.toString());

            if (response.type === "screenshot-error") {
              reject(new Error(response.error));
              return;
            }

            if (
              response.type === "screenshot-data" &&
              response.data &&
              response.path
            ) {
              // Remove the data:image/png;base64, prefix
              const base64Data = response.data.replace(
                /^data:image\/png;base64,/,
                ""
              );

              // Ensure the directory exists
              const dir = path.dirname(response.path);
              fs.mkdirSync(dir, { recursive: true });

              // Generate a unique filename using timestamp
              const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
              const filename = `screenshot-${timestamp}.png`;
              const fullPath = path.join(response.path, filename);

              // Write the file
              fs.writeFileSync(fullPath, base64Data, "base64");
              resolve({
                path: fullPath,
                filename: filename,
              });
            }
          } catch (error) {
            reject(error);
          } finally {
            this.activeConnection?.removeListener("message", messageHandler);
          }
        };

        // Add temporary message handler
        this.activeConnection?.on("message", messageHandler);

        // Request screenshot
        this.activeConnection?.send(
          JSON.stringify({ type: "take-screenshot" })
        );

        // Set timeout
        setTimeout(() => {
          this.activeConnection?.removeListener("message", messageHandler);
          reject(new Error("Screenshot timeout"));
        }, 30000); // 30 second timeout
      });

      res.json(result);
    } catch (error: unknown) {
      if (error instanceof Error) {
        res.status(500).json({ error: error.message });
      } else {
        res.status(500).json({ error: "An unknown error occurred" });
      }
    }
  }

  // Updated method to get URL for audits with improved connection tracking and waiting
  private async getUrlForAudit(): Promise<string | null> {
    try {
      console.log("getUrlForAudit called");

      // Use the stored URL if available immediately
      if (currentUrl && currentUrl !== "" && currentUrl !== "about:blank") {
        console.log(`Using existing URL immediately: ${currentUrl}`);
        return currentUrl;
      }

      // Wait for a URL to become available (retry loop)
      console.log("No valid URL available yet, waiting for navigation...");

      // Wait up to 10 seconds for a URL to be set (20 attempts x 500ms)
      const maxAttempts = 50;
      const waitTime = 500; // ms

      for (let attempt = 0; attempt < maxAttempts; attempt++) {
        // Check if URL is available now
        if (currentUrl && currentUrl !== "" && currentUrl !== "about:blank") {
          console.log(`URL became available after waiting: ${currentUrl}`);
          return currentUrl;
        }

        // Wait before checking again
        console.log(
          `Waiting for URL (attempt ${attempt + 1}/${maxAttempts})...`
        );
        await new Promise((resolve) => setTimeout(resolve, waitTime));
      }

      // If we reach here, no URL became available after waiting
      console.log("Timed out waiting for URL, returning null");
      return null;
    } catch (error) {
      console.error("Error in getUrlForAudit:", error);
      return null; // Return null to trigger an error
    }
  }

  // Public method to check if there's an active connection
  public hasActiveConnection(): boolean {
    return this.activeConnection !== null;
  }

  // Add new endpoint for programmatic screenshot capture
  async captureScreenshot(req: express.Request, res: express.Response) {
    console.log("Browser Connector: Starting captureScreenshot method");
    console.log("Browser Connector: Request headers:", req.headers);
    console.log("Browser Connector: Request method:", req.method);

    if (!this.activeConnection) {
      console.log(
        "Browser Connector: No active WebSocket connection to Chrome extension"
      );
      return res.status(503).json({ error: "Chrome extension not connected" });
    }

    try {
      console.log("Browser Connector: Starting screenshot capture...");
      const requestId = Date.now().toString();
      console.log("Browser Connector: Generated requestId:", requestId);

      // Create promise that will resolve when we get the screenshot data
      const screenshotPromise = new Promise<{
        data: string;
        path?: string;
        autoPaste?: boolean;
      }>((resolve, reject) => {
        console.log(
          `Browser Connector: Setting up screenshot callback for requestId: ${requestId}`
        );
        // Store callback in map
        screenshotCallbacks.set(requestId, { resolve, reject });
        console.log(
          "Browser Connector: Current callbacks:",
          Array.from(screenshotCallbacks.keys())
        );

        // Set timeout to clean up if we don't get a response
        setTimeout(() => {
          if (screenshotCallbacks.has(requestId)) {
            console.log(
              `Browser Connector: Screenshot capture timed out for requestId: ${requestId}`
            );
            screenshotCallbacks.delete(requestId);
            reject(
              new Error(
                "Screenshot capture timed out - no response from Chrome extension"
              )
            );
          }
        }, 10000);
      });

      // Send screenshot request to extension
      const message = JSON.stringify({
        type: "take-screenshot",
        requestId: requestId,
      });
      console.log(
        `Browser Connector: Sending WebSocket message to extension:`,
        message
      );
      this.activeConnection.send(message);

      // Wait for screenshot data
      console.log("Browser Connector: Waiting for screenshot data...");
      const {
        data: base64Data,
        path: customPath,
        autoPaste,
      } = await screenshotPromise;
      console.log("Browser Connector: Received screenshot data, saving...");
      console.log("Browser Connector: Custom path from extension:", customPath);
      console.log("Browser Connector: Auto-paste setting:", autoPaste);

      // Always prioritize the path from the Chrome extension
      let targetPath = customPath;

      // If no path provided by extension, fall back to defaults
      if (!targetPath) {
        targetPath =
          currentSettings.screenshotPath || getDefaultDownloadsFolder();
      }

      // Convert the path for the current platform
      targetPath = convertPathForCurrentPlatform(targetPath);

      console.log(`Browser Connector: Using path: ${targetPath}`);

      if (!base64Data) {
        throw new Error("No screenshot data received from Chrome extension");
      }

      try {
        fs.mkdirSync(targetPath, { recursive: true });
        console.log(`Browser Connector: Created directory: ${targetPath}`);
      } catch (err) {
        console.error(
          `Browser Connector: Error creating directory: ${targetPath}`,
          err
        );
        throw new Error(
          `Failed to create screenshot directory: ${
            err instanceof Error ? err.message : String(err)
          }`
        );
      }

      const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
      const filename = `screenshot-${timestamp}.png`;
      const fullPath = path.join(targetPath, filename);
      console.log(`Browser Connector: Full screenshot path: ${fullPath}`);

      // Remove the data:image/png;base64, prefix if present
      const cleanBase64 = base64Data.replace(/^data:image\/png;base64,/, "");

      // Save the file
      try {
        fs.writeFileSync(fullPath, cleanBase64, "base64");
        console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);
      } catch (err) {
        console.error(
          `Browser Connector: Error saving screenshot to: ${fullPath}`,
          err
        );
        throw new Error(
          `Failed to save screenshot: ${
            err instanceof Error ? err.message : String(err)
          }`
        );
      }

      // Check if running on macOS before executing AppleScript
      if (os.platform() === "darwin" && autoPaste === true) {
        console.log(
          "Browser Connector: Running on macOS with auto-paste enabled, executing AppleScript to paste into Cursor"
        );

        // Create the AppleScript to copy the image to clipboard and paste into Cursor
        // This version is more robust and includes debugging
        const appleScript = `
          -- Set path to the screenshot
          set imagePath to "${fullPath}"
          
          -- Copy the image to clipboard
          try
            set the clipboard to (read (POSIX file imagePath) as «class PNGf»)
          on error errMsg
            log "Error copying image to clipboard: " & errMsg
            return "Failed to copy image to clipboard: " & errMsg
          end try
          
          -- Activate Cursor application
          try
            tell application "Cursor"
              activate
            end tell
          on error errMsg
            log "Error activating Cursor: " & errMsg
            return "Failed to activate Cursor: " & errMsg
          end try
          
          -- Wait for the application to fully activate
          delay 3
          
          -- Try to interact with Cursor
          try
            tell application "System Events"
              tell process "Cursor"
                -- Get the frontmost window
                if (count of windows) is 0 then
                  return "No windows found in Cursor"
                end if
                
                set cursorWindow to window 1
                
                -- Try Method 1: Look for elements of class "Text Area"
                set foundElements to {}
                
                -- Try different selectors to find the text input area
                try
                  -- Try with class
                  set textAreas to UI elements of cursorWindow whose class is "Text Area"
                  if (count of textAreas) > 0 then
                    set foundElements to textAreas
                  end if
                end try
                
                if (count of foundElements) is 0 then
                  try
                    -- Try with AXTextField role
                    set textFields to UI elements of cursorWindow whose role is "AXTextField"
                    if (count of textFields) > 0 then
                      set foundElements to textFields
                    end if
                  end try
                end if
                
                if (count of foundElements) is 0 then
                  try
                    -- Try with AXTextArea role in nested elements
                    set allElements to UI elements of cursorWindow
                    repeat with anElement in allElements
                      try
                        set childElements to UI elements of anElement
                        repeat with aChild in childElements
                          try
                            if role of aChild is "AXTextArea" or role of aChild is "AXTextField" then
                              set end of foundElements to aChild
                            end if
                          end try
                        end repeat
                      end try
                    end repeat
                  end try
                end if
                
                -- If no elements found with specific attributes, try a broader approach
                if (count of foundElements) is 0 then
                  -- Just try to use the Command+V shortcut on the active window
                   -- This assumes Cursor already has focus on the right element
                    keystroke "v" using command down
                    delay 1
                    keystroke "here is the screenshot"
                    delay 1
                   -- Try multiple methods to press Enter
                   key code 36 -- Use key code for Return key
                   delay 0.5
                   keystroke return -- Use keystroke return as alternative
                   return "Used fallback method: Command+V on active window"
                else
                  -- We found a potential text input element
                  set inputElement to item 1 of foundElements
                  
                  -- Try to focus and paste
                  try
                    set focused of inputElement to true
                    delay 0.5
                    
                    -- Paste the image
                    keystroke "v" using command down
                    delay 1
                    
                    -- Type the text
                    keystroke "here is the screenshot"
                    delay 1
                    -- Try multiple methods to press Enter
                    key code 36 -- Use key code for Return key
                    delay 0.5
                    keystroke return -- Use keystroke return as alternative
                    return "Successfully pasted screenshot into Cursor text element"
                  on error errMsg
                    log "Error interacting with found element: " & errMsg
                    -- Fallback to just sending the key commands
                    keystroke "v" using command down
                    delay 1
                    keystroke "here is the screenshot"
                    delay 1
                    -- Try multiple methods to press Enter
                    key code 36 -- Use key code for Return key
                    delay 0.5
                    keystroke return -- Use keystroke return as alternative
                    return "Used fallback after element focus error: " & errMsg
                  end try
                end if
              end tell
            end tell
          on error errMsg
            log "Error in System Events block: " & errMsg
            return "Failed in System Events: " & errMsg
          end try
        `;

        // Execute the AppleScript
        exec(`osascript -e '${appleScript}'`, (error, stdout, stderr) => {
          if (error) {
            console.error(
              `Browser Connector: Error executing AppleScript: ${error.message}`
            );
            console.error(`Browser Connector: stderr: ${stderr}`);
            // Don't fail the response; log the error and proceed
          } else {
            console.log(`Browser Connector: AppleScript executed successfully`);
            console.log(`Browser Connector: stdout: ${stdout}`);
          }
        });
      } else {
        if (os.platform() === "darwin" && !autoPaste) {
          console.log(
            `Browser Connector: Running on macOS but auto-paste is disabled, skipping AppleScript execution`
          );
        } else {
          console.log(
            `Browser Connector: Not running on macOS, skipping AppleScript execution`
          );
        }
      }

      res.json({
        path: fullPath,
        filename: filename,
      });
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : String(error);
      console.error(
        "Browser Connector: Error capturing screenshot:",
        errorMessage
      );
      res.status(500).json({
        error: errorMessage,
      });
    }
  }

  // Add shutdown method
  public shutdown() {
    return new Promise<void>((resolve) => {
      console.log("Shutting down WebSocket server...");

      // Send close message to client if connection is active
      if (
        this.activeConnection &&
        this.activeConnection.readyState === WebSocket.OPEN
      ) {
        console.log("Notifying client to close connection...");
        try {
          this.activeConnection.send(
            JSON.stringify({ type: "server-shutdown" })
          );
        } catch (err) {
          console.error("Error sending shutdown message to client:", err);
        }
      }

      // Set a timeout to force close after 2 seconds
      const forceCloseTimeout = setTimeout(() => {
        console.log("Force closing connections after timeout...");
        if (this.activeConnection) {
          this.activeConnection.terminate(); // Force close the connection
          this.activeConnection = null;
        }
        this.wss.close();
        resolve();
      }, 2000);

      // Close active WebSocket connection if exists
      if (this.activeConnection) {
        this.activeConnection.close(1000, "Server shutting down");
        this.activeConnection = null;
      }

      // Close WebSocket server
      this.wss.close(() => {
        clearTimeout(forceCloseTimeout);
        console.log("WebSocket server closed gracefully");
        resolve();
      });
    });
  }

  // Sets up the accessibility audit endpoint
  private setupAccessibilityAudit() {
    this.setupAuditEndpoint(
      AuditCategory.ACCESSIBILITY,
      "/accessibility-audit",
      runAccessibilityAudit
    );
  }

  // Sets up the performance audit endpoint
  private setupPerformanceAudit() {
    this.setupAuditEndpoint(
      AuditCategory.PERFORMANCE,
      "/performance-audit",
      runPerformanceAudit
    );
  }

  // Set up SEO audit endpoint
  private setupSEOAudit() {
    this.setupAuditEndpoint(AuditCategory.SEO, "/seo-audit", runSEOAudit);
  }

  // Add a setup method for Best Practices audit
  private setupBestPracticesAudit() {
    this.setupAuditEndpoint(
      AuditCategory.BEST_PRACTICES,
      "/best-practices-audit",
      runBestPracticesAudit
    );
  }

  /**
   * Generic method to set up an audit endpoint
   * @param auditType The type of audit (accessibility, performance, SEO)
   * @param endpoint The endpoint path
   * @param auditFunction The audit function to call
   */
  private setupAuditEndpoint(
    auditType: string,
    endpoint: string,
    auditFunction: (url: string) => Promise<LighthouseReport>
  ) {
    // Add server identity validation endpoint
    this.app.get("/.identity", (req, res) => {
      res.json({
        signature: "mcp-browser-connector-24x7",
        version: "1.2.0",
      });
    });

    this.app.post(endpoint, async (req: any, res: any) => {
      try {
        console.log(`${auditType} audit request received`);

        // Get URL using our helper method
        const url = await this.getUrlForAudit();

        if (!url) {
          console.log(`No URL available for ${auditType} audit`);
          return res.status(400).json({
            error: `URL is required for ${auditType} audit. Make sure you navigate to a page in the browser first, and the browser-tool extension tab is open.`,
          });
        }

        // If we're using the stored URL (not from request body), log it now
        if (!req.body?.url && url === currentUrl) {
          console.log(`Using stored URL for ${auditType} audit:`, url);
        }

        // Check if we're using the default URL
        if (url === "about:blank") {
          console.log(`Cannot run ${auditType} audit on about:blank`);
          return res.status(400).json({
            error: `Cannot run ${auditType} audit on about:blank`,
          });
        }

        console.log(`Preparing to run ${auditType} audit for: ${url}`);

        // Run the audit using the provided function
        try {
          const result = await auditFunction(url);

          console.log(`${auditType} audit completed successfully`);
          // Return the results
          res.json(result);
        } catch (auditError) {
          console.error(`${auditType} audit failed:`, auditError);
          const errorMessage =
            auditError instanceof Error
              ? auditError.message
              : String(auditError);
          res.status(500).json({
            error: `Failed to run ${auditType} audit: ${errorMessage}`,
          });
        }
      } catch (error) {
        console.error(`Error in ${auditType} audit endpoint:`, error);
        const errorMessage =
          error instanceof Error ? error.message : String(error);
        res.status(500).json({
          error: `Error in ${auditType} audit endpoint: ${errorMessage}`,
        });
      }
    });
  }
}

// Use an async IIFE to allow for async/await in the initial setup
(async () => {
  try {
    console.log(`Starting Browser Tools Server...`);
    console.log(`Requested port: ${REQUESTED_PORT}`);

    // Find an available port
    try {
      PORT = await getAvailablePort(REQUESTED_PORT);

      if (PORT !== REQUESTED_PORT) {
        console.log(`\n====================================`);
        console.log(`NOTICE: Requested port ${REQUESTED_PORT} was in use.`);
        console.log(`Using port ${PORT} instead.`);
        console.log(`====================================\n`);
      }
    } catch (portError) {
      console.error(`Failed to find an available port:`, portError);
      process.exit(1);
    }

    // Create the server with the available port
    const server = app.listen(PORT, currentSettings.serverHost, () => {
      console.log(`\n=== Browser Tools Server Started ===`);
      console.log(
        `Aggregator listening on http://${currentSettings.serverHost}:${PORT}`
      );

      if (PORT !== REQUESTED_PORT) {
        console.log(
          `NOTE: Using fallback port ${PORT} instead of requested port ${REQUESTED_PORT}`
        );
      }

      // Log all available network interfaces for easier discovery
      const networkInterfaces = os.networkInterfaces();
      console.log("\nAvailable on the following network addresses:");

      Object.keys(networkInterfaces).forEach((interfaceName) => {
        const interfaces = networkInterfaces[interfaceName];
        if (interfaces) {
          interfaces.forEach((iface) => {
            if (!iface.internal && iface.family === "IPv4") {
              console.log(`  - http://${iface.address}:${PORT}`);
            }
          });
        }
      });

      console.log(`\nFor local access use: http://localhost:${PORT}`);
    });

    // Handle server startup errors
    server.on("error", (err: any) => {
      if (err.code === "EADDRINUSE") {
        console.error(
          `ERROR: Port ${PORT} is still in use, despite our checks!`
        );
        console.error(
          `This might indicate another process started using this port after our check.`
        );
      } else {
        console.error(`Server error:`, err);
      }
      process.exit(1);
    });

    // Initialize the browser connector with the existing app AND server
    const browserConnector = new BrowserConnector(app, server);

    // Handle shutdown gracefully with improved error handling
    process.on("SIGINT", async () => {
      console.log("\nReceived SIGINT signal. Starting graceful shutdown...");

      try {
        // First shutdown WebSocket connections
        await browserConnector.shutdown();

        // Then close the HTTP server
        await new Promise<void>((resolve, reject) => {
          server.close((err) => {
            if (err) {
              console.error("Error closing HTTP server:", err);
              reject(err);
            } else {
              console.log("HTTP server closed successfully");
              resolve();
            }
          });
        });

        // Clear all logs
        clearAllLogs();

        console.log("Shutdown completed successfully");
        process.exit(0);
      } catch (error) {
        console.error("Error during shutdown:", error);
        // Force exit in case of error
        process.exit(1);
      }
    });

    // Also handle SIGTERM
    process.on("SIGTERM", () => {
      console.log("\nReceived SIGTERM signal");
      process.emit("SIGINT");
    });
  } catch (error) {
    console.error("Failed to start server:", error);
    process.exit(1);
  }
})().catch((err) => {
  console.error("Unhandled error during server startup:", err);
  process.exit(1);
});


================================================
FILE: browser-tools-server/lighthouse/accessibility.ts
================================================
import { Result as LighthouseResult } from "lighthouse";
import { AuditCategory, LighthouseReport } from "./types.js";
import { runLighthouseAudit } from "./index.js";

// === Accessibility Report Types ===

/**
 * Accessibility-specific report content structure
 */
export interface AccessibilityReportContent {
  score: number; // Overall score (0-100)
  audit_counts: {
    // Counts of different audit types
    failed: number;
    passed: number;
    manual: number;
    informative: number;
    not_applicable: number;
  };
  issues: AIAccessibilityIssue[];
  categories: {
    [category: string]: {
      score: number;
      issues_count: number;
    };
  };
  critical_elements: AIAccessibilityElement[];
  prioritized_recommendations?: string[]; // Ordered list of recommendations
}

/**
 * Full accessibility report implementing the base LighthouseReport interface
 */
export type AIOptimizedAccessibilityReport =
  LighthouseReport<AccessibilityReportContent>;

/**
 * AI-optimized accessibility issue
 */
interface AIAccessibilityIssue {
  id: string; // e.g., "color-contrast"
  title: string; // e.g., "Color contrast is sufficient"
  impact: "critical" | "serious" | "moderate" | "minor";
  category: string; // e.g., "contrast", "aria", "forms", "keyboard"
  elements?: AIAccessibilityElement[]; // Elements with issues
  score: number | null; // 0-1 or null
}

/**
 * Accessibility element with issues
 */
interface AIAccessibilityElement {
  selector: string; // CSS selector
  snippet?: string; // HTML snippet
  label?: string; // Element label
  issue_description?: string; // Description of the issue
  value?: string | number; // Current value (e.g., contrast ratio)
}

// Original interfaces for backward compatibility
interface AccessibilityAudit {
  id: string; // e.g., "color-contrast"
  title: string; // e.g., "Color contrast is sufficient"
  description: string; // e.g., "Ensures text is readable..."
  score: number | null; // 0-1 (normalized), null for manual/informative
  scoreDisplayMode: string; // e.g., "binary", "numeric", "manual"
  details?: AuditDetails; // Optional, structured details
  weight?: number; // Optional, audit weight for impact calculation
}

type AuditDetails = {
  items?: Array<{
    node?: {
      selector: string; // e.g., ".my-class"
      snippet?: string; // HTML snippet
      nodeLabel?: string; // e.g., "Modify logging size limits / truncation"
      explanation?: string; // Explanation of why the node fails the audit
    };
    value?: string | number; // Specific value (e.g., contrast ratio)
    explanation?: string; // Explanation at the item level
  }>;
  debugData?: string; // Optional, debug information
  [key: string]: any; // Flexible for other detail types (tables, etc.)
};

// Original limits were optimized for human consumption
// This ensures we always include critical issues while limiting less important ones
const DETAIL_LIMITS = {
  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues
  serious: 15, // Up to 15 items for serious issues
  moderate: 10, // Up to 10 items for moderate issues
  minor: 3, // Up to 3 items for minor issues
};

/**
 * Runs an accessibility audit on the specified URL
 * @param url The URL to audit
 * @returns Promise resolving to AI-optimized accessibility audit results
 */
export async function runAccessibilityAudit(
  url: string
): Promise<AIOptimizedAccessibilityReport> {
  try {
    const lhr = await runLighthouseAudit(url, [AuditCategory.ACCESSIBILITY]);
    return extractAIOptimizedData(lhr, url);
  } catch (error) {
    throw new Error(
      `Accessibility audit failed: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
}

/**
 * Extract AI-optimized accessibility data from Lighthouse results
 */
const extractAIOptimizedData = (
  lhr: LighthouseResult,
  url: string
): AIOptimizedAccessibilityReport => {
  const categoryData = lhr.categories[AuditCategory.ACCESSIBILITY];
  const audits = lhr.audits || {};

  // Add metadata
  const metadata = {
    url,
    timestamp: lhr.fetchTime || new Date().toISOString(),
    device: "desktop", // This could be made configurable
    lighthouseVersion: lhr.lighthouseVersion,
  };

  // Initialize variables
  const issues: AIAccessibilityIssue[] = [];
  const criticalElements: AIAccessibilityElement[] = [];
  const categories: {
    [category: string]: { score: number; issues_count: number };
  } = {};

  // Count audits by type
  let failedCount = 0;
  let passedCount = 0;
  let manualCount = 0;
  let informativeCount = 0;
  let notApplicableCount = 0;

  // Process audit refs
  const auditRefs = categoryData?.auditRefs || [];

  // First pass: count audits by type and initialize categories
  auditRefs.forEach((ref) => {
    const audit = audits[ref.id];
    if (!audit) return;

    // Count by scoreDisplayMode
    if (audit.scoreDisplayMode === "manual") {
      manualCount++;
    } else if (audit.scoreDisplayMode === "informative") {
      informativeCount++;
    } else if (audit.scoreDisplayMode === "notApplicable") {
      notApplicableCount++;
    } else if (audit.score !== null) {
      // Binary pass/fail
      if (audit.score >= 0.9) {
        passedCount++;
      } else {
        failedCount++;
      }
    }

    // Process categories
    if (ref.group) {
      // Initialize category if not exists
      if (!categories[ref.group]) {
        categories[ref.group] = { score: 0, issues_count: 0 };
      }

      // Update category score and issues count
      if (audit.score !== null && audit.score < 0.9) {
        categories[ref.group].issues_count++;
      }
    }
  });

  // Second pass: process failed audits into AI-friendly format
  auditRefs
    .filter((ref) => {
      const audit = audits[ref.id];
      return audit && audit.score !== null && audit.score < 0.9;
    })
    .sort((a, b) => (b.weight || 0) - (a.weight || 0))
    // No limit on number of failed audits - we'll show them all
    .forEach((ref) => {
      const audit = audits[ref.id];

      // Determine impact level based on score and weight
      let impact: "critical" | "serious" | "moderate" | "minor" = "moderate";
      if (audit.score === 0) {
        impact = "critical";
      } else if (audit.score !== null && audit.score <= 0.5) {
        impact = "serious";
      } else if (audit.score !== null && audit.score > 0.7) {
        impact = "minor";
      }

      // Create elements array
      const elements: AIAccessibilityElement[] = [];

      if (audit.details) {
        const details = audit.details as any;
        if (details.items && Array.isArray(details.items)) {
          const items = details.items;
          // Apply limits based on impact level
          const itemLimit = DETAIL_LIMITS[impact];
          items.slice(0, itemLimit).forEach((item: any) => {
            if (item.node) {
              const element: AIAccessibilityElement = {
                selector: item.node.selector,
                snippet: item.node.snippet,
                label: item.node.nodeLabel,
                issue_description: item.node.explanation || item.explanation,
              };

              if (item.value !== undefined) {
                element.value = item.value;
              }

              elements.push(element);

              // Add to critical elements if impact is critical or serious
              if (impact === "critical" || impact === "serious") {
                criticalElements.push(element);
              }
            }
          });
        }
      }

      // Create the issue
      const issue: AIAccessibilityIssue = {
        id: ref.id,
        title: audit.title,
        impact,
        category: ref.group || "other",
        elements: elements.length > 0 ? elements : undefined,
        score: audit.score,
      };

      issues.push(issue);
    });

  // Calculate overall score
  const score = Math.round((categoryData?.score || 0) * 100);

  // Generate prioritized recommendations
  const prioritized_recommendations: string[] = [];

  // Add category-specific recommendations
  Object.entries(categories)
    .filter(([_, data]) => data.issues_count > 0)
    .sort(([_, a], [__, b]) => b.issues_count - a.issues_count)
    .forEach(([category, data]) => {
      let recommendation = "";

      switch (category) {
        case "a11y-color-contrast":
          recommendation = "Improve color contrast for better readability";
          break;
        case "a11y-names-labels":
          recommendation = "Add proper labels to all interactive elements";
          break;
        case "a11y-aria":
          recommendation = "Fix ARIA attributes and roles";
          break;
        case "a11y-navigation":
          recommendation = "Improve keyboard navigation and focus management";
          break;
        case "a11y-language":
          recommendation = "Add proper language attributes to HTML";
          break;
        case "a11y-tables-lists":
          recommendation = "Fix table and list structures for screen readers";
          break;
        default:
          recommendation = `Fix ${data.issues_count} issues in ${category}`;
      }

      prioritized_recommendations.push(recommendation);
    });

  // Add specific high-impact recommendations
  if (issues.some((issue) => issue.id === "color-contrast")) {
    prioritized_recommendations.push(
      "Fix low contrast text for better readability"
    );
  }

  if (issues.some((issue) => issue.id === "document-title")) {
    prioritized_recommendations.push("Add a descriptive page title");
  }

  if (issues.some((issue) => issue.id === "image-alt")) {
    prioritized_recommendations.push("Add alt text to all images");
  }

  // Create the report content
  const reportContent: AccessibilityReportContent = {
    score,
    audit_counts: {
      failed: failedCount,
      passed: passedCount,
      manual: manualCount,
      informative: informativeCount,
      not_applicable: notApplicableCount,
    },
    issues,
    categories,
    critical_elements: criticalElements,
    prioritized_recommendations:
      prioritized_recommendations.length > 0
        ? prioritized_recommendations
        : undefined,
  };

  // Return the full report following the LighthouseReport interface
  return {
    metadata,
    report: reportContent,
  };
};


================================================
FILE: browser-tools-server/lighthouse/best-practices.ts
================================================
import { Result as LighthouseResult } from "lighthouse";
import { AuditCategory, LighthouseReport } from "./types.js";
import { runLighthouseAudit } from "./index.js";

// === Best Practices Report Types ===

/**
 * Best Practices-specific report content structure
 */
export interface BestPracticesReportContent {
  score: number; // Overall score (0-100)
  audit_counts: {
    // Counts of different audit types
    failed: number;
    passed: number;
    manual: number;
    informative: number;
    not_applicable: number;
  };
  issues: AIBestPracticesIssue[];
  categories: {
    [category: string]: {
      score: number;
      issues_count: number;
    };
  };
  prioritized_recommendations?: string[]; // Ordered list of recommendations
}

/**
 * Full Best Practices report implementing the base LighthouseReport interface
 */
export type AIOptimizedBestPracticesReport =
  LighthouseReport<BestPracticesReportContent>;

/**
 * AI-optimized Best Practices issue
 */
interface AIBestPracticesIssue {
  id: string; // e.g., "js-libraries"
  title: string; // e.g., "Detected JavaScript libraries"
  impact: "critical" | "serious" | "moderate" | "minor";
  category: string; // e.g., "security", "trust", "user-experience", "browser-compat"
  details?: {
    name?: string; // Name of the item (e.g., library name, vulnerability)
    version?: string; // Version information if applicable
    value?: string; // Current value or status
    issue?: string; // Description of the issue
  }[];
  score: number | null; // 0-1 or null
}

// Original interfaces for backward compatibility
interface BestPracticesAudit {
  id: string;
  title: string;
  description: string;
  score: number | null;
  scoreDisplayMode: string;
  details?: BestPracticesAuditDetails;
}

interface BestPracticesAuditDetails {
  items?: Array<Record<string, unknown>>;
  type?: string; // e.g., "table"
}

// This ensures we always include critical issues while limiting less important ones
const DETAIL_LIMITS: Record<string, number> = {
  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues
  serious: 15, // Up to 15 items for serious issues
  moderate: 10, // Up to 10 items for moderate issues
  minor: 3, // Up to 3 items for minor issues
};

/**
 * Runs a Best Practices audit on the specified URL
 * @param url The URL to audit
 * @returns Promise resolving to AI-optimized Best Practices audit results
 */
export async function runBestPracticesAudit(
  url: string
): Promise<AIOptimizedBestPracticesReport> {
  try {
    const lhr = await runLighthouseAudit(url, [AuditCategory.BEST_PRACTICES]);
    return extractAIOptimizedData(lhr, url);
  } catch (error) {
    throw new Error(
      `Best Practices audit failed: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
}

/**
 * Extract AI-optimized Best Practices data from Lighthouse results
 */
const extractAIOptimizedData = (
  lhr: LighthouseResult,
  url: string
): AIOptimizedBestPracticesReport => {
  const categoryData = lhr.categories[AuditCategory.BEST_PRACTICES];
  const audits = lhr.audits || {};

  // Add metadata
  const metadata = {
    url,
    timestamp: lhr.fetchTime || new Date().toISOString(),
    device: lhr.configSettings?.formFactor || "desktop",
    lighthouseVersion: lhr.lighthouseVersion || "unknown",
  };

  // Process audit results
  const issues: AIBestPracticesIssue[] = [];
  const categories: { [key: string]: { score: number; issues_count: number } } =
    {
      security: { score: 0, issues_count: 0 },
      trust: { score: 0, issues_count: 0 },
      "user-experience": { score: 0, issues_count: 0 },
      "browser-compat": { score: 0, issues_count: 0 },
      other: { score: 0, issues_count: 0 },
    };

  // Counters for audit types
  let failedCount = 0;
  let passedCount = 0;
  let manualCount = 0;
  let informativeCount = 0;
  let notApplicableCount = 0;

  // Process failed audits (score < 1)
  const failedAudits = Object.entries(audits)
    .filter(([, audit]) => {
      const score = audit.score;
      return (
        score !== null &&
        score < 1 &&
        audit.scoreDisplayMode !== "manual" &&
        audit.scoreDisplayMode !== "notApplicable"
      );
    })
    .map(([auditId, audit]) => ({ auditId, ...audit }));

  // Update counters
  Object.values(audits).forEach((audit) => {
    const { score, scoreDisplayMode } = audit;

    if (scoreDisplayMode === "manual") {
      manualCount++;
    } else if (scoreDisplayMode === "informative") {
      informativeCount++;
    } else if (scoreDisplayMode === "notApplicable") {
      notApplicableCount++;
    } else if (score === 1) {
      passedCount++;
    } else if (score !== null && score < 1) {
      failedCount++;
    }
  });

  // Process failed audits into AI-friendly format
  failedAudits.forEach((ref: any) => {
    // Determine impact level based on audit score and weight
    let impact: "critical" | "serious" | "moderate" | "minor" = "moderate";
    const score = ref.score || 0;

    // Use a more reliable approach to determine impact
    if (score === 0) {
      impact = "critical";
    } else if (score < 0.5) {
      impact = "serious";
    } else if (score < 0.9) {
      impact = "moderate";
    } else {
      impact = "minor";
    }

    // Categorize the issue
    let category = "other";

    // Security-related issues
    if (
      ref.auditId.includes("csp") ||
      ref.auditId.includes("security") ||
      ref.auditId.includes("vulnerab") ||
      ref.auditId.includes("password") ||
      ref.auditId.includes("cert") ||
      ref.auditId.includes("deprecat")
    ) {
      category = "security";
    }
    // Trust and legitimacy issues
    else if (
      ref.auditId.includes("doctype") ||
      ref.auditId.includes("charset") ||
      ref.auditId.includes("legit") ||
      ref.auditId.includes("trust")
    ) {
      category = "trust";
    }
    // User experience issues
    else if (
      ref.auditId.includes("user") ||
      ref.auditId.includes("experience") ||
      ref.auditId.includes("console") ||
      ref.auditId.includes("errors") ||
      ref.auditId.includes("paste")
    ) {
      category = "user-experience";
    }
    // Browser compatibility issues
    else if (
      ref.auditId.includes("compat") ||
      ref.auditId.includes("browser") ||
      ref.auditId.includes("vendor") ||
      ref.auditId.includes("js-lib")
    ) {
      category = "browser-compat";
    }

    // Count issues by category
    categories[category].issues_count++;

    // Create issue object
    const issue: AIBestPracticesIssue = {
      id: ref.auditId,
      title: ref.title,
      impact,
      category,
      score: ref.score,
      details: [],
    };

    // Extract details if available
    const refDetails = ref.details as BestPracticesAuditDetails | undefined;
    if (refDetails?.items && Array.isArray(refDetails.items)) {
      const itemLimit = DETAIL_LIMITS[impact];
      const detailItems = refDetails.items.slice(0, itemLimit);

      detailItems.forEach((item: Record<string, unknown>) => {
        issue.details = issue.details || [];

        // Different audits have different detail structures
        const detail: Record<string, string> = {};

        if (typeof item.name === "string") detail.name = item.name;
        if (typeof item.version === "string") detail.version = item.version;
        if (typeof item.issue === "string") detail.issue = item.issue;
        if (item.value !== undefined) detail.value = String(item.value);

        // For JS libraries, extract name and version
        if (
          ref.auditId === "js-libraries" &&
          typeof item.name === "string" &&
          typeof item.version === "string"
        ) {
          detail.name = item.name;
          detail.version = item.version;
        }

        // Add other generic properties that might exist
        for (const [key, value] of Object.entries(item)) {
          if (!detail[key] && typeof value === "string") {
            detail[key] = value;
          }
        }

        issue.details.push(detail as any);
      });
    }

    issues.push(issue);
  });

  // Calculate category scores (0-100)
  Object.keys(categories).forEach((category) => {
    // Simplified scoring: if there are issues in this category, score is reduced proportionally
    const issueCount = categories[category].issues_count;
    if (issueCount > 0) {
      // More issues = lower score, max penalty of 25 points per issue
      const penalty = Math.min(100, issueCount * 25);
      categories[category].score = Math.max(0, 100 - penalty);
    } else {
      categories[category].score = 100;
    }
  });

  // Generate prioritized recommendations
  const prioritized_recommendations: string[] = [];

  // Prioritize recommendations by category with most issues
  Object.entries(categories)
    .filter(([_, data]) => data.issues_count > 0)
    .sort(([_, a], [__, b]) => b.issues_count - a.issues_count)
    .forEach(([category, data]) => {
      let recommendation = "";

      switch (category) {
        case "security":
          recommendation = `Address ${data.issues_count} security issues: vulnerabilities, CSP, deprecations`;
          break;
        case "trust":
          recommendation = `Fix ${data.issues_count} trust & legitimacy issues: doctype, charset`;
          break;
        case "user-experience":
          recommendation = `Improve ${data.issues_count} user experience issues: console errors, user interactions`;
          break;
        case "browser-compat":
          recommendation = `Resolve ${data.issues_count} browser compatibility issues: outdated libraries, vendor prefixes`;
          break;
        default:
          recommendation = `Fix ${data.issues_count} other best practice issues`;
      }

      prioritized_recommendations.push(recommendation);
    });

  // Return the optimized report
  return {
    metadata,
    report: {
      score: categoryData?.score ? Math.round(categoryData.score * 100) : 0,
      audit_counts: {
        failed: failedCount,
        passed: passedCount,
        manual: manualCount,
        informative: informativeCount,
        not_applicable: notApplicableCount,
      },
      issues,
      categories,
      prioritized_recommendations,
    },
  };
};


================================================
FILE: browser-tools-server/lighthouse/index.ts
================================================
import lighthouse from "lighthouse";
import type { Result as LighthouseResult, Flags } from "lighthouse";
import {
  connectToHeadlessBrowser,
  scheduleBrowserCleanup,
} from "../puppeteer-service.js";
import { LighthouseConfig, AuditCategory } from "./types.js";

/**
 * Creates a Lighthouse configuration object
 * @param categories Array of categories to audit
 * @returns Lighthouse configuration and flags
 */
export function createLighthouseConfig(
  categories: string[] = [AuditCategory.ACCESSIBILITY]
): LighthouseConfig {
  return {
    flags: {
      output: ["json"],
      onlyCategories: categories,
      formFactor: "desktop",
      port: undefined as number | undefined,
      screenEmulation: {
        mobile: false,
        width: 1350,
        height: 940,
        deviceScaleFactor: 1,
        disabled: false,
      },
    },
    config: {
      extends: "lighthouse:default",
      settings: {
        onlyCategories: categories,
        emulatedFormFactor: "desktop",
        throttling: { cpuSlowdownMultiplier: 1 },
      },
    },
  };
}

/**
 * Runs a Lighthouse audit on the specified URL via CDP
 * @param url The URL to audit
 * @param categories Array of categories to audit, defaults to ["accessibility"]
 * @returns Promise resolving to the Lighthouse result
 * @throws Error if the URL is invalid or if the audit fails
 */
export async function runLighthouseAudit(
  url: string,
  categories: string[]
): Promise<LighthouseResult> {
  console.log(`Starting Lighthouse ${categories.join(", ")} audit for: ${url}`);

  if (!url || url === "about:blank") {
    console.error("Invalid URL for Lighthouse audit");
    throw new Error(
      "Cannot run audit on an empty page or about:blank. Please navigate to a valid URL first."
    );
  }

  try {
    // Always use a dedicated headless browser for audits
    console.log("Using dedicated headless browser for audit");

    // Determine if this is a performance audit - we need to load all resources for performance audits
    const isPerformanceAudit = categories.includes(AuditCategory.PERFORMANCE);

    // For performance audits, we want to load all resources
    // For accessibility or other audits, we can block non-essential resources
    try {
      const { port } = await connectToHeadlessBrowser(url, {
        blockResources: !isPerformanceAudit,
      });

      console.log(`Connected to browser on port: ${port}`);

      // Create Lighthouse config
      const { flags, config } = createLighthouseConfig(categories);
      flags.port = port;

      console.log(
        `Running Lighthouse with categories: ${categories.join(", ")}`
      );
      const runnerResult = await lighthouse(url, flags as Flags, config);
      console.log("Lighthouse scan completed");

      if (!runnerResult?.lhr) {
        console.error("Lighthouse audit failed to produce results");
        throw new Error("Lighthouse audit failed to produce results");
      }

      // Schedule browser cleanup after a delay to allow for subsequent audits
      scheduleBrowserCleanup();

      // Return the result
      const result = runnerResult.lhr;

      return result;
    } catch (browserError) {
      // Check if the error is related to Chrome/Edge not being available
      const errorMessage =
        browserError instanceof Error
          ? browserError.message
          : String(browserError);
      if (
        errorMessage.includes("Chrome could not be found") ||
        errorMessage.includes("Failed to launch browser") ||
        errorMessage.includes("spawn ENOENT")
      ) {
        throw new Error(
          "Chrome or Edge browser could not be found. Please ensure that Chrome or Edge is installed on your system to run audits."
        );
      }
      // Re-throw other errors
      throw browserError;
    }
  } catch (error) {
    console.error("Lighthouse audit failed:", error);
    // Schedule browser cleanup even if the audit fails
    scheduleBrowserCleanup();
    throw new Error(
      `Lighthouse audit failed: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
}

// Export from specific audit modules
export * from "./accessibility.js";
export * from "./performance.js";
export * from "./seo.js";
export * from "./types.js";


================================================
FILE: browser-tools-server/lighthouse/performance.ts
================================================
import { Result as LighthouseResult } from "lighthouse";
import { AuditCategory, LighthouseReport } from "./types.js";
import { runLighthouseAudit } from "./index.js";

// === Performance Report Types ===

/**
 * Performance-specific report content structure
 */
export interface PerformanceReportContent {
  score: number; // Overall score (0-100)
  audit_counts: {
    // Counts of different audit types
    failed: number;
    passed: number;
    manual: number;
    informative: number;
    not_applicable: number;
  };
  metrics: AIOptimizedMetric[];
  opportunities: AIOptimizedOpportunity[];
  page_stats?: AIPageStats; // Optional page statistics
  prioritized_recommendations?: string[]; // Ordered list of recommendations
}

/**
 * Full performance report implementing the base LighthouseReport interface
 */
export type AIOptimizedPerformanceReport =
  LighthouseReport<PerformanceReportContent>;

// AI-optimized performance metric format
interface AIOptimizedMetric {
  id: string; // Short ID like "lcp", "fcp"
  score: number | null; // 0-1 score
  value_ms: number; // Value in milliseconds
  element_type?: string; // For LCP: "image", "text", etc.
  element_selector?: string; // DOM selector for the element
  element_url?: string; // For images/videos
  element_content?: string; // For text content (truncated)
  passes_core_web_vital?: boolean; // Whether this metric passes as a Core Web Vital
}

// AI-optimized opportunity format
interface AIOptimizedOpportunity {
  id: string; // Like "render_blocking", "http2"
  savings_ms: number; // Time savings in ms
  severity?: "critical" | "serious" | "moderate" | "minor"; // Severity classification
  resources: Array<{
    url: string; // Resource URL
    savings_ms?: number; // Individual resource savings
    size_kb?: number; // Size in KB
    type?: string; // Resource type (js, css, img, etc.)
    is_third_party?: boolean; // Whether this is a third-party resource
  }>;
}

// Page stats for AI analysis
interface AIPageStats {
  total_size_kb: number; // Total page weight in KB
  total_requests: number; // Total number of requests
  resource_counts: {
    // Count by resource type
    js: number;
    css: number;
    img: number;
    font: number;
    other: number;
  };
  third_party_size_kb: number; // Size of third-party resources
  main_thread_blocking_time_ms: number; // Time spent blocking the main thread
}

// This ensures we always include critical issues while limiting less important ones
const DETAIL_LIMITS = {
  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues
  serious: 15, // Up to 15 items for serious issues
  moderate: 10, // Up to 10 items for moderate issues
  minor: 3, // Up to 3 items for minor issues
};

/**
 * Performance audit adapted for AI consumption
 * This format is optimized for AI agents with:
 * - Concise, relevant information without redundant descriptions
 * - Key metrics and opportunities clearly structured
 * - Only actionable data that an AI can use for recommendations
 */
export async function runPerformanceAudit(
  url: string
): Promise<AIOptimizedPerformanceReport> {
  try {
    const lhr = await runLighthouseAudit(url, [AuditCategory.PERFORMANCE]);
    return extractAIOptimizedData(lhr, url);
  } catch (error) {
    throw new Error(
      `Performance audit failed: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
}

/**
 * Extract AI-optimized performance data from Lighthouse results
 */
const extractAIOptimizedData = (
  lhr: LighthouseResult,
  url: string
): AIOptimizedPerformanceReport => {
  const audits = lhr.audits || {};
  const categoryData = lhr.categories[AuditCategory.PERFORMANCE];
  const score = Math.round((categoryData?.score || 0) * 100);

  // Add metadata
  const metadata = {
    url,
    timestamp: lhr.fetchTime || new Date().toISOString(),
    device: "desktop", // This could be made configurable
    lighthouseVersion: lhr.lighthouseVersion,
  };

  // Count audits by type
  const auditRefs = categoryData?.auditRefs || [];
  let failedCount = 0;
  let passedCount = 0;
  let manualCount = 0;
  let informativeCount = 0;
  let notApplicableCount = 0;

  auditRefs.forEach((ref) => {
    const audit = audits[ref.id];
    if (!audit) return;

    if (audit.scoreDisplayMode === "manual") {
      manualCount++;
    } else if (audit.scoreDisplayMode === "informative") {
      informativeCount++;
    } else if (audit.scoreDisplayMode === "notApplicable") {
      notApplicableCount++;
    } else if (audit.score !== null) {
      if (audit.score >= 0.9) {
        passedCount++;
      } else {
        failedCount++;
      }
    }
  });

  const audit_counts = {
    failed: failedCount,
    passed: passedCount,
    manual: manualCount,
    informative: informativeCount,
    not_applicable: notApplicableCount,
  };

  const metrics: AIOptimizedMetric[] = [];
  const opportunities: AIOptimizedOpportunity[] = [];

  // Extract core metrics
  if (audits["largest-contentful-paint"]) {
    const lcp = audits["largest-contentful-paint"];
    const lcpElement = audits["largest-contentful-paint-element"];

    const metric: AIOptimizedMetric = {
      id: "lcp",
      score: lcp.score,
      value_ms: Math.round(lcp.numericValue || 0),
      passes_core_web_vital: lcp.score !== null && lcp.score >= 0.9,
    };

    // Enhanced LCP element detection

    // 1. Try from largest-contentful-paint-element audit
    if (lcpElement && lcpElement.details) {
      const lcpDetails = lcpElement.details as any;

      // First attempt - try to get directly from items
      if (
        lcpDetails.items &&
        Array.isArray(lcpDetails.items) &&
        lcpDetails.items.length > 0
      ) {
        const item = lcpDetails.items[0];

        // For text elements in tables format
        if (item.type === "table" && item.items && item.items.length > 0) {
          const firstTableItem = item.items[0];

          if (firstTableItem.node) {
            if (firstTableItem.node.selector) {
              metric.element_selector = firstTableItem.node.selector;
            }

            // Determine element type based on path or selector
            const path = firstTableItem.node.path;
            const selector = firstTableItem.node.selector || "";

            if (path) {
              if (
                selector.includes(" > img") ||
                selector.includes(" img") ||
                selector.endsWith("img") ||
                path.includes(",IMG")
              ) {
                metric.element_type = "image";

                // Try to extract image name from selector
                const imgMatch = selector.match(/img[.][^> ]+/);
                if (imgMatch && !metric.element_url) {
                  metric.element_url = imgMatch[0];
                }
              } else if (
                path.includes(",SPAN") ||
                path.includes(",P") ||
                path.includes(",H")
              ) {
                metric.element_type = "text";
              }
            }

            // Try to extract text content if available
            if (firstTableItem.node.nodeLabel) {
              metric.element_content = firstTableItem.node.nodeLabel.substring(
                0,
                100
              );
            }
          }
        }
        // Original handling for direct items
        else if (item.node?.nodeLabel) {
          // Determine element type from node label
          if (item.node.nodeLabel.startsWith("<img")) {
            metric.element_type = "image";
            // Try to extract image URL from the node snippet
            const match = item.node.snippet?.match(/src="([^"]+)"/);
            if (match && match[1]) {
              metric.element_url = match[1];
            }
          } else if (item.node.nodeLabel.startsWith("<video")) {
            metric.element_type = "video";
          } else if (item.node.nodeLabel.startsWith("<h")) {
            metric.element_type = "heading";
          } else {
            metric.element_type = "text";
          }

          if (item.node?.selector) {
            metric.element_selector = item.node.selector;
          }
        }
      }
    }

    // 2. Try from lcp-lazy-loaded audit
    const lcpImageAudit = audits["lcp-lazy-loaded"];
    if (lcpImageAudit && lcpImageAudit.details) {
      const lcpImageDetails = lcpImageAudit.details as any;

      if (
        lcpImageDetails.items &&
        Array.isArray(lcpImageDetails.items) &&
        lcpImageDetails.items.length > 0
      ) {
        const item = lcpImageDetails.items[0];

        if (item.url) {
          metric.element_type = "image";
          metric.element_url = item.url;
        }
      }
    }

    // 3. Try directly from the LCP audit details
    if (!metric.element_url && lcp.details) {
      const lcpDirectDetails = lcp.details as any;

      if (lcpDirectDetails.items && Array.isArray(lcpDirectDetails.items)) {
        for (const item of lcpDirectDetails.items) {
          if (item.url || (item.node && item.node.path)) {
            if (item.url) {
              metric.element_url = item.url;
              metric.element_type = item.url.match(
                /\.(jpg|jpeg|png|gif|webp|svg)$/i
              )
                ? "image"
                : "resource";
            }
            if (item.node && item.node.selector) {
              metric.element_selector = item.node.selector;
            }
            break;
          }
        }
      }
    }

    // 4. Check for specific audit that might contain image info
    const largestImageAudit = audits["largest-image-paint"];
    if (largestImageAudit && largestImageAudit.details) {
      const imageDetails = largestImageAudit.details as any;

      if (
        imageDetails.items &&
        Array.isArray(imageDetails.items) &&
        imageDetails.items.length > 0
      ) {
        const item = imageDetails.items[0];

        if (item.url) {
          // If we have a large image that's close in time to LCP, it's likely the LCP element
          metric.element_type = "image";
          metric.element_url = item.url;
        }
      }
    }

    // 5. Check for network requests audit to find image resources
    if (!metric.element_url) {
      const networkRequests = audits["network-requests"];

      if (networkRequests && networkRequests.details) {
        const networkDetails = networkRequests.details as any;

        if (networkDetails.items && Array.isArray(networkDetails.items)) {
          // Get all image resources loaded close to the LCP time
          const lcpTime = lcp.numericValue || 0;
          const imageResources = networkDetails.items
            .filter(
              (item: any) =>
                item.url &&
                item.mimeType &&
                item.mimeType.startsWith("image/") &&
                item.endTime &&
                Math.abs(item.endTime - lcpTime) < 500 // Within 500ms of LCP
            )
            .sort(
              (a: any, b: any) =>
                Math.abs(a.endTime - lcpTime) - Math.abs(b.endTime - lcpTime)
            );

          if (imageResources.length > 0) {
            const closestImage = imageResources[0];

            if (!metric.element_type) {
              metric.element_type = "image";
              metric.element_url = closestImage.url;
            }
          }
        }
      }
    }

    metrics.push(metric);
  }

  if (audits["first-contentful-paint"]) {
    const fcp = audits["first-contentful-paint"];
    metrics.push({
      id: "fcp",
      score: fcp.score,
      value_ms: Math.round(fcp.numericValue || 0),
      passes_core_web_vital: fcp.score !== null && fcp.score >= 0.9,
    });
  }

  if (audits["speed-index"]) {
    const si = audits["speed-index"];
    metrics.push({
      id: "si",
      score: si.score,
      value_ms: Math.round(si.numericValue || 0),
    });
  }

  if (audits["interactive"]) {
    const tti = audits["interactive"];
    metrics.push({
      id: "tti",
      score: tti.score,
      value_ms: Math.round(tti.numericValue || 0),
    });
  }

  // Add CLS (Cumulative Layout Shift)
  if (audits["cumulative-layout-shift"]) {
    const cls = audits["cumulative-layout-shift"];
    metrics.push({
      id: "cls",
      score: cls.score,
      // CLS is not in ms, but a unitless value
      value_ms: Math.round((cls.numericValue || 0) * 1000) / 1000, // Convert to 3 decimal places
      passes_core_web_vital: cls.score !== null && cls.score >= 0.9,
    });
  }

  // Add TBT (Total Blocking Time)
  if (audits["total-blocking-time"]) {
    const tbt = audits["total-blocking-time"];
    metrics.push({
      id: "tbt",
      score: tbt.score,
      value_ms: Math.round(tbt.numericValue || 0),
      passes_core_web_vital: tbt.score !== null && tbt.score >= 0.9,
    });
  }

  // Extract opportunities
  if (audits["render-blocking-resources"]) {
    const rbrAudit = audits["render-blocking-resources"];

    // Determine impact level based on potential savings
    let impact: "critical" | "serious" | "moderate" | "minor" = "moderate";
    const savings = Math.round(rbrAudit.numericValue || 0);

    if (savings > 2000) {
      impact = "critical";
    } else if (savings > 1000) {
      impact = "serious";
    } else if (savings < 300) {
      impact = "minor";
    }

    const opportunity: AIOptimizedOpportunity = {
      id: "render_blocking_resources",
      savings_ms: savings,
      severity: impact,
      resources: [],
    };

    const rbrDetails = rbrAudit.details as any;
    if (rbrDetails && rbrDetails.items && Array.isArray(rbrDetails.items)) {
      // Determine how many items to include based on impact
      const itemLimit = DETAIL_LIMITS[impact];

      rbrDetails.items
        .slice(0, itemLimit)
        .forEach((item: { url?: string; wastedMs?: number }) => {
          if (item.url) {
            // Extract file name from full URL
            const fileName = item.url.split("/").pop() || item.url;
            opportunity.resources.push({
              url: fileName,
              savings_ms: Math.round(item.wastedMs || 0),
            });
          }
        });
    }

    if (opportunity.resources.length > 0) {
      opportunities.push(opportunity);
    }
  }

  if (audits["uses-http2"]) {
    const http2Audit = audits["uses-http2"];

    // Determine impact level based on potential savings
    let impact: "critical" | "serious" | "moderate" | "minor" = "moderate";
    const savings = Math.round(http2Audit.numericValue || 0);

    if (savings > 2000) {
      impact = "critical";
    } else if (savings > 1000) {
      impact = "serious";
    } else if (savings < 300) {
      impact = "minor";
    }

    const opportunity: AIOptimizedOpportunity = {
      id: "http2",
      savings_ms: savings,
      severity: impact,
      resources: [],
    };

    const http2Details = http2Audit.details as any;
    if (
      http2Details &&
      http2Details.items &&
      Array.isArray(http2Details.items)
    ) {
      // Determine how many items to include based on impact
      const itemLimit = DETAIL_LIMITS[impact];

      http2Details.items
        .slice(0, itemLimit)
        .forEach((item: { url?: string }) => {
          if (item.url) {
            // Extract file name from full URL
            const fileName = item.url.split("/").pop() || item.url;
            opportunity.resources.push({ url: fileName });
          }
        });
    }

    if (opportunity.resources.length > 0) {
      opportunities.push(opportunity);
    }
  }

  // After extracting all metrics and opportunities, collect page stats
  // Extract page stats
  let page_stats: AIPageStats | undefined;

  // Total page stats
  const totalByteWeight = audits["total-byte-weight"];
  const networkRequests = audits["network-requests"];
  const thirdPartyAudit = audits["third-party-summary"];
  const mainThreadWork = audits["mainthread-work-breakdown"];

  if (networkRequests && networkRequests.details) {
    const resourceDetails = networkRequests.details as any;

    if (resourceDetails.items && Array.isArray(resourceDetails.items)) {
      const resources = resourceDetails.items;
      const totalRequests = resources.length;

      // Calculate total size and counts by type
      let totalSizeKb = 0;
      let jsCount = 0,
        cssCount = 0,
        imgCount = 0,
        fontCount = 0,
        otherCount = 0;

      resources.forEach((resource: any) => {
        const sizeKb = resource.transferSize
          ? Math.round(resource.transferSize / 1024)
          : 0;
        totalSizeKb += sizeKb;

        // Count by mime type
        const mimeType = resource.mimeType || "";
        if (mimeType.includes("javascript") || resource.url.endsWith(".js")) {
          jsCount++;
        } else if (mimeType.includes("css") || resource.url.endsWith(".css")) {
          cssCount++;
        } else if (
          mimeType.includes("image") ||
          /\.(jpg|jpeg|png|gif|webp|svg)$/i.test(resource.url)
        ) {
          imgCount++;
        } else if (
          mimeType.includes("font") ||
          /\.(woff|woff2|ttf|otf|eot)$/i.test(resource.url)
        ) {
          fontCount++;
        } else {
          otherCount++;
        }
      });

      // Calculate third-party size
      let thirdPartySizeKb = 0;
      if (thirdPartyAudit && thirdPartyAudit.details) {
        const thirdPartyDetails = thirdPartyAudit.details as any;
        if (thirdPartyDetails.items && Array.isArray(thirdPartyDetails.items)) {
          thirdPartyDetails.items.forEach((item: any) => {
            if (item.transferSize) {
              thirdPartySizeKb += Math.round(item.transferSize / 1024);
            }
          });
        }
      }

      // Get main thread blocking time
      let mainThreadBlockingTimeMs = 0;
      if (mainThreadWork && mainThreadWork.numericValue) {
        mainThreadBlockingTimeMs = Math.round(mainThreadWork.numericValue);
      }

      // Create page stats object
      page_stats = {
        total_size_kb: totalSizeKb,
        total_requests: totalRequests,
        resource_counts: {
          js: jsCount,
          css: cssCount,
          img: imgCount,
          font: fontCount,
          other: otherCount,
        },
        third_party_size_kb: thirdPartySizeKb,
        main_thread_blocking_time_ms: mainThreadBlockingTimeMs,
      };
    }
  }

  // Generate prioritized recommendations
  const prioritized_recommendations: string[] = [];

  // Add key recommendations based on failed audits with high impact
  if (
    audits["render-blocking-resources"] &&
    audits["render-blocking-resources"].score !== null &&
    audits["render-blocking-resources"].score === 0
  ) {
    prioritized_recommendations.push("Eliminate render-blocking resources");
  }

  if (
    audits["uses-responsive-images"] &&
    audits["uses-responsive-images"].score !== null &&
    audits["uses-responsive-images"].score === 0
  ) {
    prioritized_recommendations.push("Properly size images");
  }

  if (
    audits["uses-optimized-images"] &&
    audits["uses-optimized-images"].score !== null &&
    audits["uses-optimized-images"].score === 0
  ) {
    prioritized_recommendations.push("Efficiently encode images");
  }

  if (
    audits["uses-text-compression"] &&
    audits["uses-text-compression"].score !== null &&
    audits["uses-text-compression"].score === 0
  ) {
    prioritized_recommendations.push("Enable text compression");
  }

  if (
    audits["uses-http2"] &&
    audits["uses-http2"].score !== null &&
    audits["uses-http2"].score === 0
  ) {
    prioritized_recommendations.push("Use HTTP/2");
  }

  // Add more specific recommendations based on Core Web Vitals
  if (
    audits["largest-contentful-paint"] &&
    audits["largest-contentful-paint"].score !== null &&
    audits["largest-contentful-paint"].score < 0.5
  ) {
    prioritized_recommendations.push("Improve Largest Contentful Paint (LCP)");
  }

  if (
    audits["cumulative-layout-shift"] &&
    audits["cumulative-layout-shift"].score !== null &&
    audits["cumulative-layout-shift"].score < 0.5
  ) {
    prioritized_recommendations.push("Reduce layout shifts (CLS)");
  }

  if (
    audits["total-blocking-time"] &&
    audits["total-blocking-time"].score !== null &&
    audits["total-blocking-time"].score < 0.5
  ) {
    prioritized_recommendations.push("Reduce JavaScript execution time");
  }

  // Create the performance report content
  const reportContent: PerformanceReportContent = {
    score,
    audit_counts,
    metrics,
    opportunities,
    page_stats,
    prioritized_recommendations:
      prioritized_recommendations.length > 0
        ? prioritized_recommendations
        : undefined,
  };

  // Return the full report following the LighthouseReport interface
  return {
    metadata,
    report: reportContent,
  };
};


================================================
FILE: browser-tools-server/lighthouse/seo.ts
================================================
import { Result as LighthouseResult } from "lighthouse";
import { AuditCategory, LighthouseReport } from "./types.js";
import { runLighthouseAudit } from "./index.js";

// === SEO Report Types ===

/**
 * SEO-specific report content structure
 */
export interface SEOReportContent {
  score: number; // Overall score (0-100)
  audit_counts: {
    // Counts of different audit types
    failed: number;
    passed: number;
    manual: number;
    informative: number;
    not_applicable: number;
  };
  issues: AISEOIssue[];
  categories: {
    [category: string]: {
      score: number;
      issues_count: number;
    };
  };
  prioritized_recommendations?: string[]; // Ordered list of recommendations
}

/**
 * Full SEO report implementing the base LighthouseReport interface
 */
export type AIOptimizedSEOReport = LighthouseReport<SEOReportContent>;

/**
 * AI-optimized SEO issue
 */
interface AISEOIssue {
  id: string; // e.g., "meta-description"
  title: string; // e.g., "Document has a meta description"
  impact: "critical" | "serious" | "moderate" | "minor";
  category: string; // e.g., "content", "mobile", "crawlability"
  details?: {
    selector?: string; // CSS selector if applicable
    value?: string; // Current value
    issue?: string; // Description of the issue
  }[];
  score: number | null; // 0-1 or null
}

// Original interfaces for backward compatibility
interface SEOAudit {
  id: string; // e.g., "meta-description"
  title: string; // e.g., "Document has a meta description"
  description: string; // e.g., "Meta descriptions improve SEO..."
  score: number | null; // 0-1 or null
  scoreDisplayMode: string; // e.g., "binary"
  details?: SEOAuditDetails; // Optional, structured details
  weight?: number; // For prioritization
}

interface SEOAuditDetails {
  items?: Array<{
    selector?: string; // e.g., "meta[name='description']"
    issue?: string; // e.g., "Meta description is missing"
    value?: string; // e.g., Current meta description text
  }>;
  type?: string; // e.g., "table"
}

// This ensures we always include critical issues while limiting less important ones
const DETAIL_LIMITS = {
  critical: Number.MAX_SAFE_INTEGER, // No limit for critical issues
  serious: 15, // Up to 15 items for serious issues
  moderate: 10, // Up to 10 items for moderate issues
  minor: 3, // Up to 3 items for minor issues
};

/**
 * Runs an SEO audit on the specified URL
 * @param url The URL to audit
 * @returns Promise resolving to AI-optimized SEO audit results
 */
export async function runSEOAudit(url: string): Promise<AIOptimizedSEOReport> {
  try {
    const lhr = await runLighthouseAudit(url, [AuditCategory.SEO]);
    return extractAIOptimizedData(lhr, url);
  } catch (error) {
    throw new Error(
      `SEO audit failed: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
}

/**
 * Extract AI-optimized SEO data from Lighthouse results
 */
const extractAIOptimizedData = (
  lhr: LighthouseResult,
  url: string
): AIOptimizedSEOReport => {
  const categoryData = lhr.categories[AuditCategory.SEO];
  const audits = lhr.audits || {};

  // Add metadata
  const metadata = {
    url,
    timestamp: lhr.fetchTime || new Date().toISOString(),
    device: "desktop", // This could be made configurable
    lighthouseVersion: lhr.lighthouseVersion,
  };

  // Initialize variables
  const issues: AISEOIssue[] = [];
  const categories: {
    [category: string]: { score: number; issues_count: number };
  } = {
    content: { score: 0, issues_count: 0 },
    mobile: { score: 0, issues_count: 0 },
    crawlability: { score: 0, issues_count: 0 },
    other: { score: 0, issues_count: 0 },
  };

  // Count audits by type
  let failedCount = 0;
  let passedCount = 0;
  let manualCount = 0;
  let informativeCount = 0;
  let notApplicableCount = 0;

  // Process audit refs
  const auditRefs = categoryData?.auditRefs || [];

  // First pass: count audits by type and initialize categories
  auditRefs.forEach((ref) => {
    const audit = audits[ref.id];
    if (!audit) return;

    // Count by scoreDisplayMode
    if (audit.scoreDisplayMode === "manual") {
      manualCount++;
    } else if (audit.scoreDisplayMode === "informative") {
      informativeCount++;
    } else if (audit.scoreDisplayMode === "notApplicable") {
      notApplicableCount++;
    } else if (audit.score !== null) {
      // Binary pass/fail
      if (audit.score >= 0.9) {
        passedCount++;
      } else {
        failedCount++;
      }
    }

    // Categorize the issue
    let category = "other";
    if (
      ref.id.includes("crawl") ||
      ref.id.includes("http") ||
      ref.id.includes("redirect") ||
      ref.id.includes("robots")
    ) {
      category = "crawlability";
    } else if (
      ref.id.includes("viewport") ||
      ref.id.includes("font-size") ||
      ref.id.includes("tap-targets")
    ) {
      category = "mobile";
    } else if (
      ref.id.includes("document") ||
      ref.id.includes("meta") ||
      ref.id.includes("description") ||
      ref.id.includes("canonical") ||
      ref.id.includes("title") ||
      ref.id.includes("link")
    ) {
      category = "content";
    }

    // Update category score and issues count
    if (audit.score !== null && audit.score < 0.9) {
      categories[category].issues_count++;
    }
  });

  // Second pass: process failed audits into AI-friendly format
  auditRefs
    .filter((ref) => {
      const audit = audits[ref.id];
      return audit && audit.score !== null && audit.score < 0.9;
    })
    .sort((a, b) => (b.weight || 0) - (a.weight || 0))
    // No limit on failed audits - we'll filter dynamically based on impact
    .forEach((ref) => {
      const audit = audits[ref.id];

      // Determine impact level based on score and weight
      let impact: "critical" | "serious" | "moderate" | "minor" = "moderate";
      if (audit.score === 0) {
        impact = "critical";
      } else if (audit.score !== null && audit.score <= 0.5) {
        impact = "serious";
      } else if (audit.score !== null && audit.score > 0.7) {
        impact = "minor";
      }

      // Categorize the issue
      let category = "other";
      if (
        ref.id.includes("crawl") ||
        ref.id.includes("http") ||
        ref.id.includes("redirect") ||
        ref.id.includes("robots")
      ) {
        category = "crawlability";
      } else if (
        ref.id.includes("viewport") ||
        ref.id.includes("font-size") ||
        ref.id.includes("tap-targets")
      ) {
        category = "mobile";
      } else if (
        ref.id.includes("document") ||
        ref.id.includes("meta") ||
        ref.id.includes("description") ||
        ref.id.includes("canonical") ||
        ref.id.includes("title") ||
        ref.id.includes("link")
      ) {
        category = "content";
      }

      // Extract details
      const details: { selector?: string; value?: string; issue?: string }[] =
        [];

      if (audit.details) {
        const auditDetails = audit.details as any;
        if (auditDetails.items && Array.isArray(auditDetails.items)) {
          // Determine item limit based on impact
          const itemLimit = DETAIL_LIMITS[impact];

          auditDetails.items.slice(0, itemLimit).forEach((item: any) => {
            const detail: {
              selector?: string;
              value?: string;
              issue?: string;
            } = {};

            if (item.selector) {
              detail.selector = item.selector;
            }

            if (item.value !== undefined) {
              detail.value = item.value;
            }

            if (item.issue) {
              detail.issue = item.issue;
            }

            if (Object.keys(detail).length > 0) {
              details.push(detail);
            }
          });
        }
      }

      // Create the issue
      const issue: AISEOIssue = {
        id: ref.id,
        title: audit.title,
        impact,
        category,
        details: details.length > 0 ? details : undefined,
        score: audit.score,
      };

      issues.push(issue);
    });

  // Calculate overall score
  const score = Math.round((categoryData?.score || 0) * 100);

  // Generate prioritized recommendations
  const prioritized_recommendations: string[] = [];

  // Add category-specific recommendations
  Object.entries(categories)
    .filter(([_, data]) => data.issues_count > 0)
    .sort(([_, a], [__, b]) => b.issues_count - a.issues_count)
    .forEach(([category, data]) => {
      if (data.issues_count === 0) return;

      let recommendation = "";

      switch (category) {
        case "content":
          recommendation = `Improve SEO content (${data.issues_count} issues): titles, descriptions, and headers`;
          break;
        case "mobile":
          recommendation = `Optimize for mobile devices (${data.issues_count} issues)`;
          break;
        case "crawlability":
          recommendation = `Fix crawlability issues (${data.issues_count} issues): robots.txt, sitemaps, and redirects`;
          break;
        default:
          recommendation = `Fix ${data.issues_count} SEO issues in category: ${category}`;
      }

      prioritized_recommendations.push(recommendation);
    });

  // Add specific high-impact recommendations
  if (issues.some((issue) => issue.id === "meta-description")) {
    prioritized_recommendations.push(
      "Add a meta description to improve click-through rate"
    );
  }

  if (issues.some((issue) => issue.id === "document-title")) {
    prioritized_recommendations.push(
      "Add a descriptive page title with keywords"
    );
  }

  if (issues.some((issue) => issue.id === "hreflang")) {
    prioritized_recommendations.push(
      "Fix hreflang implementation for international SEO"
    );
  }

  if (issues.some((issue) => issue.id === "canonical")) {
    prioritized_recommendations.push("Implement proper canonical tags");
  }

  // Create the report content
  const reportContent: SEOReportContent = {
    score,
    audit_counts: {
      failed: failedCount,
      passed: passedCount,
      manual: manualCount,
      informative: informativeCount,
      not_applicable: notApplicableCount,
    },
    issues,
    categories,
    prioritized_recommendations:
      prioritized_recommendations.length > 0
        ? prioritized_recommendations
        : undefined,
  };

  // Return the full report following the LighthouseReport interface
  return {
    metadata,
    report: reportContent,
  };
};


================================================
FILE: browser-tools-server/lighthouse/types.ts
================================================
/**
 * Audit categories available in Lighthouse
 */
export enum AuditCategory {
  ACCESSIBILITY = "accessibility",
  PERFORMANCE = "performance",
  SEO = "seo",
  BEST_PRACTICES = "best-practices", // Not yet implemented
  PWA = "pwa", // Not yet implemented
}

/**
 * Base interface for Lighthouse report metadata
 */
export interface LighthouseReport<T = any> {
  metadata: {
    url: string;
    timestamp: string; // ISO 8601, e.g., "2025-02-27T14:30:00Z"
    device: string; // e.g., "mobile", "desktop"
    lighthouseVersion: string; // e.g., "10.4.0"
  };

  // For backward compatibility with existing report formats
  overallScore?: number;
  failedAuditsCount?: number;
  passedAuditsCount?: number;
  manualAuditsCount?: number;
  informativeAuditsCount?: number;
  notApplicableAuditsCount?: number;
  failedAudits?: any[];

  // New format for specialized reports
  report?: T; // Generic report data that will be specialized by each audit type
}

/**
 * Configuration options for Lighthouse audits
 */
export interface LighthouseConfig {
  flags: {
    output: string[];
    onlyCategories: string[];
    formFactor: string;
    port: number | undefined;
    screenEmulation: {
      mobile: boolean;
      width: number;
      height: number;
      deviceScaleFactor: number;
      disabled: boolean;
    };
  };
  config: {
    extends: string;
    settings: {
      onlyCategories: string[];
      emulatedFormFactor: string;
      throttling: {
        cpuSlowdownMultiplier: number;
      };
    };
  };
}


================================================
FILE: browser-tools-server/package.json
================================================
{
  "name": "@agentdeskai/browser-tools-server",
  "version": "1.2.0",
  "description": "A browser tools server for capturing and managing browser events, logs, and screenshots",
  "type": "module",
  "main": "dist/browser-connector.js",
  "bin": {
    "browser-tools-server": "./dist/browser-connector.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "tsc && node dist/browser-connector.js",
    "prepublishOnly": "npm run build"
  },
  "keywords": [
    "browser",
    "tools",
    "debugging",
    "logging",
    "screenshots",
    "chrome",
    "extension"
  ],
  "author": "AgentDesk AI",
  "license": "MIT",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.4.1",
    "body-parser": "^1.20.3",
    "cors": "^2.8.5",
    "express": "^4.21.2",
    "lighthouse": "^11.6.0",
    "llm-cost": "^1.0.5",
    "node-fetch": "^2.7.0",
    "puppeteer-core": "^22.4.1",
    "ws": "^8.18.0"
  },
  "optionalDependencies": {
    "chrome-launcher": "^1.1.2"
  },
  "devDependencies": {
    "@types/ws": "^8.5.14",
    "@types/body-parser": "^1.19.5",
    "@types/cors": "^2.8.17",
    "@types/express": "^5.0.0",
    "@types/node": "^22.13.1",
    "@types/node-fetch": "^2.6.11",
    "@types/puppeteer-core": "^7.0.4",
    "typescript": "^5.7.3"
  }
}


================================================
FILE: browser-tools-server/puppeteer-service.ts
================================================
import fs from "fs";
import puppeteer from "puppeteer-core";
import path from "path";
import os from "os";
import { execSync } from "child_process";
import * as ChromeLauncher from "chrome-launcher";
// ===== Configuration Types and Defaults =====

/**
 * Configuration interface for the Puppeteer service
 */
export interface PuppeteerServiceConfig {
  // Browser preferences
  preferredBrowsers?: string[]; // Order of browser preference ("chrome", "edge", "brave", "firefox")
  customBrowserPaths?: { [key: string]: string }; // Custom browser executable paths

  // Connection settings
  debugPorts?: number[]; // Ports to try when connecting to existing browsers
  connectionTimeout?: number; // Timeout for connection attempts in ms
  maxRetries?: number; // Maximum number of retries for connections

  // Browser cleanup settings
  browserCleanupTimeout?: number; // Timeout before closing inactive browsers (ms)

  // Performance settings
  blockResourceTypes?: string[]; // Resource types to block for performance
}

// Default configuration values
const DEFAULT_CONFIG: PuppeteerServiceConfig = {
  preferredBrowsers: ["chrome", "edge", "brave", "firefox"],
  debugPorts: [9222, 9223, 9224, 9225],
  connectionTimeout: 10000,
  maxRetries: 3,
  browserCleanupTimeout: 60000,
  blockResourceTypes: ["image", "font", "media"],
};

// Browser support notes:
// - Chrome/Chromium: Fully supported (primary target)
// - Edge: Fully supported (Chromium-based)
// - Brave: Fully supported (Chromium-based)
// - Firefox: Partially supported (some features may not work)
// - Safari: Not supported by Puppeteer

// ===== Global State =====

// Current active configuration
let currentConfig: PuppeteerServiceConfig = { ...DEFAULT_CONFIG };

// Browser instance management
let headlessBrowserInstance: puppeteer.Browser | null = null;
let launchedBrowserWSEndpoint: string | null = null;

// Cleanup management
let browserCleanupTimeout: NodeJS.Timeout | null = null;
let BROWSER_CLEANUP_TIMEOUT = 60000; // 60 seconds default

// Cache for browser executable paths
let detectedBrowserPath: string | null = null;

// ===== Configuration Functions =====

/**
 * Configure the Puppeteer service with custom settings
 * @param config Partial configuration to override defaults
 */
export function configurePuppeteerService(
  config: Partial<PuppeteerServiceConfig>
): void {
  currentConfig = { ...DEFAULT_CONFIG, ...config };

  // Update the timeout if it was changed
  if (
    config.browserCleanupTimeout &&
    config.browserCleanupTimeout !== BROWSER_CLEANUP_TIMEOUT
  ) {
    BROWSER_CLEANUP_TIMEOUT = config.browserCleanupTimeout;
  }

  console.log("Puppeteer service configured:", currentConfig);
}

// ===== Browser Management =====

/**
 * Get or create a headless browser instance
 * @returns Promise resolving to a browser instance
 */
async function getHeadlessBrowserInstance(): Promise<puppeteer.Browser> {
  console.log("Browser instance request started");

  // Cancel any scheduled cleanup
  cancelScheduledCleanup();

  // Try to reuse existing browser
  if (headlessBrowserInstance) {
    try {
      const pages = await headlessBrowserInstance.pages();
      console.log(
        `Reusing existing headless browser with ${pages.length} pages`
      );
      return headlessBrowserInstance;
    } catch (error) {
      console.log(
        "Existing browser instance is no longer valid, creating a new one"
      );
      headlessBrowserInstance = null;
      launchedBrowserWSEndpoint = null;
    }
  }

  // Create a new browser instance
  return launchNewBrowser();
}

/**
 * Launches a new browser instance
 * @returns Promise resolving to a browser instance
 */
async function launchNewBrowser(): Promise<puppeteer.Browser> {
  console.log("Creating new headless browser instance");

  // Setup temporary user data directory
  const userDataDir = createTempUserDataDir();
  let browser: puppeteer.Browser | null = null;

  try {
    // Configure launch options
    const launchOptions = configureLaunchOptions(userDataDir);

    // Set custom browser executable
    await setCustomBrowserExecutable(launchOptions);

    // Launch the browser
    console.log(
      "Launching browser with options:",
      JSON.stringify({
        headless: launchOptions.headless,
        executablePath: launchOptions.executablePath,
      })
    );

    browser = await puppeteer.launch(launchOptions);

    // Store references to the browser instance
    launchedBrowserWSEndpoint = browser.wsEndpoint();
    headlessBrowserInstance = browser;

    // Setup cleanup handlers
    setupBrowserCleanupHandlers(browser, userDataDir);

    console.log("Browser ready");
    return browser;
  } catch (error) {
    console.error("Failed to launch browser:", error);

    // Clean up resources
    if (browser) {
      try {
        await browser.close();
      } catch (closeError) {
        console.error("Error closing browser:", closeError);
      }
      headlessBrowserInstance = null;
      launchedBrowserWSEndpoint = null;
    }

    // Clean up the temporary directory
    try {
      fs.rmSync(userDataDir, { recursive: true, force: true });
    } catch (fsError) {
      console.error("Error removing temporary directory:", fsError);
    }

    throw error;
  }
}

/**
 * Creates a temporary user data directory for the browser
 * @returns Path to the created directory
 */
function createTempUserDataDir(): string {
  const tempDir = os.tmpdir();
  const uniqueId = `${Date.now().toString()}-${Math.random()
    .toString(36)
    .substring(2)}`;
  const userDataDir = path.join(tempDir, `browser-debug-profile-${uniqueId}`);
  fs.mkdirSync(userDataDir, { recursive: true });
  console.log(`Using temporary user data directory: ${userDataDir}`);
  return userDataDir;
}

/**
 * Configures browser launch options
 * @param userDataDir Path to the user data directory
 * @returns Launch options object
 */
function configureLaunchOptions(userDataDir: string): any {
  const launchOptions: any = {
    args: [
      "--remote-debugging-port=0", // Use dynamic port
      `--user-data-dir=${userDataDir}`,
      "--no-first-run",
      "--no-default-browser-check",
      "--disable-dev-shm-usage",
      "--disable-extensions",
      "--disable-component-extensions-with-background-pages",
      "--disable-background-networking",
      "--disable-backgrounding-occluded-windows",
      "--disable-default-apps",
      "--disable-sync",
      "--disable-translate",
      "--metrics-recording-only",
      "--no-pings",
      "--safebrowsing-disable-auto-update",
    ],
  };

  // Add headless mode (using any to bypass type checking issues)
  launchOptions.headless = "new";

  return launchOptions;
}

/**
 * Sets a custom browser executable path if configured
 * @param launchOptions Launch options object to modify
 */
async function setCustomBrowserExecutable(launchOptions: any): Promise<void> {
  // First, try to use a custom browser path from configuration
  if (
    currentConfig.customBrowserPaths &&
    Object.keys(currentConfig.customBrowserPaths).length > 0
  ) {
    const preferredBrowsers = currentConfig.preferredBrowsers || [
      "chrome",
      "edge",
      "brave",
      "firefox",
    ];

    for (const browser of preferredBrowsers) {
      if (
        currentConfig.customBrowserPaths[browser] &&
        fs.existsSync(currentConfig.customBrowserPaths[browser])
      ) {
        launchOptions.executablePath =
          currentConfig.customBrowserPaths[browser];

        // Set product to firefox if using Firefox browser
        if (browser === "firefox") {
          launchOptions.product = "firefox";
        }

        console.log(
          `Using custom ${browser} path: ${launchOptions.executablePath}`
        );
        return;
      }
    }
  }

  // If no custom path is found, use cached path or detect a new one
  try {
    if (detectedBrowserPath && fs.existsSync(detectedBrowserPath)) {
      console.log(`Using cached browser path: ${detectedBrowserPath}`);
      launchOptions.executablePath = detectedBrowserPath;

      // Check if the detected browser is Firefox
      if (detectedBrowserPath.includes("firefox")) {
        launchOptions.product = "firefox";
        console.log("Setting product to firefox for Firefox browser");
      }
    } else {
      detectedBrowserPath = await findBrowserExecutablePath();
      launchOptions.executablePath = detectedBrowserPath;

      // Check if the detected browser is Firefox
      if (detectedBrowserPath.includes("firefox")) {
        launchOptions.product = "firefox";
        console.log("Setting product to firefox for Firefox browser");
      }

      console.log(
        `Using detected browser path: ${launchOptions.executablePath}`
      );
    }
  } catch (error) {
    console.error("Failed to detect browser executable path:", error);
    throw new Error(
      "No browser executable path found. Please specify a custom browser path in the configuration."
    );
  }
}

/**
 * Find a browser executable path on the current system
 * @returns Path to a browser executable
 */
async function findBrowserExecutablePath(): Promise<string> {
  // Try to use chrome-launcher (most reliable method)
  try {
    console.log("Attempting to find Chrome using chrome-launcher...");

    // Launch Chrome using chrome-launcher
    const chrome = await ChromeLauncher.launch({
      chromeFlags: ["--headless"],
      handleSIGINT: false,
    });

    // chrome-launcher stores the Chrome executable path differently than Puppeteer
    // Let's try different approaches to get it

    // First check if we can access it directly
    let chromePath = "";

    // Chrome version data often contains the path
    if (chrome.process && chrome.process.spawnfile) {
      chromePath = chrome.process.spawnfile;
      console.log("Found Chrome path from process.spawnfile");
    } else {
      // Try to get the Chrome path from chrome-launcher
      // In newer versions, it's directly accessible
      console.log("Trying to determine Chrome path using other methods");

      // This will actually return the real Chrome path for us
      // chrome-launcher has this inside but doesn't expose it directly
      const possiblePaths = [
        process.env.CHROME_PATH,
        // Common paths by OS
        ...(process.platform === "darwin"
          ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]
          : process.platform === "win32"
          ? [
              `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`,
              `${process.env["PROGRAMFILES(X86)"]}\\Google\\Chrome\\Application\\chrome.exe`,
            ]
          : ["/usr/bin/google-chrome"]),
      ].filter(Boolean);

      // Use the first valid path
      for (const p of possiblePaths) {
        if (p && fs.existsSync(p)) {
          chromePath = p;
          console.log("Found Chrome path from common locations");
          break;
        }
      }
    }

    // Always kill the Chrome instance we just launched
    await chrome.kill();

    if (chromePath) {
      console.log(`Chrome found via chrome-launcher: ${chromePath}`);
      return chromePath;
    } else {
      console.log("Chrome launched but couldn't determine executable path");
    }
  } catch (error) {
    // Check if it's a ChromeNotInstalledError
    const errorMessage = error instanceof Error ? error.message : String(error);
    if (
      errorMessage.includes("No Chrome installations found") ||
      (error as any)?.code === "ERR_LAUNCHER_NOT_INSTALLED"
    ) {
      console.log("Chrome not instal
Download .txt
gitextract_uipce7r9/

├── .gitignore
├── LICENSE
├── README.md
├── browser-tools-mcp/
│   ├── README.md
│   ├── mcp-server.ts
│   ├── package.json
│   └── tsconfig.json
├── browser-tools-server/
│   ├── README.md
│   ├── browser-connector.ts
│   ├── lighthouse/
│   │   ├── accessibility.ts
│   │   ├── best-practices.ts
│   │   ├── index.ts
│   │   ├── performance.ts
│   │   ├── seo.ts
│   │   └── types.ts
│   ├── package.json
│   ├── puppeteer-service.ts
│   └── tsconfig.json
├── chrome-extension/
│   ├── background.js
│   ├── devtools.html
│   ├── devtools.js
│   ├── manifest.json
│   ├── panel.html
│   └── panel.js
└── docs/
    ├── mcp-docs.md
    └── mcp.md
Download .txt
SYMBOL INDEX (110 symbols across 12 files)

FILE: browser-tools-mcp/mcp-server.ts
  function getDefaultServerPort (line 20) | function getDefaultServerPort(): number {
  function getDefaultServerHost (line 47) | function getDefaultServerHost(): string {
  function discoverServer (line 58) | async function discoverServer(): Promise<boolean> {
  function withServerConnection (line 116) | async function withServerConnection<T>(
  type AuditCategory (line 343) | enum AuditCategory {

FILE: browser-tools-server/browser-connector.ts
  function convertPathForCurrentPlatform (line 31) | function convertPathForCurrentPlatform(inputPath: string): string {
  function getDefaultDownloadsFolder (line 135) | function getDefaultDownloadsFolder(): string {
  type ScreenshotCallback (line 173) | interface ScreenshotCallback {
  function getAvailablePort (line 185) | async function getAvailablePort(
  constant REQUESTED_PORT (line 241) | const REQUESTED_PORT = parseInt(process.env.PORT || "3025", 10);
  constant PORT (line 242) | let PORT = REQUESTED_PORT;
  function truncateStringsInData (line 252) | function truncateStringsInData(data: any, maxLength: number): any {
  function processJsonString (line 275) | function processJsonString(jsonString: string, maxLength: number): string {
  function processLogsWithSettings (line 290) | function processLogsWithSettings(logs: any[]) {
  function calculateLogSize (line 309) | function calculateLogSize(log: any): number {
  function truncateLogsToQueryLimit (line 314) | function truncateLogsToQueryLimit(logs: any[]): any[] {
  function clearAllLogs (line 526) | function clearAllLogs() {
  type ScreenshotMessage (line 594) | interface ScreenshotMessage {
  class BrowserConnector (line 602) | class BrowserConnector {
    method constructor (line 609) | constructor(app: express.Application, server: any) {
    method handleScreenshot (line 815) | private async handleScreenshot(req: express.Request, res: express.Resp...
    method getUrlForAudit (line 894) | private async getUrlForAudit(): Promise<string | null> {
    method hasActiveConnection (line 935) | public hasActiveConnection(): boolean {
    method captureScreenshot (line 940) | async captureScreenshot(req: express.Request, res: express.Response) {
    method shutdown (line 1252) | public shutdown() {
    method setupAccessibilityAudit (line 1298) | private setupAccessibilityAudit() {
    method setupPerformanceAudit (line 1307) | private setupPerformanceAudit() {
    method setupSEOAudit (line 1316) | private setupSEOAudit() {
    method setupBestPracticesAudit (line 1321) | private setupBestPracticesAudit() {
    method setupAuditEndpoint (line 1335) | private setupAuditEndpoint(

FILE: browser-tools-server/lighthouse/accessibility.ts
  type AccessibilityReportContent (line 10) | interface AccessibilityReportContent {
  type AIOptimizedAccessibilityReport (line 34) | type AIOptimizedAccessibilityReport =
  type AIAccessibilityIssue (line 40) | interface AIAccessibilityIssue {
  type AIAccessibilityElement (line 52) | interface AIAccessibilityElement {
  type AccessibilityAudit (line 61) | interface AccessibilityAudit {
  type AuditDetails (line 71) | type AuditDetails = {
  constant DETAIL_LIMITS (line 88) | const DETAIL_LIMITS = {
  function runAccessibilityAudit (line 100) | async function runAccessibilityAudit(

FILE: browser-tools-server/lighthouse/best-practices.ts
  type BestPracticesReportContent (line 10) | interface BestPracticesReportContent {
  type AIOptimizedBestPracticesReport (line 33) | type AIOptimizedBestPracticesReport =
  type AIBestPracticesIssue (line 39) | interface AIBestPracticesIssue {
  type BestPracticesAudit (line 54) | interface BestPracticesAudit {
  type BestPracticesAuditDetails (line 63) | interface BestPracticesAuditDetails {
  constant DETAIL_LIMITS (line 69) | const DETAIL_LIMITS: Record<string, number> = {
  function runBestPracticesAudit (line 81) | async function runBestPracticesAudit(

FILE: browser-tools-server/lighthouse/index.ts
  function createLighthouseConfig (line 14) | function createLighthouseConfig(
  function runLighthouseAudit (line 49) | async function runLighthouseAudit(

FILE: browser-tools-server/lighthouse/performance.ts
  type PerformanceReportContent (line 10) | interface PerformanceReportContent {
  type AIOptimizedPerformanceReport (line 29) | type AIOptimizedPerformanceReport =
  type AIOptimizedMetric (line 33) | interface AIOptimizedMetric {
  type AIOptimizedOpportunity (line 45) | interface AIOptimizedOpportunity {
  type AIPageStats (line 59) | interface AIPageStats {
  constant DETAIL_LIMITS (line 75) | const DETAIL_LIMITS = {
  function runPerformanceAudit (line 89) | async function runPerformanceAudit(

FILE: browser-tools-server/lighthouse/seo.ts
  type SEOReportContent (line 10) | interface SEOReportContent {
  type AIOptimizedSEOReport (line 33) | type AIOptimizedSEOReport = LighthouseReport<SEOReportContent>;
  type AISEOIssue (line 38) | interface AISEOIssue {
  type SEOAudit (line 52) | interface SEOAudit {
  type SEOAuditDetails (line 62) | interface SEOAuditDetails {
  constant DETAIL_LIMITS (line 72) | const DETAIL_LIMITS = {
  function runSEOAudit (line 84) | async function runSEOAudit(url: string): Promise<AIOptimizedSEOReport> {

FILE: browser-tools-server/lighthouse/types.ts
  type AuditCategory (line 4) | enum AuditCategory {
  type LighthouseReport (line 15) | interface LighthouseReport<T = any> {
  type LighthouseConfig (line 39) | interface LighthouseConfig {

FILE: browser-tools-server/puppeteer-service.ts
  type PuppeteerServiceConfig (line 12) | interface PuppeteerServiceConfig {
  constant DEFAULT_CONFIG (line 30) | const DEFAULT_CONFIG: PuppeteerServiceConfig = {
  constant BROWSER_CLEANUP_TIMEOUT (line 57) | let BROWSER_CLEANUP_TIMEOUT = 60000;
  function configurePuppeteerService (line 68) | function configurePuppeteerService(
  function getHeadlessBrowserInstance (line 90) | async function getHeadlessBrowserInstance(): Promise<puppeteer.Browser> {
  function launchNewBrowser (line 121) | async function launchNewBrowser(): Promise<puppeteer.Browser> {
  function createTempUserDataDir (line 184) | function createTempUserDataDir(): string {
  function configureLaunchOptions (line 200) | function configureLaunchOptions(userDataDir: string): any {
  function setCustomBrowserExecutable (line 231) | async function setCustomBrowserExecutable(launchOptions: any): Promise<v...
  function findBrowserExecutablePath (line 302) | async function findBrowserExecutablePath(): Promise<string> {
  function setupBrowserCleanupHandlers (line 599) | function setupBrowserCleanupHandlers(
  function cancelScheduledCleanup (line 638) | function cancelScheduledCleanup(): void {
  function scheduleBrowserCleanup (line 649) | function scheduleBrowserCleanup(): void {
  function connectToHeadlessBrowser (line 680) | async function connectToHeadlessBrowser(

FILE: chrome-extension/background.js
  function validateServerIdentity (line 74) | async function validateServerIdentity(host, port) {
  function processTabForAudit (line 101) | function processTabForAudit(tab, tabId) {
  function getCurrentTabUrl (line 118) | async function getCurrentTabUrl(tabId) {
  function updateServerWithUrl (line 218) | async function updateServerWithUrl(tabId, url, source = "background_upda...
  function retestConnectionOnRefresh (line 300) | async function retestConnectionOnRefresh(tabId) {
  function captureAndSendScreenshot (line 343) | function captureAndSendScreenshot(message, settings, sendResponse) {

FILE: chrome-extension/devtools.js
  constant MAX_ATTACH_RETRIES (line 21) | const MAX_ATTACH_RETRIES = 3;
  constant ATTACH_RETRY_DELAY (line 22) | const ATTACH_RETRY_DELAY = 1000;
  function truncateStringsInData (line 114) | function truncateStringsInData(data, maxLength, depth = 0, path = "") {
  function calculateObjectSize (line 166) | function calculateObjectSize(obj) {
  function processArrayWithSizeLimit (line 171) | function processArrayWithSizeLimit(array, maxTotalSize, processFunc) {
  function processJsonString (line 200) | function processJsonString(jsonString, maxLength) {
  function sendToBrowserConnector (line 242) | async function sendToBrowserConnector(logData) {
  function validateServerIdentity (line 354) | async function validateServerIdentity() {
  function wipeLogs (line 432) | function wipeLogs() {
  function attachDebugger (line 498) | async function attachDebugger() {
  function performAttach (line 523) | function performAttach() {
  function detachDebugger (line 554) | function detachDebugger() {
  function captureAndSendElement (line 690) | function captureAndSendElement() {
  constant WS_RECONNECT_DELAY (line 740) | const WS_RECONNECT_DELAY = 5000;
  constant HEARTBEAT_INTERVAL (line 741) | const HEARTBEAT_INTERVAL = 30000;
  function sendHeartbeat (line 748) | function sendHeartbeat() {
  function setupWebSocket (line 755) | async function setupWebSocket() {

FILE: chrome-extension/panel.js
  function createConnectionBanner (line 159) | function createConnectionBanner() {
  function updateConnectionBanner (line 264) | function updateConnectionBanner(connected, serverInfo) {
  function updateUIFromSettings (line 348) | function updateUIFromSettings() {
  function saveSettings (line 362) | function saveSettings() {
  function cancelOngoingDiscovery (line 429) | function cancelOngoingDiscovery() {
  function testConnection (line 471) | async function testConnection(host, port) {
  function scheduleReconnectAttempt (line 547) | function scheduleReconnectAttempt() {
  function tryServerConnection (line 562) | async function tryServerConnection(host, port) {
  function discoverServer (line 656) | async function discoverServer(quietMode = false) {
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (363K chars).
[
  {
    "path": ".gitignore",
    "chars": 34,
    "preview": "node_modules\ndist\n.port\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright (c) 2025 AgentDesk LLC\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 10924,
    "preview": "THIS PROJECT IS NO LONGER ACTIVE PLEASE USE A DIFFERENT SOLUTION FOR THIS. \n\n# BrowserTools MCP\n\n\n> Make your AI tools 1"
  },
  {
    "path": "browser-tools-mcp/README.md",
    "chars": 2138,
    "preview": "# Browser Tools MCP Server\n\nA Model Context Protocol (MCP) server that provides AI-powered browser tools integration. Th"
  },
  {
    "path": "browser-tools-mcp/mcp-server.ts",
    "chars": 47802,
    "preview": "#!/usr/bin/env node\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport "
  },
  {
    "path": "browser-tools-mcp/package.json",
    "chars": 1290,
    "preview": "{\n  \"name\": \"@agentdeskai/browser-tools-mcp\",\n  \"version\": \"1.2.0\",\n  \"description\": \"MCP (Model Context Protocol) serve"
  },
  {
    "path": "browser-tools-mcp/tsconfig.json",
    "chars": 343,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"esMo"
  },
  {
    "path": "browser-tools-server/README.md",
    "chars": 12653,
    "preview": "# Browser Tools Server\n\nA powerful browser tools server for capturing and managing browser events, logs, and screenshots"
  },
  {
    "path": "browser-tools-server/browser-connector.ts",
    "chars": 50636,
    "preview": "#!/usr/bin/env node\n\nimport express from \"express\";\nimport cors from \"cors\";\nimport bodyParser from \"body-parser\";\nimpor"
  },
  {
    "path": "browser-tools-server/lighthouse/accessibility.ts",
    "chars": 10338,
    "preview": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\ni"
  },
  {
    "path": "browser-tools-server/lighthouse/best-practices.ts",
    "chars": 10298,
    "preview": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\ni"
  },
  {
    "path": "browser-tools-server/lighthouse/index.ts",
    "chars": 4276,
    "preview": "import lighthouse from \"lighthouse\";\nimport type { Result as LighthouseResult, Flags } from \"lighthouse\";\nimport {\n  con"
  },
  {
    "path": "browser-tools-server/lighthouse/performance.ts",
    "chars": 20902,
    "preview": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\ni"
  },
  {
    "path": "browser-tools-server/lighthouse/seo.ts",
    "chars": 10533,
    "preview": "import { Result as LighthouseResult } from \"lighthouse\";\nimport { AuditCategory, LighthouseReport } from \"./types.js\";\ni"
  },
  {
    "path": "browser-tools-server/lighthouse/types.ts",
    "chars": 1525,
    "preview": "/**\n * Audit categories available in Lighthouse\n */\nexport enum AuditCategory {\n  ACCESSIBILITY = \"accessibility\",\n  PER"
  },
  {
    "path": "browser-tools-server/package.json",
    "chars": 1258,
    "preview": "{\n  \"name\": \"@agentdeskai/browser-tools-server\",\n  \"version\": \"1.2.0\",\n  \"description\": \"A browser tools server for capt"
  },
  {
    "path": "browser-tools-server/puppeteer-service.ts",
    "chars": 29449,
    "preview": "import fs from \"fs\";\nimport puppeteer from \"puppeteer-core\";\nimport path from \"path\";\nimport os from \"os\";\nimport { exec"
  },
  {
    "path": "browser-tools-server/tsconfig.json",
    "chars": 377,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"esMo"
  },
  {
    "path": "chrome-extension/background.js",
    "chars": 13356,
    "preview": "// Listen for messages from the devtools panel\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {"
  },
  {
    "path": "chrome-extension/devtools.html",
    "chars": 213,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>BrowserTools MCP</title>\n  </head>\n  <body>\n    "
  },
  {
    "path": "chrome-extension/devtools.js",
    "chars": 34415,
    "preview": "// devtools.js\n\n// Store settings with defaults\nlet settings = {\n  logLimit: 50,\n  queryLimit: 30000,\n  stringSizeLimit:"
  },
  {
    "path": "chrome-extension/manifest.json",
    "chars": 518,
    "preview": "{\n    \"name\": \"BrowserTools MCP\",\n    \"version\": \"1.2.0\",\n    \"description\": \"MCP tool for AI code editors to capture da"
  },
  {
    "path": "chrome-extension/panel.html",
    "chars": 6713,
    "preview": "<!DOCTYPE html>\n<html>\n<head>\n    <style>\n        body {\n            padding: 16px;\n            font-family: system-ui, "
  },
  {
    "path": "chrome-extension/panel.js",
    "chars": 33473,
    "preview": "// Store settings\nlet settings = {\n  logLimit: 50,\n  queryLimit: 30000,\n  stringSizeLimit: 500,\n  showRequestHeaders: fa"
  },
  {
    "path": "docs/mcp-docs.md",
    "chars": 28846,
    "preview": "## Resources\n\nExpose data and content from your servers to LLMs\n\nResources are a core primitive in the Model Context Pro"
  },
  {
    "path": "docs/mcp.md",
    "chars": 12128,
    "preview": "# MCP TypeScript SDK ![NPM Version](mdc:https:/img.shields.io/npm/v/%40modelcontextprotocol%2Fsdk) ![MIT licensed](mdc:h"
  }
]

About this extraction

This page contains the full source code of the AgentDeskAI/browser-tools-mcp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (337.4 KB), approximately 77.1k tokens, and a symbol index with 110 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!