#
tokens: 48396/50000 22/25 files (page 1/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 1 of 2. Use http://codebase.md/weotzi/browser-tools-mcp?page={x} to view the full context.

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules
dist
.port
.DS_Store

```

--------------------------------------------------------------------------------
/browser-tools-mcp/README.md:
--------------------------------------------------------------------------------

```markdown
# 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

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# 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

  

Please make sure to update the version in your IDE / MCP client as so:
`npx @agentdeskai/[email protected]`

Also make sure to download the latest version of the chrome extension 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)

From there you can run the local node server like so:
`npx @agentdeskai/[email protected]`

Make sure to specify version 1.2.0 since NPX caching may prevent you from getting the latest version! You should only have to do this once for every update. After you do it once, you should be on the latest version.

And once you've opened your chrome dev tools, logs should be getting sent to your server 🦾

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

```

--------------------------------------------------------------------------------
/browser-tools-server/README.md:
--------------------------------------------------------------------------------

```markdown
# 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

```

--------------------------------------------------------------------------------
/chrome-extension/devtools.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>BrowserTools MCP</title>
  </head>
  <body>
    <!-- DevTools extension script -->
    <script src="devtools.js"></script>
  </body>
</html>

```

--------------------------------------------------------------------------------
/browser-tools-mcp/tsconfig.json:
--------------------------------------------------------------------------------

```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"]
} 
```

--------------------------------------------------------------------------------
/browser-tools-server/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": ".",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["**/*.ts"],
  "exclude": ["node_modules", "dist"]
}

```

--------------------------------------------------------------------------------
/chrome-extension/manifest.json:
--------------------------------------------------------------------------------

```json
{
    "name": "BrowserTools MCP",
    "version": "1.2.0",
    "description": "MCP tool for AI code editors to capture data from a browser such as console logs, network requests, screenshots and more",
    "manifest_version": 3,
    "devtools_page": "devtools.html",
    "permissions": [
      "activeTab",
      "debugger",
      "storage",
      "tabs",
      "tabCapture",
      "windows"
    ],
    "host_permissions": [
      "<all_urls>"
    ],
    "background": {
      "service_worker": "background.js"
    }
}

```

--------------------------------------------------------------------------------
/browser-tools-server/package.json:
--------------------------------------------------------------------------------

```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"
  }
}

```

--------------------------------------------------------------------------------
/browser-tools-mcp/package.json:
--------------------------------------------------------------------------------

```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"
  }
}

```

--------------------------------------------------------------------------------
/browser-tools-server/lighthouse/types.ts:
--------------------------------------------------------------------------------

```typescript
/**
 * 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;
      };
    };
  };
}

```

--------------------------------------------------------------------------------
/browser-tools-server/lighthouse/index.ts:
--------------------------------------------------------------------------------

```typescript
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";

```

--------------------------------------------------------------------------------
/chrome-extension/panel.html:
--------------------------------------------------------------------------------

```html
<!DOCTYPE html>
<html>
<head>
    <style>
        body {
            padding: 16px;
            font-family: system-ui, -apple-system, sans-serif;
            background-color: #282828;
            color: #fff;
        }
        .endpoint-list {
            margin: 16px 0;
        }
        .endpoint-item {
            display: flex;
            gap: 8px;
            margin-bottom: 8px;
            align-items: center;
        }
        .endpoint-form {
            display: flex;
            gap: 8px;
            margin-bottom: 16px;
            align-items: center;
        }
        button {
            padding: 4px 8px;
        }
        input {
            padding: 4px;
        }
        .status-indicator {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            display: inline-block;
        }
        .status-connected {
            background: #4caf50;
        }
        .status-disconnected {
            background: #f44336;
        }
        .form-group {
            margin-bottom: 16px;
        }
        .form-group label {
            display: block;
            margin-bottom: 4px;
        }
        .checkbox-group {
            margin-bottom: 8px;
        }
        .checkbox-group-2 {
            margin-bottom: 6px;
        }
        input[type="number"],
        input[type="text"] {
            padding: 4px;
            width: 200px;
        }
        .settings-section {
            border: 1px solid #ccc;
            padding: 16px;
            margin-bottom: 16px;
            border-radius: 4px;
        }
        .settings-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            cursor: pointer;
            user-select: none;
        }
        .settings-header h3 {
            margin: 0;
        }
        .settings-content {
            display: none;
            margin-top: 16px;
        }
        .settings-content.visible {
            display: block;
        }
        .chevron {
            width: 20px;
            height: 20px;
            transition: transform 0.3s ease;
        }
        .chevron.open {
            transform: rotate(180deg);
        }
        .quick-actions {
            display: flex;
            gap: 8px;
            margin-bottom: 16px;
        }
        .action-button {
            background-color: #4a4a4a;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            cursor: pointer;
            transition: background-color 0.2s;
        }
        .action-button:hover {
            background-color: #5a5a5a;
        }
        .action-button.danger {
            background-color: #f44336;
        }
        .action-button.danger:hover {
            background-color: #d32f2f;
        }
    </style>
</head>
<body>
    <div class="settings-section">
        <h3>Quick Actions</h3>
        <div class="quick-actions">
            <button id="capture-screenshot" class="action-button">
                Capture Screenshot
            </button>
            <button id="wipe-logs" class="action-button danger">
                Wipe All Logs
            </button>
        </div>
        <div class="checkbox-group-2" style="margin-top: 10px; display: flex; align-items: center;">
            <label>
                <input type="checkbox" id="allow-auto-paste">
                Allow Auto-paste to Cursor
            </label>
        </div>
    </div>

    <div class="settings-section">
        <h3>Screenshot Settings</h3>
        <div class="form-group">
            <label for="screenshot-path">Provide a directory to save screenshots to (by default screenshots will be saved to your downloads folder if no path is provided)</label>
            <input type="text" id="screenshot-path" placeholder="/path/to/screenshots">
        </div>
    </div>

    <div class="settings-section">
        <h3>Server Connection Settings</h3>
        <div class="form-group">
            <label for="server-host">Server Host</label>
            <input type="text" id="server-host" placeholder="localhost or IP address">
        </div>
        <div class="form-group">
            <label for="server-port">Server Port</label>
            <input type="number" id="server-port" min="1" max="65535" value="3025">
        </div>
        <div class="quick-actions">
            <button id="discover-server" class="action-button">
                Auto-Discover Server
            </button>
            <button id="test-connection" class="action-button">
                Test Connection
            </button>
        </div>
        <div id="connection-status" style="margin-top: 8px; display: none;">
            <span id="status-icon" class="status-indicator"></span>
            <span id="status-text"></span>
        </div>
    </div>

    <div class="settings-section">
        <div class="settings-header" id="advanced-settings-header">
            <h3>Advanced Settings</h3>
            <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <polyline points="6 9 12 15 18 9"></polyline>
            </svg>
        </div>
        
        <div class="settings-content" id="advanced-settings-content">
            <div class="form-group">
                <label for="log-limit">Log Limit (number of logs)</label>
                <input type="number" id="log-limit" min="1" value="50">
            </div>

            <div class="form-group">
                <label for="query-limit">Query Limit (characters)</label>
                <input type="number" id="query-limit" min="1" value="30000">
            </div>

            <div class="form-group">
                <label for="string-size-limit">String Size Limit (characters)</label>
                <input type="number" id="string-size-limit" min="1" value="500">
            </div>

            <div class="form-group">
                <label for="max-log-size">Max Log Size (characters)</label>
                <input type="number" id="max-log-size" min="1000" value="20000">
            </div>

            <div class="checkbox-group">
                <label>
                    <input type="checkbox" id="show-request-headers">
                    Include Request Headers
                </label>
            </div>

            <div class="checkbox-group">
                <label>
                    <input type="checkbox" id="show-response-headers">
                    Include Response Headers
                </label>
            </div>
        </div>
    </div>

    <script src="panel.js"></script>
</body>
</html> 
```

--------------------------------------------------------------------------------
/browser-tools-server/lighthouse/best-practices.ts:
--------------------------------------------------------------------------------

```typescript
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,
    },
  };
};

```

--------------------------------------------------------------------------------
/browser-tools-server/lighthouse/accessibility.ts:
--------------------------------------------------------------------------------

```typescript
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,
  };
};

```

--------------------------------------------------------------------------------
/browser-tools-server/lighthouse/seo.ts:
--------------------------------------------------------------------------------

```typescript
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,
  };
};

```

--------------------------------------------------------------------------------
/docs/mcp.md:
--------------------------------------------------------------------------------

```markdown
# MCP TypeScript SDK ![NPM Version](mdc:https:/img.shields.io/npm/v/%40modelcontextprotocol%2Fsdk) ![MIT licensed](mdc:https:/img.shields.io/npm/l/%40modelcontextprotocol%2Fsdk)

## Table of Contents

- [Overview](mdc:#overview)
- [Installation](mdc:#installation)
- [Quickstart](mdc:#quickstart)
- [What is MCP?](mdc:#what-is-mcp)
- [Core Concepts](mdc:#core-concepts)
  - [Server](mdc:#server)
  - [Resources](mdc:#resources)
  - [Tools](mdc:#tools)
  - [Prompts](mdc:#prompts)
- [Running Your Server](mdc:#running-your-server)
  - [stdio](mdc:#stdio)
  - [HTTP with SSE](mdc:#http-with-sse)
  - [Testing and Debugging](mdc:#testing-and-debugging)
- [Examples](mdc:#examples)
  - [Echo Server](mdc:#echo-server)
  - [SQLite Explorer](mdc:#sqlite-explorer)
- [Advanced Usage](mdc:#advanced-usage)
  - [Low-Level Server](mdc:#low-level-server)
  - [Writing MCP Clients](mdc:#writing-mcp-clients)
  - [Server Capabilities](mdc:#server-capabilities)

## Overview

The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This TypeScript SDK implements the full MCP specification, making it easy to:

- Build MCP clients that can connect to any MCP server
- Create MCP servers that expose resources, prompts and tools
- Use standard transports like stdio and SSE
- Handle all MCP protocol messages and lifecycle events

## Installation

```bash
npm install @modelcontextprotocol/sdk
```

## Quick Start

Let's create a simple MCP server that exposes a calculator tool and some data:

```typescript
import {
  McpServer,
  ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Create an MCP server
const server = new McpServer({
  name: "Demo",
  version: "1.0.0",
});

// Add an addition tool
server.tool("add", { a: z.number(), b: z.number() }, async ({ a, b }) => ({
  content: [{ type: "text", text: String(a + b) }],
}));

// Add a dynamic greeting resource
server.resource(
  "greeting",
  new ResourceTemplate("greeting://{name}", { list: undefined }),
  async (uri, { name }) => ({
    contents: [
      {
        uri: uri.href,
        text: `Hello, ${name}!`,
      },
    ],
  })
);

// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
```

## What is MCP?

The [Model Context Protocol (MCP)](mdc:https:/modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:

- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
- And more!

## Core Concepts

### Server

The McpServer is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:

```typescript
const server = new McpServer({
  name: "My App",
  version: "1.0.0",
});
```

### Resources

Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:

```typescript
// Static resource
server.resource("config", "config://app", async (uri) => ({
  contents: [
    {
      uri: uri.href,
      text: "App configuration here",
    },
  ],
}));

// Dynamic resource with parameters
server.resource(
  "user-profile",
  new ResourceTemplate("users://{userId}/profile", { list: undefined }),
  async (uri, { userId }) => ({
    contents: [
      {
        uri: uri.href,
        text: `Profile data for user ${userId}`,
      },
    ],
  })
);
```

### Tools

Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:

```typescript
// Simple tool with parameters
server.tool(
  "calculate-bmi",
  {
    weightKg: z.number(),
    heightM: z.number(),
  },
  async ({ weightKg, heightM }) => ({
    content: [
      {
        type: "text",
        text: String(weightKg / (heightM * heightM)),
      },
    ],
  })
);

// Async tool with external API call
server.tool("fetch-weather", { city: z.string() }, async ({ city }) => {
  const response = await fetch(`https://api.weather.com/${city}`);
  const data = await response.text();
  return {
    content: [{ type: "text", text: data }],
  };
});
```

### Prompts

Prompts are reusable templates that help LLMs interact with your server effectively:

```typescript
server.prompt("review-code", { code: z.string() }, ({ code }) => ({
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: `Please review this code:\n\n${code}`,
      },
    },
  ],
}));
```

## Running Your Server

MCP servers in TypeScript need to be connected to a transport to communicate with clients. How you start the server depends on the choice of transport:

### stdio

For command-line tools and direct integrations:

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

const server = new McpServer({
  name: "example-server",
  version: "1.0.0",
});

// ... set up server resources, tools, and prompts ...

const transport = new StdioServerTransport();
await server.connect(transport);
```

### HTTP with SSE

For remote servers, start a web server with a Server-Sent Events (SSE) endpoint, and a separate endpoint for the client to send its messages to:

```typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";

const server = new McpServer({
  name: "example-server",
  version: "1.0.0",
});

// ... set up server resources, tools, and prompts ...

const app = express();

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res);
  await server.connect(transport);
});

app.post("/messages", async (req, res) => {
  // Note: to support multiple simultaneous connections, these messages will
  // need to be routed to a specific matching transport. (This logic isn't
  // implemented here, for simplicity.)
  await transport.handlePostMessage(req, res);
});

app.listen(3001);
```

### Testing and Debugging

To test your server, you can use the [MCP Inspector](mdc:https:/github.com/modelcontextprotocol/inspector). See its README for more information.

## Examples

### Echo Server

A simple server demonstrating resources, tools, and prompts:

```typescript
import {
  McpServer,
  ResourceTemplate,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({
  name: "Echo",
  version: "1.0.0",
});

server.resource(
  "echo",
  new ResourceTemplate("echo://{message}", { list: undefined }),
  async (uri, { message }) => ({
    contents: [
      {
        uri: uri.href,
        text: `Resource echo: ${message}`,
      },
    ],
  })
);

server.tool("echo", { message: z.string() }, async ({ message }) => ({
  content: [{ type: "text", text: `Tool echo: ${message}` }],
}));

server.prompt("echo", { message: z.string() }, ({ message }) => ({
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: `Please process this message: ${message}`,
      },
    },
  ],
}));
```

### SQLite Explorer

A more complex example showing database integration:

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import sqlite3 from "sqlite3";
import { promisify } from "util";
import { z } from "zod";

const server = new McpServer({
  name: "SQLite Explorer",
  version: "1.0.0",
});

// Helper to create DB connection
const getDb = () => {
  const db = new sqlite3.Database("database.db");
  return {
    all: promisify<string, any[]>(db.all.bind(db)),
    close: promisify(db.close.bind(db)),
  };
};

server.resource("schema", "schema://main", async (uri) => {
  const db = getDb();
  try {
    const tables = await db.all(
      "SELECT sql FROM sqlite_master WHERE type='table'"
    );
    return {
      contents: [
        {
          uri: uri.href,
          text: tables.map((t: { sql: string }) => t.sql).join("\n"),
        },
      ],
    };
  } finally {
    await db.close();
  }
});

server.tool("query", { sql: z.string() }, async ({ sql }) => {
  const db = getDb();
  try {
    const results = await db.all(sql);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  } catch (err: unknown) {
    const error = err as Error;
    return {
      content: [
        {
          type: "text",
          text: `Error: ${error.message}`,
        },
      ],
      isError: true,
    };
  } finally {
    await db.close();
  }
});
```

## Advanced Usage

### Low-Level Server

For more control, you can use the low-level Server class directly:

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  {
    name: "example-server",
    version: "1.0.0",
  },
  {
    capabilities: {
      prompts: {},
    },
  }
);

server.setRequestHandler(ListPromptsRequestSchema, async () => {
  return {
    prompts: [
      {
        name: "example-prompt",
        description: "An example prompt template",
        arguments: [
          {
            name: "arg1",
            description: "Example argument",
            required: true,
          },
        ],
      },
    ],
  };
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  if (request.params.name !== "example-prompt") {
    throw new Error("Unknown prompt");
  }
  return {
    description: "Example prompt",
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: "Example prompt text",
        },
      },
    ],
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);
```

### Writing MCP Clients

The SDK provides a high-level client interface:

```typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

const transport = new StdioClientTransport({
  command: "node",
  args: ["server.js"],
});

const client = new Client(
  {
    name: "example-client",
    version: "1.0.0",
  },
  {
    capabilities: {
      prompts: {},
      resources: {},
      tools: {},
    },
  }
);

await client.connect(transport);

// List prompts
const prompts = await client.listPrompts();

// Get a prompt
const prompt = await client.getPrompt("example-prompt", {
  arg1: "value",
});

// List resources
const resources = await client.listResources();

// Read a resource
const resource = await client.readResource("file:///example.txt");

// Call a tool
const result = await client.callTool({
  name: "example-tool",
  arguments: {
    arg1: "value",
  },
});
```

## Documentation

- [Model Context Protocol documentation](mdc:https:/modelcontextprotocol.io)
- [MCP Specification](mdc:https:/spec.modelcontextprotocol.io)
- [Example Servers](mdc:https:/github.com/modelcontextprotocol/servers)

## Contributing

Issues and pull requests are welcome on GitHub at https://github.com/modelcontextprotocol/typescript-sdk.

## License

This project is licensed under the MIT License—see the [LICENSE](mdc:LICENSE) file for details.

```

--------------------------------------------------------------------------------
/chrome-extension/background.js:
--------------------------------------------------------------------------------

```javascript
// Listen for messages from the devtools panel
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "GET_CURRENT_URL" && message.tabId) {
    getCurrentTabUrl(message.tabId)
      .then((url) => {
        sendResponse({ success: true, url: url });
      })
      .catch((error) => {
        sendResponse({ success: false, error: error.message });
      });
    return true; // Required to use sendResponse asynchronously
  }

  // Handle explicit request to update the server with the URL
  if (message.type === "UPDATE_SERVER_URL" && message.tabId && message.url) {
    console.log(
      `Background: Received request to update server with URL for tab ${message.tabId}: ${message.url}`
    );
    updateServerWithUrl(
      message.tabId,
      message.url,
      message.source || "explicit_update"
    )
      .then(() => {
        if (sendResponse) sendResponse({ success: true });
      })
      .catch((error) => {
        console.error("Background: Error updating server with URL:", error);
        if (sendResponse)
          sendResponse({ success: false, error: error.message });
      });
    return true; // Required to use sendResponse asynchronously
  }

  if (message.type === "CAPTURE_SCREENSHOT" && message.tabId) {
    // First get the server settings
    chrome.storage.local.get(["browserConnectorSettings"], (result) => {
      const settings = result.browserConnectorSettings || {
        serverHost: "localhost",
        serverPort: 3025,
      };

      // Validate server identity first
      validateServerIdentity(settings.serverHost, settings.serverPort)
        .then((isValid) => {
          if (!isValid) {
            console.error(
              "Cannot capture screenshot: Not connected to a valid browser tools server"
            );
            sendResponse({
              success: false,
              error:
                "Not connected to a valid browser tools server. Please check your connection settings.",
            });
            return;
          }

          // Continue with screenshot capture
          captureAndSendScreenshot(message, settings, sendResponse);
        })
        .catch((error) => {
          console.error("Error validating server:", error);
          sendResponse({
            success: false,
            error: "Failed to validate server identity: " + error.message,
          });
        });
    });
    return true; // Required to use sendResponse asynchronously
  }
});

// Validate server identity
async function validateServerIdentity(host, port) {
  try {
    const response = await fetch(`http://${host}:${port}/.identity`, {
      signal: AbortSignal.timeout(3000), // 3 second timeout
    });

    if (!response.ok) {
      console.error(`Invalid server response: ${response.status}`);
      return false;
    }

    const identity = await response.json();

    // Validate the server signature
    if (identity.signature !== "mcp-browser-connector-24x7") {
      console.error("Invalid server signature - not the browser tools server");
      return false;
    }

    return true;
  } catch (error) {
    console.error("Error validating server identity:", error);
    return false;
  }
}

// Helper function to process the tab and run the audit
function processTabForAudit(tab, tabId) {
  const url = tab.url;

  if (!url) {
    console.error(`No URL available for tab ${tabId}`);
    return;
  }

  // Update our cache and the server with this URL
  tabUrls.set(tabId, url);
  updateServerWithUrl(tabId, url);
}

// Track URLs for each tab
const tabUrls = new Map();

// Function to get the current URL for a tab
async function getCurrentTabUrl(tabId) {
  try {
    console.log("Background: Getting URL for tab", tabId);

    // First check if we have it cached
    if (tabUrls.has(tabId)) {
      const cachedUrl = tabUrls.get(tabId);
      console.log("Background: Found cached URL:", cachedUrl);
      return cachedUrl;
    }

    // Otherwise get it from the tab
    try {
      const tab = await chrome.tabs.get(tabId);
      if (tab && tab.url) {
        // Cache the URL
        tabUrls.set(tabId, tab.url);
        console.log("Background: Got URL from tab:", tab.url);
        return tab.url;
      } else {
        console.log("Background: Tab exists but no URL found");
      }
    } catch (tabError) {
      console.error("Background: Error getting tab:", tabError);
    }

    // If we can't get the tab directly, try querying for active tabs
    try {
      const tabs = await chrome.tabs.query({
        active: true,
        currentWindow: true,
      });
      if (tabs && tabs.length > 0 && tabs[0].url) {
        const activeUrl = tabs[0].url;
        console.log("Background: Got URL from active tab:", activeUrl);
        // Cache this URL as well
        tabUrls.set(tabId, activeUrl);
        return activeUrl;
      }
    } catch (queryError) {
      console.error("Background: Error querying tabs:", queryError);
    }

    console.log("Background: Could not find URL for tab", tabId);
    return null;
  } catch (error) {
    console.error("Background: Error getting tab URL:", error);
    return null;
  }
}

// Listen for tab updates to detect page refreshes and URL changes
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  // Track URL changes
  if (changeInfo.url) {
    console.log(`URL changed in tab ${tabId} to ${changeInfo.url}`);
    tabUrls.set(tabId, changeInfo.url);

    // Send URL update to server if possible
    updateServerWithUrl(tabId, changeInfo.url, "tab_url_change");
  }

  // Check if this is a page refresh (status becoming "complete")
  if (changeInfo.status === "complete") {
    // Update URL in our cache
    if (tab.url) {
      tabUrls.set(tabId, tab.url);
      // Send URL update to server if possible
      updateServerWithUrl(tabId, tab.url, "page_complete");
    }

    retestConnectionOnRefresh(tabId);
  }
});

// Listen for tab activation (switching between tabs)
chrome.tabs.onActivated.addListener((activeInfo) => {
  const tabId = activeInfo.tabId;
  console.log(`Tab activated: ${tabId}`);

  // Get the URL of the newly activated tab
  chrome.tabs.get(tabId, (tab) => {
    if (chrome.runtime.lastError) {
      console.error("Error getting tab info:", chrome.runtime.lastError);
      return;
    }

    if (tab && tab.url) {
      console.log(`Active tab changed to ${tab.url}`);

      // Update our cache
      tabUrls.set(tabId, tab.url);

      // Send URL update to server
      updateServerWithUrl(tabId, tab.url, "tab_activated");
    }
  });
});

// Function to update the server with the current URL
async function updateServerWithUrl(tabId, url, source = "background_update") {
  if (!url) {
    console.error("Cannot update server with empty URL");
    return;
  }

  console.log(`Updating server with URL for tab ${tabId}: ${url}`);

  // Get the saved settings
  chrome.storage.local.get(["browserConnectorSettings"], async (result) => {
    const settings = result.browserConnectorSettings || {
      serverHost: "localhost",
      serverPort: 3025,
    };

    // Maximum number of retry attempts
    const maxRetries = 3;
    let retryCount = 0;
    let success = false;

    while (retryCount < maxRetries && !success) {
      try {
        // Send the URL to the server
        const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/current-url`;
        console.log(
          `Attempt ${
            retryCount + 1
          }/${maxRetries} to update server with URL: ${url}`
        );

        const response = await fetch(serverUrl, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            url: url,
            tabId: tabId,
            timestamp: Date.now(),
            source: source,
          }),
          // Add a timeout to prevent hanging requests
          signal: AbortSignal.timeout(5000),
        });

        if (response.ok) {
          const responseData = await response.json();
          console.log(
            `Successfully updated server with URL: ${url}`,
            responseData
          );
          success = true;
        } else {
          console.error(
            `Server returned error: ${response.status} ${response.statusText}`
          );
          retryCount++;
          // Wait before retrying
          await new Promise((resolve) => setTimeout(resolve, 500));
        }
      } catch (error) {
        console.error(`Error updating server with URL: ${error.message}`);
        retryCount++;
        // Wait before retrying
        await new Promise((resolve) => setTimeout(resolve, 500));
      }
    }

    if (!success) {
      console.error(
        `Failed to update server with URL after ${maxRetries} attempts`
      );
    }
  });
}

// Clean up when tabs are closed
chrome.tabs.onRemoved.addListener((tabId) => {
  tabUrls.delete(tabId);
});

// Function to retest connection when a page is refreshed
async function retestConnectionOnRefresh(tabId) {
  console.log(`Page refreshed in tab ${tabId}, retesting connection...`);

  // Get the saved settings
  chrome.storage.local.get(["browserConnectorSettings"], async (result) => {
    const settings = result.browserConnectorSettings || {
      serverHost: "localhost",
      serverPort: 3025,
    };

    // Test the connection with the last known host and port
    const isConnected = await validateServerIdentity(
      settings.serverHost,
      settings.serverPort
    );

    // Notify all devtools instances about the connection status
    chrome.runtime.sendMessage({
      type: "CONNECTION_STATUS_UPDATE",
      isConnected: isConnected,
      tabId: tabId,
    });

    // Always notify for page refresh, whether connected or not
    // This ensures any ongoing discovery is cancelled and restarted
    chrome.runtime.sendMessage({
      type: "INITIATE_AUTO_DISCOVERY",
      reason: "page_refresh",
      tabId: tabId,
      forceRestart: true, // Add a flag to indicate this should force restart any ongoing processes
    });

    if (!isConnected) {
      console.log(
        "Connection test failed after page refresh, initiating auto-discovery..."
      );
    } else {
      console.log("Connection test successful after page refresh");
    }
  });
}

// Function to capture and send screenshot
function captureAndSendScreenshot(message, settings, sendResponse) {
  // Get the inspected window's tab
  chrome.tabs.get(message.tabId, (tab) => {
    if (chrome.runtime.lastError) {
      console.error("Error getting tab:", chrome.runtime.lastError);
      sendResponse({
        success: false,
        error: chrome.runtime.lastError.message,
      });
      return;
    }

    // Get all windows to find the one containing our tab
    chrome.windows.getAll({ populate: true }, (windows) => {
      const targetWindow = windows.find((w) =>
        w.tabs.some((t) => t.id === message.tabId)
      );

      if (!targetWindow) {
        console.error("Could not find window containing the inspected tab");
        sendResponse({
          success: false,
          error: "Could not find window containing the inspected tab",
        });
        return;
      }

      // Capture screenshot of the window containing our tab
      chrome.tabs.captureVisibleTab(
        targetWindow.id,
        { format: "png" },
        (dataUrl) => {
          // Ignore DevTools panel capture error if it occurs
          if (
            chrome.runtime.lastError &&
            !chrome.runtime.lastError.message.includes("devtools://")
          ) {
            console.error(
              "Error capturing screenshot:",
              chrome.runtime.lastError
            );
            sendResponse({
              success: false,
              error: chrome.runtime.lastError.message,
            });
            return;
          }

          // Send screenshot data to browser connector using configured settings
          const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/screenshot`;
          console.log(`Sending screenshot to ${serverUrl}`);

          fetch(serverUrl, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              data: dataUrl,
              path: message.screenshotPath,
            }),
          })
            .then((response) => response.json())
            .then((result) => {
              if (result.error) {
                console.error("Error from server:", result.error);
                sendResponse({ success: false, error: result.error });
              } else {
                console.log("Screenshot saved successfully:", result.path);
                // Send success response even if DevTools capture failed
                sendResponse({
                  success: true,
                  path: result.path,
                  title: tab.title || "Current Tab",
                });
              }
            })
            .catch((error) => {
              console.error("Error sending screenshot data:", error);
              sendResponse({
                success: false,
                error: error.message || "Failed to save screenshot",
              });
            });
        }
      );
    });
  });
}

```

--------------------------------------------------------------------------------
/browser-tools-server/lighthouse/performance.ts:
--------------------------------------------------------------------------------

```typescript
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,
  };
};

```

--------------------------------------------------------------------------------
/docs/mcp-docs.md:
--------------------------------------------------------------------------------

```markdown
## Resources

Expose data and content from your servers to LLMs

Resources are a core primitive in the Model Context Protocol (MCP) that allow servers to expose data and content that can be read by clients and used as context for LLM interactions.

Resources are designed to be application-controlled, meaning that the client application can decide how and when they should be used. Different MCP clients may handle resources differently. For example:

Claude Desktop currently requires users to explicitly select resources before they can be used
Other clients might automatically select resources based on heuristics
Some implementations may even allow the AI model itself to determine which resources to use
Server authors should be prepared to handle any of these interaction patterns when implementing resource support. In order to expose data to models automatically, server authors should use a model-controlled primitive such as Tools.

​
Overview
Resources represent any kind of data that an MCP server wants to make available to clients. This can include:

File contents
Database records
API responses
Live system data
Screenshots and images
Log files
And more
Each resource is identified by a unique URI and can contain either text or binary data.

​
Resource URIs
Resources are identified using URIs that follow this format:

[protocol]://[host]/[path]
For example:

file:///home/user/documents/report.pdf
postgres://database/customers/schema
screen://localhost/display1
The protocol and path structure is defined by the MCP server implementation. Servers can define their own custom URI schemes.

​
Resource types
Resources can contain two types of content:

​
Text resources
Text resources contain UTF-8 encoded text data. These are suitable for:

Source code
Configuration files
Log files
JSON/XML data
Plain text
​
Binary resources
Binary resources contain raw binary data encoded in base64. These are suitable for:

Images
PDFs
Audio files
Video files
Other non-text formats
​
Resource discovery
Clients can discover available resources through two main methods:

​
Direct resources
Servers expose a list of concrete resources via the resources/list endpoint. Each resource includes:

{
uri: string; // Unique identifier for the resource
name: string; // Human-readable name
description?: string; // Optional description
mimeType?: string; // Optional MIME type
}
​
Resource templates
For dynamic resources, servers can expose URI templates that clients can use to construct valid resource URIs:

{
uriTemplate: string; // URI template following RFC 6570
name: string; // Human-readable name for this type
description?: string; // Optional description
mimeType?: string; // Optional MIME type for all matching resources
}
​
Reading resources
To read a resource, clients make a resources/read request with the resource URI.

The server responds with a list of resource contents:

{
contents: [
{
uri: string; // The URI of the resource
mimeType?: string; // Optional MIME type

      // One of:
      text?: string;      // For text resources
      blob?: string;      // For binary resources (base64 encoded)
    }

]
}
Servers may return multiple resources in response to one resources/read request. This could be used, for example, to return a list of files inside a directory when the directory is read.

​
Resource updates
MCP supports real-time updates for resources through two mechanisms:

​
List changes
Servers can notify clients when their list of available resources changes via the notifications/resources/list_changed notification.

​
Content changes
Clients can subscribe to updates for specific resources:

Client sends resources/subscribe with resource URI
Server sends notifications/resources/updated when the resource changes
Client can fetch latest content with resources/read
Client can unsubscribe with resources/unsubscribe
​
Example implementation
Here’s a simple example of implementing resource support in an MCP server:

## Prompts

Create reusable prompt templates and workflows

Prompts enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. They provide a powerful way to standardize and share common LLM interactions.

Prompts are designed to be user-controlled, meaning they are exposed from servers to clients with the intention of the user being able to explicitly select them for use.

​
Overview
Prompts in MCP are predefined templates that can:

Accept dynamic arguments
Include context from resources
Chain multiple interactions
Guide specific workflows
Surface as UI elements (like slash commands)
​
Prompt structure
Each prompt is defined with:

{
name: string; // Unique identifier for the prompt
description?: string; // Human-readable description
arguments?: [ // Optional list of arguments
{
name: string; // Argument identifier
description?: string; // Argument description
required?: boolean; // Whether argument is required
}
]
}
​
Discovering prompts
Clients can discover available prompts through the prompts/list endpoint:

// Request
{
method: "prompts/list"
}

// Response
{
prompts: [
{
name: "analyze-code",
description: "Analyze code for potential improvements",
arguments: [
{
name: "language",
description: "Programming language",
required: true
}
]
}
]
}
​
Using prompts
To use a prompt, clients make a prompts/get request:

// Request
{
method: "prompts/get",
params: {
name: "analyze-code",
arguments: {
language: "python"
}
}
}

// Response
{
description: "Analyze Python code for potential improvements",
messages: [
{
role: "user",
content: {
type: "text",
text: "Please analyze the following Python code for potential improvements:\n\n`python\ndef calculate_sum(numbers):\n    total = 0\n    for num in numbers:\n        total = total + num\n    return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n`"
}
}
]
}
​
Dynamic prompts
Prompts can be dynamic and include:

​
Embedded resource context

{
"name": "analyze-project",
"description": "Analyze project logs and code",
"arguments": [
{
"name": "timeframe",
"description": "Time period to analyze logs",
"required": true
},
{
"name": "fileUri",
"description": "URI of code file to review",
"required": true
}
]
}
When handling the prompts/get request:

{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Analyze these system logs and the code file for any issues:"
}
},
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": "logs://recent?timeframe=1h",
"text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded",
"mimeType": "text/plain"
}
}
},
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": "file:///path/to/code.py",
"text": "def connect_to_service(timeout=30):\n retries = 3\n for attempt in range(retries):\n try:\n return establish_connection(timeout)\n except TimeoutError:\n if attempt == retries - 1:\n raise\n time.sleep(5)\n\ndef establish_connection(timeout):\n # Connection implementation\n pass",
"mimeType": "text/x-python"
}
}
}
]
}
​
Multi-step workflows

const debugWorkflow = {
name: "debug-error",
async getMessages(error: string) {
return [
{
role: "user",
content: {
type: "text",
text: `Here's an error I'm seeing: ${error}`
}
},
{
role: "assistant",
content: {
type: "text",
text: "I'll help analyze this error. What have you tried so far?"
}
},
{
role: "user",
content: {
type: "text",
text: "I've tried restarting the service, but the error persists."
}
}
];
}
};
​
Example implementation
Here’s a complete example of implementing prompts in an MCP server:

TypeScript
Python

import { Server } from "@modelcontextprotocol/sdk/server";
import {
ListPromptsRequestSchema,
GetPromptRequestSchema
} from "@modelcontextprotocol/sdk/types";

const PROMPTS = {
"git-commit": {
name: "git-commit",
description: "Generate a Git commit message",
arguments: [
{
name: "changes",
description: "Git diff or description of changes",
required: true
}
]
},
"explain-code": {
name: "explain-code",
description: "Explain how code works",
arguments: [
{
name: "code",
description: "Code to explain",
required: true
},
{
name: "language",
description: "Programming language",
required: false
}
]
}
};

const server = new Server({
name: "example-prompts-server",
version: "1.0.0"
}, {
capabilities: {
prompts: {}
}
});

// List available prompts
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: Object.values(PROMPTS)
};
});

// Get specific prompt
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
const prompt = PROMPTS[request.params.name];
if (!prompt) {
throw new Error(`Prompt not found: ${request.params.name}`);
}

if (request.params.name === "git-commit") {
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Generate a concise but descriptive commit message for these changes:\n\n${request.params.arguments?.changes}`
}
}
]
};
}

if (request.params.name === "explain-code") {
const language = request.params.arguments?.language || "Unknown";
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Explain how this ${language} code works:\n\n${request.params.arguments?.code}`
}
}
]
};
}

throw new Error("Prompt implementation not found");
});
​
Best practices
When implementing prompts:

Use clear, descriptive prompt names
Provide detailed descriptions for prompts and arguments
Validate all required arguments
Handle missing arguments gracefully
Consider versioning for prompt templates
Cache dynamic content when appropriate
Implement error handling
Document expected argument formats
Consider prompt composability
Test prompts with various inputs
​
UI integration
Prompts can be surfaced in client UIs as:

Slash commands
Quick actions
Context menu items
Command palette entries
Guided workflows
Interactive forms
​
Updates and changes
Servers can notify clients about prompt changes:

Server capability: prompts.listChanged
Notification: notifications/prompts/list_changed
Client re-fetches prompt list
​
Security considerations
When implementing prompts:

Validate all arguments
Sanitize user input
Consider rate limiting
Implement access controls
Audit prompt usage
Handle sensitive data appropriately
Validate generated content
Implement timeouts
Consider prompt injection risks
Document security requirements

## Tools

Tools
Enable LLMs to perform actions through your server

Tools are a powerful primitive in the Model Context Protocol (MCP) that enable servers to expose executable functionality to clients. Through tools, LLMs can interact with external systems, perform computations, and take actions in the real world.

Tools are designed to be model-controlled, meaning that tools are exposed from servers to clients with the intention of the AI model being able to automatically invoke them (with a human in the loop to grant approval).

​
Overview
Tools in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. Key aspects of tools include:

Discovery: Clients can list available tools through the tools/list endpoint
Invocation: Tools are called using the tools/call endpoint, where servers perform the requested operation and return results
Flexibility: Tools can range from simple calculations to complex API interactions
Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.

​
Tool definition structure
Each tool is defined with the following structure:

{
name: string; // Unique identifier for the tool
description?: string; // Human-readable description
inputSchema: { // JSON Schema for the tool's parameters
type: "object",
properties: { ... } // Tool-specific parameters
}
}
​
Implementing tools
Here’s an example of implementing a basic tool in an MCP server:

TypeScript
Python

const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});

// Define available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "Add two numbers together",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}]
};
});

// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate_sum") {
const { a, b } = request.params.arguments;
return {
content: [
{
type: "text",
text: String(a + b)
}
]
};
}
throw new Error("Tool not found");
});
​
Example tool patterns
Here are some examples of types of tools that a server could provide:

​
System operations
Tools that interact with the local system:

{
name: "execute_command",
description: "Run a shell command",
inputSchema: {
type: "object",
properties: {
command: { type: "string" },
args: { type: "array", items: { type: "string" } }
}
}
}
​
API integrations
Tools that wrap external APIs:

{
name: "github_create_issue",
description: "Create a GitHub issue",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
body: { type: "string" },
labels: { type: "array", items: { type: "string" } }
}
}
}
​
Data processing
Tools that transform or analyze data:

{
name: "analyze_csv",
description: "Analyze a CSV file",
inputSchema: {
type: "object",
properties: {
filepath: { type: "string" },
operations: {
type: "array",
items: {
enum: ["sum", "average", "count"]
}
}
}
}
}
​
Best practices
When implementing tools:

Provide clear, descriptive names and descriptions
Use detailed JSON Schema definitions for parameters
Include examples in tool descriptions to demonstrate how the model should use them
Implement proper error handling and validation
Use progress reporting for long operations
Keep tool operations focused and atomic
Document expected return value structures
Implement proper timeouts
Consider rate limiting for resource-intensive operations
Log tool usage for debugging and monitoring
​
Security considerations
When exposing tools:

​
Input validation
Validate all parameters against the schema
Sanitize file paths and system commands
Validate URLs and external identifiers
Check parameter sizes and ranges
Prevent command injection
​
Access control
Implement authentication where needed
Use appropriate authorization checks
Audit tool usage
Rate limit requests
Monitor for abuse
​
Error handling
Don’t expose internal errors to clients
Log security-relevant errors
Handle timeouts appropriately
Clean up resources after errors
Validate return values
​
Tool discovery and updates
MCP supports dynamic tool discovery:

Clients can list available tools at any time
Servers can notify clients when tools change using notifications/tools/list_changed
Tools can be added or removed during runtime
Tool definitions can be updated (though this should be done carefully)
​
Error handling
Tool errors should be reported within the result object, not as MCP protocol-level errors. This allows the LLM to see and potentially handle the error. When a tool encounters an error:

Set isError to true in the result
Include error details in the content array
Here’s an example of proper error handling for tools:

TypeScript
Python

try {
// Tool operation
const result = performOperation();
return {
content: [
{
type: "text",
text: `Operation successful: ${result}`
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `Error: ${error.message}`
}
]
};
}
This approach allows the LLM to see that an error occurred and potentially take corrective action or request human intervention.

​
Testing tools
A comprehensive testing strategy for MCP tools should cover:

Functional testing: Verify tools execute correctly with valid inputs and handle invalid inputs appropriately
Integration testing: Test tool interaction with external systems using both real and mocked dependencies
Security testing: Validate authentication, authorization, input sanitization, and rate limiting
Performance testing: Check behavior under load, timeout handling, and resource cleanup
Error handling: Ensure tools properly report errors through the MCP protocol and clean up resources

## Sampling

Sampling
Let your servers request completions from LLMs

Sampling is a powerful MCP feature that allows servers to request LLM completions through the client, enabling sophisticated agentic behaviors while maintaining security and privacy.

This feature of MCP is not yet supported in the Claude Desktop client.

​
How sampling works
The sampling flow follows these steps:

Server sends a sampling/createMessage request to the client
Client reviews the request and can modify it
Client samples from an LLM
Client reviews the completion
Client returns the result to the server
This human-in-the-loop design ensures users maintain control over what the LLM sees and generates.

​
Message format
Sampling requests use a standardized message format:

{
messages: [
{
role: "user" | "assistant",
content: {
type: "text" | "image",

        // For text:
        text?: string,

        // For images:
        data?: string,             // base64 encoded
        mimeType?: string
      }
    }

],
modelPreferences?: {
hints?: [{
name?: string // Suggested model name/family
}],
costPriority?: number, // 0-1, importance of minimizing cost
speedPriority?: number, // 0-1, importance of low latency
intelligencePriority?: number // 0-1, importance of capabilities
},
systemPrompt?: string,
includeContext?: "none" | "thisServer" | "allServers",
temperature?: number,
maxTokens: number,
stopSequences?: string[],
metadata?: Record<string, unknown>
}
​
Request parameters
​
Messages
The messages array contains the conversation history to send to the LLM. Each message has:

role: Either “user” or “assistant”
content: The message content, which can be:
Text content with a text field
Image content with data (base64) and mimeType fields
​
Model preferences
The modelPreferences object allows servers to specify their model selection preferences:

hints: Array of model name suggestions that clients can use to select an appropriate model:

name: String that can match full or partial model names (e.g. “claude-3”, “sonnet”)
Clients may map hints to equivalent models from different providers
Multiple hints are evaluated in preference order
Priority values (0-1 normalized):

costPriority: Importance of minimizing costs
speedPriority: Importance of low latency response
intelligencePriority: Importance of advanced model capabilities
Clients make the final model selection based on these preferences and their available models.

​
System prompt
An optional systemPrompt field allows servers to request a specific system prompt. The client may modify or ignore this.

​
Context inclusion
The includeContext parameter specifies what MCP context to include:

"none": No additional context
"thisServer": Include context from the requesting server
"allServers": Include context from all connected MCP servers
The client controls what context is actually included.

​
Sampling parameters
Fine-tune the LLM sampling with:

temperature: Controls randomness (0.0 to 1.0)
maxTokens: Maximum tokens to generate
stopSequences: Array of sequences that stop generation
metadata: Additional provider-specific parameters
​
Response format
The client returns a completion result:

{
model: string, // Name of the model used
stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string,
role: "user" | "assistant",
content: {
type: "text" | "image",
text?: string,
data?: string,
mimeType?: string
}
}
​
Example request
Here’s an example of requesting sampling from a client:

{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "What files are in the current directory?"
}
}
],
"systemPrompt": "You are a helpful file system assistant.",
"includeContext": "thisServer",
"maxTokens": 100
}
}
​
Best practices
When implementing sampling:

Always provide clear, well-structured prompts
Handle both text and image content appropriately
Set reasonable token limits
Include relevant context through includeContext
Validate responses before using them
Handle errors gracefully
Consider rate limiting sampling requests
Document expected sampling behavior
Test with various model parameters
Monitor sampling costs
​
Human in the loop controls
Sampling is designed with human oversight in mind:

​
For prompts
Clients should show users the proposed prompt
Users should be able to modify or reject prompts
System prompts can be filtered or modified
Context inclusion is controlled by the client
​
For completions
Clients should show users the completion
Users should be able to modify or reject completions
Clients can filter or modify completions
Users control which model is used
​
Security considerations
When implementing sampling:

Validate all message content
Sanitize sensitive information
Implement appropriate rate limits
Monitor sampling usage
Encrypt data in transit
Handle user data privacy
Audit sampling requests
Control cost exposure
Implement timeouts
Handle model errors gracefully
​
Common patterns
​
Agentic workflows
Sampling enables agentic patterns like:

Reading and analyzing resources
Making decisions based on context
Generating structured data
Handling multi-step tasks
Providing interactive assistance
​
Context management
Best practices for context:

Request minimal necessary context
Structure context clearly
Handle context size limits
Update context as needed
Clean up stale context
​
Error handling
Robust error handling should:

Catch sampling failures
Handle timeout errors
Manage rate limits
Validate responses
Provide fallback behaviors
Log errors appropriately
​
Limitations
Be aware of these limitations:

Sampling depends on client capabilities
Users control sampling behavior
Context size has limits
Rate limits may apply
Costs should be considered
Model availability varies
Response times vary
Not all content types supported

## Roots

Roots
Understanding roots in MCP

Roots are a concept in MCP that define the boundaries where servers can operate. They provide a way for clients to inform servers about relevant resources and their locations.

​
What are Roots?
A root is a URI that a client suggests a server should focus on. When a client connects to a server, it declares which roots the server should work with. While primarily used for filesystem paths, roots can be any valid URI including HTTP URLs.

For example, roots could be:

file:///home/user/projects/myapp
https://api.example.com/v1
​
Why Use Roots?
Roots serve several important purposes:

Guidance: They inform servers about relevant resources and locations
Clarity: Roots make it clear which resources are part of your workspace
Organization: Multiple roots let you work with different resources simultaneously
​
How Roots Work
When a client supports roots, it:

Declares the roots capability during connection
Provides a list of suggested roots to the server
Notifies the server when roots change (if supported)
While roots are informational and not strictly enforcing, servers should:

Respect the provided roots
Use root URIs to locate and access resources
Prioritize operations within root boundaries
​
Common Use Cases
Roots are commonly used to define:

Project directories
Repository locations
API endpoints
Configuration locations
Resource boundaries
​
Best Practices
When working with roots:

Only suggest necessary resources
Use clear, descriptive names for roots
Monitor root accessibility
Handle root changes gracefully
​
Example
Here’s how a typical MCP client might expose roots:

{
"roots": [
{
"uri": "file:///home/user/projects/frontend",
"name": "Frontend Repository"
},
{
"uri": "https://api.example.com/v1",
"name": "API Endpoint"
}
]
}
This configuration suggests the server focus on both a local repository and an API endpoint while keeping them logically separated.

## Transports

Transports
Learn about MCP’s communication mechanisms

Transports in the Model Context Protocol (MCP) provide the foundation for communication between clients and servers. A transport handles the underlying mechanics of how messages are sent and received.

​
Message Format
MCP uses JSON-RPC 2.0 as its wire format. The transport layer is responsible for converting MCP protocol messages into JSON-RPC format for transmission and converting received JSON-RPC messages back into MCP protocol messages.

There are three types of JSON-RPC messages used:

​
Requests

{
jsonrpc: "2.0",
id: number | string,
method: string,
params?: object
}
​
Responses

{
jsonrpc: "2.0",
id: number | string,
result?: object,
error?: {
code: number,
message: string,
data?: unknown
}
}
​
Notifications

{
jsonrpc: "2.0",
method: string,
params?: object
}
​
Built-in Transport Types
MCP includes two standard transport implementations:

​
Standard Input/Output (stdio)
The stdio transport enables communication through standard input and output streams. This is particularly useful for local integrations and command-line tools.

Use stdio when:

Building command-line tools
Implementing local integrations
Needing simple process communication
Working with shell scripts
TypeScript (Server)
TypeScript (Client)
Python (Server)
Python (Client)

const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {}
});

const transport = new StdioServerTransport();
await server.connect(transport);
​
Server-Sent Events (SSE)
SSE transport enables server-to-client streaming with HTTP POST requests for client-to-server communication.

Use SSE when:

Only server-to-client streaming is needed
Working with restricted networks
Implementing simple updates
TypeScript (Server)
TypeScript (Client)
Python (Server)
Python (Client)

import express from "express";

const app = express();

const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {}
});

let transport: SSEServerTransport | null = null;

app.get("/sse", (req, res) => {
transport = new SSEServerTransport("/messages", res);
server.connect(transport);
});

app.post("/messages", (req, res) => {
if (transport) {
transport.handlePostMessage(req, res);
}
});

app.listen(3000);
​
Custom Transports
MCP makes it easy to implement custom transports for specific needs. Any transport implementation just needs to conform to the Transport interface:

You can implement custom transports for:

Custom network protocols
Specialized communication channels
Integration with existing systems
Performance optimization
TypeScript
Python

interface Transport {
// Start processing messages
start(): Promise<void>;

// Send a JSON-RPC message
send(message: JSONRPCMessage): Promise<void>;

// Close the connection
close(): Promise<void>;

// Callbacks
onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;
}
​
Error Handling
Transport implementations should handle various error scenarios:

Connection errors
Message parsing errors
Protocol errors
Network timeouts
Resource cleanup
Example error handling:

TypeScript
Python

class ExampleTransport implements Transport {
async start() {
try {
// Connection logic
} catch (error) {
this.onerror?.(new Error(`Failed to connect: ${error}`));
throw error;
}
}

async send(message: JSONRPCMessage) {
try {
// Sending logic
} catch (error) {
this.onerror?.(new Error(`Failed to send message: ${error}`));
throw error;
}
}
}
​
Best Practices
When implementing or using MCP transport:

Handle connection lifecycle properly
Implement proper error handling
Clean up resources on connection close
Use appropriate timeouts
Validate messages before sending
Log transport events for debugging
Implement reconnection logic when appropriate
Handle backpressure in message queues
Monitor connection health
Implement proper security measures
​
Security Considerations
When implementing transport:

​
Authentication and Authorization
Implement proper authentication mechanisms
Validate client credentials
Use secure token handling
Implement authorization checks
​
Data Security
Use TLS for network transport
Encrypt sensitive data
Validate message integrity
Implement message size limits
Sanitize input data
​
Network Security
Implement rate limiting
Use appropriate timeouts
Handle denial of service scenarios
Monitor for unusual patterns
Implement proper firewall rules
​
Debugging Transport
Tips for debugging transport issues:

Enable debug logging
Monitor message flow
Check connection states
Validate message formats
Test error scenarios
Use network analysis tools
Implement health checks
Monitor resource usage
Test edge cases
Use proper error tracking

```

--------------------------------------------------------------------------------
/browser-tools-server/puppeteer-service.ts:
--------------------------------------------------------------------------------

```typescript
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 installed. Falling back to manual detection");
    } else {
      console.error("Failed to find Chrome using chrome-launcher:", error);
      console.log("Falling back to manual detection");
    }
  }

  // If chrome-launcher failed, use manual detection

  const platform = process.platform;
  const preferredBrowsers = currentConfig.preferredBrowsers || [
    "chrome",
    "edge",
    "brave",
    "firefox",
  ];

  console.log(`Attempting to detect browser executable path on ${platform}...`);

  // Platform-specific detection strategies
  if (platform === "win32") {
    // Windows - try registry detection for Chrome
    let registryPath = null;
    try {
      console.log("Checking Windows registry for Chrome...");
      // Try HKLM first
      const regOutput = execSync(
        'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe" /ve',
        { encoding: "utf8" }
      );

      // Extract path from registry output
      const match = regOutput.match(/REG_(?:SZ|EXPAND_SZ)\s+([^\s]+)/i);
      if (match && match[1]) {
        registryPath = match[1].replace(/\\"/g, "");
        // Verify the path exists
        if (fs.existsSync(registryPath)) {
          console.log(`Found Chrome via HKLM registry: ${registryPath}`);
          return registryPath;
        }
      }
    } catch (e) {
      // Try HKCU if HKLM fails
      try {
        console.log("Checking user registry for Chrome...");
        const regOutput = execSync(
          'reg query "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe" /ve',
          { encoding: "utf8" }
        );

        // Extract path from registry output
        const match = regOutput.match(/REG_(?:SZ|EXPAND_SZ)\s+([^\s]+)/i);
        if (match && match[1]) {
          registryPath = match[1].replace(/\\"/g, "");
          // Verify the path exists
          if (fs.existsSync(registryPath)) {
            console.log(`Found Chrome via HKCU registry: ${registryPath}`);
            return registryPath;
          }
        }
      } catch (innerError) {
        console.log(
          "Failed to find Chrome via registry, continuing with path checks"
        );
      }
    }

    // Try to find Chrome through BLBeacon registry key (version info)
    try {
      console.log("Checking Chrome BLBeacon registry...");
      const regOutput = execSync(
        'reg query "HKEY_CURRENT_USER\\Software\\Google\\Chrome\\BLBeacon" /v version',
        { encoding: "utf8" }
      );

      if (regOutput) {
        // If BLBeacon exists, Chrome is likely installed in the default location
        const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
        const programFilesX86 =
          process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";

        const defaultChromePaths = [
          path.join(programFiles, "Google\\Chrome\\Application\\chrome.exe"),
          path.join(programFilesX86, "Google\\Chrome\\Application\\chrome.exe"),
        ];

        for (const chromePath of defaultChromePaths) {
          if (fs.existsSync(chromePath)) {
            console.log(
              `Found Chrome via BLBeacon registry hint: ${chromePath}`
            );
            return chromePath;
          }
        }
      }
    } catch (e) {
      console.log("Failed to find Chrome via BLBeacon registry");
    }

    // Continue with regular path checks
    const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
    const programFilesX86 =
      process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";

    // Common Windows browser paths
    const winBrowserPaths = {
      chrome: [
        path.join(programFiles, "Google\\Chrome\\Application\\chrome.exe"),
        path.join(programFilesX86, "Google\\Chrome\\Application\\chrome.exe"),
      ],
      edge: [
        path.join(programFiles, "Microsoft\\Edge\\Application\\msedge.exe"),
        path.join(programFilesX86, "Microsoft\\Edge\\Application\\msedge.exe"),
      ],
      brave: [
        path.join(
          programFiles,
          "BraveSoftware\\Brave-Browser\\Application\\brave.exe"
        ),
        path.join(
          programFilesX86,
          "BraveSoftware\\Brave-Browser\\Application\\brave.exe"
        ),
      ],
      firefox: [
        path.join(programFiles, "Mozilla Firefox\\firefox.exe"),
        path.join(programFilesX86, "Mozilla Firefox\\firefox.exe"),
      ],
    };

    // Check each browser in preferred order
    for (const browser of preferredBrowsers) {
      const paths =
        winBrowserPaths[browser as keyof typeof winBrowserPaths] || [];
      for (const browserPath of paths) {
        if (fs.existsSync(browserPath)) {
          console.log(`Found ${browser} at ${browserPath}`);
          return browserPath;
        }
      }
    }
  } else if (platform === "darwin") {
    // macOS browser paths
    const macBrowserPaths = {
      chrome: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
      edge: ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
      brave: ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"],
      firefox: ["/Applications/Firefox.app/Contents/MacOS/firefox"],
      safari: ["/Applications/Safari.app/Contents/MacOS/Safari"],
    };

    // Check each browser in preferred order
    for (const browser of preferredBrowsers) {
      const paths =
        macBrowserPaths[browser as keyof typeof macBrowserPaths] || [];
      for (const browserPath of paths) {
        if (fs.existsSync(browserPath)) {
          console.log(`Found ${browser} at ${browserPath}`);
          // Safari is detected but not supported by Puppeteer
          if (browser === "safari") {
            console.log(
              "Safari detected but not supported by Puppeteer. Continuing search..."
            );
            continue;
          }
          return browserPath;
        }
      }
    }
  } else if (platform === "linux") {
    // Linux browser commands
    const linuxBrowserCommands = {
      chrome: ["google-chrome", "chromium", "chromium-browser"],
      edge: ["microsoft-edge"],
      brave: ["brave-browser"],
      firefox: ["firefox"],
    };

    // Check each browser in preferred order
    for (const browser of preferredBrowsers) {
      const commands =
        linuxBrowserCommands[browser as keyof typeof linuxBrowserCommands] ||
        [];
      for (const cmd of commands) {
        try {
          // Use more universal commands for Linux to find executables
          // command -v works in most shells, fallback to which or type
          const browserPath = execSync(
            `command -v ${cmd} || which ${cmd} || type -p ${cmd} 2>/dev/null`,
            { encoding: "utf8" }
          ).trim();

          if (browserPath && fs.existsSync(browserPath)) {
            console.log(`Found ${browser} at ${browserPath}`);
            return browserPath;
          }
        } catch (e) {
          // Command not found, continue to next
        }
      }
    }

    // Additional check for unusual locations on Linux
    const alternativeLocations = [
      "/usr/bin/google-chrome",
      "/usr/bin/chromium",
      "/usr/bin/chromium-browser",
      "/snap/bin/chromium",
      "/snap/bin/google-chrome",
      "/opt/google/chrome/chrome",
    ];

    for (const location of alternativeLocations) {
      if (fs.existsSync(location)) {
        console.log(`Found browser at alternative location: ${location}`);
        return location;
      }
    }
  }

  throw new Error(
    `No browser executable found for platform ${platform}. Please specify a custom browser path.`
  );
}

/**
 * Sets up cleanup handlers for the browser instance
 * @param browser Browser instance
 * @param userDataDir Path to the user data directory to clean up
 */
function setupBrowserCleanupHandlers(
  browser: puppeteer.Browser,
  userDataDir: string
): void {
  browser.on("disconnected", () => {
    console.log(`Browser disconnected. Scheduling cleanup for: ${userDataDir}`);

    // Clear any existing cleanup timeout when browser is disconnected
    cancelScheduledCleanup();

    // Delayed cleanup to avoid conflicts with potential new browser instances
    setTimeout(() => {
      // Only remove the directory if no new browser has been launched
      if (!headlessBrowserInstance) {
        console.log(`Cleaning up temporary directory: ${userDataDir}`);
        try {
          fs.rmSync(userDataDir, { recursive: true, force: true });
          console.log(`Successfully removed directory: ${userDataDir}`);
        } catch (error) {
          console.error(`Failed to remove directory ${userDataDir}:`, error);
        }
      } else {
        console.log(
          `Skipping cleanup for ${userDataDir} as new browser instance is active`
        );
      }
    }, 5000); // 5-second delay for cleanup

    // Reset browser instance variables
    launchedBrowserWSEndpoint = null;
    headlessBrowserInstance = null;
  });
}

// ===== Cleanup Management =====

/**
 * Cancels any scheduled browser cleanup
 */
function cancelScheduledCleanup(): void {
  if (browserCleanupTimeout) {
    console.log("Cancelling scheduled browser cleanup");
    clearTimeout(browserCleanupTimeout);
    browserCleanupTimeout = null;
  }
}

/**
 * Schedules automatic cleanup of the browser instance after inactivity
 */
export function scheduleBrowserCleanup(): void {
  // Clear any existing timeout first
  cancelScheduledCleanup();

  // Only schedule cleanup if we have an active browser instance
  if (headlessBrowserInstance) {
    console.log(
      `Scheduling browser cleanup in ${BROWSER_CLEANUP_TIMEOUT / 1000} seconds`
    );

    browserCleanupTimeout = setTimeout(() => {
      console.log("Executing scheduled browser cleanup");
      if (headlessBrowserInstance) {
        console.log("Closing headless browser instance");
        headlessBrowserInstance.close();
        headlessBrowserInstance = null;
        launchedBrowserWSEndpoint = null;
      }
      browserCleanupTimeout = null;
    }, BROWSER_CLEANUP_TIMEOUT);
  }
}

// ===== Public Browser Connection API =====

/**
 * Connects to a headless browser for web operations
 * @param url The URL to navigate to
 * @param options Connection and emulation options
 * @returns Promise resolving to browser, port, and page objects
 */
export async function connectToHeadlessBrowser(
  url: string,
  options: {
    blockResources?: boolean;
    customResourceBlockList?: string[];
    emulateDevice?: "mobile" | "tablet" | "desktop";
    emulateNetworkCondition?: "slow3G" | "fast3G" | "4G" | "offline";
    viewport?: { width: number; height: number };
    locale?: string;
    timezoneId?: string;
    userAgent?: string;
    waitForSelector?: string;
    waitForTimeout?: number;
    cookies?: Array<{
      name: string;
      value: string;
      domain?: string;
      path?: string;
    }>;
    headers?: Record<string, string>;
  } = {}
): Promise<{
  browser: puppeteer.Browser;
  port: number;
  page: puppeteer.Page;
}> {
  console.log(
    `Connecting to headless browser for ${url}${
      options.blockResources ? " (blocking non-essential resources)" : ""
    }`
  );

  try {
    // Validate URL format
    try {
      new URL(url);
    } catch (e) {
      throw new Error(`Invalid URL format: ${url}`);
    }

    // Get or create a browser instance
    const browser = await getHeadlessBrowserInstance();

    if (!launchedBrowserWSEndpoint) {
      throw new Error("Failed to retrieve WebSocket endpoint for browser");
    }

    // Extract port from WebSocket endpoint
    const port = parseInt(
      launchedBrowserWSEndpoint.split(":")[2].split("/")[0]
    );

    // Always create a new page for each audit to avoid request interception conflicts
    console.log("Creating a new page for this audit");
    const page = await browser.newPage();

    // Set a longer timeout for navigation
    const navigationTimeout = 10000; // 10 seconds
    page.setDefaultNavigationTimeout(navigationTimeout);

    // Navigate to the URL
    console.log(`Navigating to ${url}`);
    await page.goto(url, {
      waitUntil: "networkidle2", // Wait until there are no more network connections for at least 500ms
      timeout: navigationTimeout,
    });

    // Set custom headers if provided
    if (options.headers && Object.keys(options.headers).length > 0) {
      await page.setExtraHTTPHeaders(options.headers);
      console.log("Set custom HTTP headers");
    }

    // Set cookies if provided
    if (options.cookies && options.cookies.length > 0) {
      const urlObj = new URL(url);
      const cookiesWithDomain = options.cookies.map((cookie) => ({
        ...cookie,
        domain: cookie.domain || urlObj.hostname,
        path: cookie.path || "/",
      }));
      await page.setCookie(...cookiesWithDomain);
      console.log(`Set ${options.cookies.length} cookies`);
    }

    // Set custom viewport if specified
    if (options.viewport) {
      await page.setViewport(options.viewport);
      console.log(
        `Set viewport to ${options.viewport.width}x${options.viewport.height}`
      );
    } else if (options.emulateDevice) {
      // Set common device emulation presets
      let viewport;
      let userAgent = options.userAgent;

      switch (options.emulateDevice) {
        case "mobile":
          viewport = {
            width: 375,
            height: 667,
            isMobile: true,
            hasTouch: true,
          };
          userAgent =
            userAgent ||
            "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)";
          break;
        case "tablet":
          viewport = {
            width: 768,
            height: 1024,
            isMobile: true,
            hasTouch: true,
          };
          userAgent =
            userAgent || "Mozilla/5.0 (iPad; CPU OS 13_2_3 like Mac OS X)";
          break;
        case "desktop":
        default:
          viewport = {
            width: 1280,
            height: 800,
            isMobile: false,
            hasTouch: false,
          };
          break;
      }

      await page.setViewport(viewport);
      if (userAgent) await page.setUserAgent(userAgent);

      console.log(`Emulating ${options.emulateDevice} device`);
    }

    // Set locale and timezone if provided
    if (options.locale) {
      await page.evaluateOnNewDocument((locale) => {
        Object.defineProperty(navigator, "language", { get: () => locale });
        Object.defineProperty(navigator, "languages", { get: () => [locale] });
      }, options.locale);
      console.log(`Set locale to ${options.locale}`);
    }

    if (options.timezoneId) {
      await page.emulateTimezone(options.timezoneId);
      console.log(`Set timezone to ${options.timezoneId}`);
    }

    // Emulate network conditions if specified
    if (options.emulateNetworkCondition) {
      // Define network condition types that match puppeteer's expected format
      interface PuppeteerNetworkConditions {
        offline: boolean;
        latency?: number;
        download?: number;
        upload?: number;
      }

      let networkConditions: PuppeteerNetworkConditions;

      switch (options.emulateNetworkCondition) {
        case "slow3G":
          networkConditions = {
            offline: false,
            latency: 400,
            download: (500 * 1024) / 8,
            upload: (500 * 1024) / 8,
          };
          break;
        case "fast3G":
          networkConditions = {
            offline: false,
            latency: 150,
            download: (1.5 * 1024 * 1024) / 8,
            upload: (750 * 1024) / 8,
          };
          break;
        case "4G":
          networkConditions = {
            offline: false,
            latency: 50,
            download: (4 * 1024 * 1024) / 8,
            upload: (2 * 1024 * 1024) / 8,
          };
          break;
        case "offline":
          networkConditions = { offline: true };
          break;
        default:
          networkConditions = { offline: false };
      }

      // @ts-ignore - Property might not be in types but is supported
      await page.emulateNetworkConditions(networkConditions);
      console.log(
        `Emulating ${options.emulateNetworkCondition} network conditions`
      );
    }

    // Check if we should block resources based on the options
    if (options.blockResources) {
      const resourceTypesToBlock = options.customResourceBlockList ||
        currentConfig.blockResourceTypes || ["image", "font", "media"];

      await page.setRequestInterception(true);
      page.on("request", (request) => {
        // Block unnecessary resources to speed up loading
        const resourceType = request.resourceType();
        if (resourceTypesToBlock.includes(resourceType)) {
          request.abort();
        } else {
          request.continue();
        }
      });

      console.log(
        `Blocking resource types: ${resourceTypesToBlock.join(", ")}`
      );
    }

    // Wait for a specific selector if requested
    if (options.waitForSelector) {
      try {
        console.log(`Waiting for selector: ${options.waitForSelector}`);
        await page.waitForSelector(options.waitForSelector, {
          timeout: options.waitForTimeout || 30000,
        });
      } catch (selectorError: any) {
        console.warn(
          `Failed to find selector "${options.waitForSelector}": ${selectorError.message}`
        );
        // Continue anyway, don't fail the whole operation
      }
    }

    return { browser, port, page };
  } catch (error) {
    console.error("Failed to connect to headless browser:", error);
    throw new Error(
      `Failed to connect to headless browser: ${
        error instanceof Error ? error.message : String(error)
      }`
    );
  }
}

```

--------------------------------------------------------------------------------
/chrome-extension/panel.js:
--------------------------------------------------------------------------------

```javascript
// Store settings
let settings = {
  logLimit: 50,
  queryLimit: 30000,
  stringSizeLimit: 500,
  showRequestHeaders: false,
  showResponseHeaders: false,
  maxLogSize: 20000,
  screenshotPath: "",
  // Add server connection settings
  serverHost: "localhost",
  serverPort: 3025,
  allowAutoPaste: false, // Default auto-paste setting
};

// Track connection status
let serverConnected = false;
let reconnectAttemptTimeout = null;
// Add a flag to track ongoing discovery operations
let isDiscoveryInProgress = false;
// Add an AbortController to cancel fetch operations
let discoveryController = null;

// Load saved settings on startup
chrome.storage.local.get(["browserConnectorSettings"], (result) => {
  if (result.browserConnectorSettings) {
    settings = { ...settings, ...result.browserConnectorSettings };
    updateUIFromSettings();
  }

  // Create connection status banner at the top
  createConnectionBanner();

  // Automatically discover server on panel load with quiet mode enabled
  discoverServer(true);
});

// Add listener for connection status updates from background script (page refresh events)
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "CONNECTION_STATUS_UPDATE") {
    console.log(
      `Received connection status update: ${
        message.isConnected ? "Connected" : "Disconnected"
      }`
    );

    // Update UI based on connection status
    if (message.isConnected) {
      // If already connected, just maintain the current state
      if (!serverConnected) {
        // Connection was re-established, update UI
        serverConnected = true;
        updateConnectionBanner(true, {
          name: "Browser Tools Server",
          version: "reconnected",
          host: settings.serverHost,
          port: settings.serverPort,
        });
      }
    } else {
      // Connection lost, update UI to show disconnected
      serverConnected = false;
      updateConnectionBanner(false, null);
    }
  }

  if (message.type === "INITIATE_AUTO_DISCOVERY") {
    console.log(
      `Initiating auto-discovery after page refresh (reason: ${message.reason})`
    );

    // For page refreshes or if forceRestart is set to true, always cancel any ongoing discovery and restart
    if (message.reason === "page_refresh" || message.forceRestart === true) {
      // Cancel any ongoing discovery operation
      cancelOngoingDiscovery();

      // Update UI to indicate we're starting a fresh scan
      if (connectionStatusDiv) {
        connectionStatusDiv.style.display = "block";
        if (statusIcon) statusIcon.className = "status-indicator";
        if (statusText)
          statusText.textContent =
            "Page refreshed. Restarting server discovery...";
      }

      // Always update the connection banner when a page refresh occurs
      updateConnectionBanner(false, null);

      // Start a new discovery process with quiet mode
      console.log("Starting fresh discovery after page refresh");
      discoverServer(true);
    }
    // For other types of auto-discovery requests, only start if not already in progress
    else if (!isDiscoveryInProgress) {
      // Use quiet mode for auto-discovery to minimize UI changes
      discoverServer(true);
    }
  }

  // Handle successful server validation
  if (message.type === "SERVER_VALIDATION_SUCCESS") {
    console.log(
      `Server validation successful: ${message.serverHost}:${message.serverPort}`
    );

    // Update the connection status banner
    serverConnected = true;
    updateConnectionBanner(true, message.serverInfo);

    // If we were showing the connection status dialog, we can hide it now
    if (connectionStatusDiv && connectionStatusDiv.style.display === "block") {
      connectionStatusDiv.style.display = "none";
    }
  }

  // Handle failed server validation
  if (message.type === "SERVER_VALIDATION_FAILED") {
    console.log(
      `Server validation failed: ${message.reason} - ${message.serverHost}:${message.serverPort}`
    );

    // Update the connection status
    serverConnected = false;
    updateConnectionBanner(false, null);

    // Start auto-discovery if this was a page refresh validation
    if (
      message.reason === "connection_error" ||
      message.reason === "http_error"
    ) {
      // If we're not already trying to discover the server, start the process
      if (!isDiscoveryInProgress) {
        console.log("Starting auto-discovery after validation failure");
        discoverServer(true);
      }
    }
  }

  // Handle successful WebSocket connection
  if (message.type === "WEBSOCKET_CONNECTED") {
    console.log(
      `WebSocket connected to ${message.serverHost}:${message.serverPort}`
    );

    // Update connection status if it wasn't already connected
    if (!serverConnected) {
      serverConnected = true;
      updateConnectionBanner(true, {
        name: "Browser Tools Server",
        version: "connected via WebSocket",
        host: message.serverHost,
        port: message.serverPort,
      });
    }
  }
});

// Create connection status banner
function createConnectionBanner() {
  // Check if banner already exists
  if (document.getElementById("connection-banner")) {
    return;
  }

  // Create the banner
  const banner = document.createElement("div");
  banner.id = "connection-banner";
  banner.style.cssText = `
    padding: 6px 0px; 
    margin-bottom: 4px;
    width: 40%; 
    display: flex; 
    flex-direction: column;
    align-items: flex-start; 
    background-color:rgba(0,0,0,0);
    border-radius: 11px;
    font-size: 11px;
    font-weight: 500;
    color: #ffffff;
  `;

  // Create reconnect button (now placed at the top)
  const reconnectButton = document.createElement("button");
  reconnectButton.id = "banner-reconnect-btn";
  reconnectButton.textContent = "Reconnect";
  reconnectButton.style.cssText = `
    background-color: #333333;
    color: #ffffff;
    border: 1px solid #444444;
    border-radius: 3px;
    padding: 2px 8px;
    font-size: 10px;
    cursor: pointer;
    margin-bottom: 6px;
    align-self: flex-start;
    display: none;
    transition: background-color 0.2s;
  `;
  reconnectButton.addEventListener("mouseover", () => {
    reconnectButton.style.backgroundColor = "#444444";
  });
  reconnectButton.addEventListener("mouseout", () => {
    reconnectButton.style.backgroundColor = "#333333";
  });
  reconnectButton.addEventListener("click", () => {
    // Hide the button while reconnecting
    reconnectButton.style.display = "none";
    reconnectButton.textContent = "Reconnecting...";

    // Update UI to show searching state
    updateConnectionBanner(false, null);

    // Try to discover server
    discoverServer(false);
  });

  // Create a container for the status indicator and text
  const statusContainer = document.createElement("div");
  statusContainer.style.cssText = `
    display: flex;
    align-items: center;
    width: 100%;
  `;

  // Create status indicator
  const indicator = document.createElement("div");
  indicator.id = "banner-status-indicator";
  indicator.style.cssText = `
    width: 6px; 
    height: 6px; 
    position: relative;
    top: 1px;
    border-radius: 50%; 
    background-color: #ccc; 
    margin-right: 8px; 
    flex-shrink: 0;
    transition: background-color 0.3s ease;
  `;

  // Create status text
  const statusText = document.createElement("div");
  statusText.id = "banner-status-text";
  statusText.textContent = "Searching for server...";
  statusText.style.cssText =
    "flex-grow: 1; font-weight: 400; letter-spacing: 0.1px; font-size: 11px;";

  // Add elements to statusContainer
  statusContainer.appendChild(indicator);
  statusContainer.appendChild(statusText);

  // Add elements to banner - reconnect button first, then status container
  banner.appendChild(reconnectButton);
  banner.appendChild(statusContainer);

  // Add banner to the beginning of the document body
  // This ensures it's the very first element
  document.body.prepend(banner);

  // Set initial state
  updateConnectionBanner(false, null);
}

// Update the connection banner with current status
function updateConnectionBanner(connected, serverInfo) {
  const indicator = document.getElementById("banner-status-indicator");
  const statusText = document.getElementById("banner-status-text");
  const banner = document.getElementById("connection-banner");
  const reconnectButton = document.getElementById("banner-reconnect-btn");

  if (!indicator || !statusText || !banner || !reconnectButton) return;

  if (connected && serverInfo) {
    // Connected state with server info
    indicator.style.backgroundColor = "#4CAF50"; // Green indicator
    statusText.style.color = "#ffffff"; // White text for contrast on black
    statusText.textContent = `Connected to ${serverInfo.name} v${serverInfo.version} at ${settings.serverHost}:${settings.serverPort}`;

    // Hide reconnect button when connected
    reconnectButton.style.display = "none";
  } else if (connected) {
    // Connected without server info
    indicator.style.backgroundColor = "#4CAF50"; // Green indicator
    statusText.style.color = "#ffffff"; // White text for contrast on black
    statusText.textContent = `Connected to server at ${settings.serverHost}:${settings.serverPort}`;

    // Hide reconnect button when connected
    reconnectButton.style.display = "none";
  } else {
    // Disconnected state
    indicator.style.backgroundColor = "#F44336"; // Red indicator
    statusText.style.color = "#ffffff"; // White text for contrast on black

    // Only show "searching" message if discovery is in progress
    if (isDiscoveryInProgress) {
      statusText.textContent = "Not connected to server. Searching...";
      // Hide reconnect button while actively searching
      reconnectButton.style.display = "none";
    } else {
      statusText.textContent = "Not connected to server.";
      // Show reconnect button above status message when disconnected and not searching
      reconnectButton.style.display = "block";
      reconnectButton.textContent = "Reconnect";
    }
  }
}

// Initialize UI elements
const logLimitInput = document.getElementById("log-limit");
const queryLimitInput = document.getElementById("query-limit");
const stringSizeLimitInput = document.getElementById("string-size-limit");
const showRequestHeadersCheckbox = document.getElementById(
  "show-request-headers"
);
const showResponseHeadersCheckbox = document.getElementById(
  "show-response-headers"
);
const maxLogSizeInput = document.getElementById("max-log-size");
const screenshotPathInput = document.getElementById("screenshot-path");
const captureScreenshotButton = document.getElementById("capture-screenshot");

// Server connection UI elements
const serverHostInput = document.getElementById("server-host");
const serverPortInput = document.getElementById("server-port");
const discoverServerButton = document.getElementById("discover-server");
const testConnectionButton = document.getElementById("test-connection");
const connectionStatusDiv = document.getElementById("connection-status");
const statusIcon = document.getElementById("status-icon");
const statusText = document.getElementById("status-text");

// Initialize collapsible advanced settings
const advancedSettingsHeader = document.getElementById(
  "advanced-settings-header"
);
const advancedSettingsContent = document.getElementById(
  "advanced-settings-content"
);
const chevronIcon = advancedSettingsHeader.querySelector(".chevron");

advancedSettingsHeader.addEventListener("click", () => {
  advancedSettingsContent.classList.toggle("visible");
  chevronIcon.classList.toggle("open");
});

// Get all inputs by ID
const allowAutoPasteCheckbox = document.getElementById("allow-auto-paste");

// Update UI from settings
function updateUIFromSettings() {
  logLimitInput.value = settings.logLimit;
  queryLimitInput.value = settings.queryLimit;
  stringSizeLimitInput.value = settings.stringSizeLimit;
  showRequestHeadersCheckbox.checked = settings.showRequestHeaders;
  showResponseHeadersCheckbox.checked = settings.showResponseHeaders;
  maxLogSizeInput.value = settings.maxLogSize;
  screenshotPathInput.value = settings.screenshotPath;
  serverHostInput.value = settings.serverHost;
  serverPortInput.value = settings.serverPort;
  allowAutoPasteCheckbox.checked = settings.allowAutoPaste;
}

// Save settings
function saveSettings() {
  chrome.storage.local.set({ browserConnectorSettings: settings });
  // Notify devtools.js about settings change
  chrome.runtime.sendMessage({
    type: "SETTINGS_UPDATED",
    settings,
  });
}

// Add event listeners for all inputs
logLimitInput.addEventListener("change", (e) => {
  settings.logLimit = parseInt(e.target.value, 10);
  saveSettings();
});

queryLimitInput.addEventListener("change", (e) => {
  settings.queryLimit = parseInt(e.target.value, 10);
  saveSettings();
});

stringSizeLimitInput.addEventListener("change", (e) => {
  settings.stringSizeLimit = parseInt(e.target.value, 10);
  saveSettings();
});

showRequestHeadersCheckbox.addEventListener("change", (e) => {
  settings.showRequestHeaders = e.target.checked;
  saveSettings();
});

showResponseHeadersCheckbox.addEventListener("change", (e) => {
  settings.showResponseHeaders = e.target.checked;
  saveSettings();
});

maxLogSizeInput.addEventListener("change", (e) => {
  settings.maxLogSize = parseInt(e.target.value, 10);
  saveSettings();
});

screenshotPathInput.addEventListener("change", (e) => {
  settings.screenshotPath = e.target.value;
  saveSettings();
});

// Add event listeners for server settings
serverHostInput.addEventListener("change", (e) => {
  settings.serverHost = e.target.value;
  saveSettings();
  // Automatically test connection when host is changed
  testConnection(settings.serverHost, settings.serverPort);
});

serverPortInput.addEventListener("change", (e) => {
  settings.serverPort = parseInt(e.target.value, 10);
  saveSettings();
  // Automatically test connection when port is changed
  testConnection(settings.serverHost, settings.serverPort);
});

// Add event listener for auto-paste checkbox
allowAutoPasteCheckbox.addEventListener("change", (e) => {
  settings.allowAutoPaste = e.target.checked;
  saveSettings();
});

// Function to cancel any ongoing discovery operations
function cancelOngoingDiscovery() {
  if (isDiscoveryInProgress) {
    console.log("Cancelling ongoing discovery operation");

    // Abort any fetch requests in progress
    if (discoveryController) {
      try {
        discoveryController.abort();
      } catch (error) {
        console.error("Error aborting discovery controller:", error);
      }
      discoveryController = null;
    }

    // Reset the discovery status
    isDiscoveryInProgress = false;

    // Update UI to indicate the operation was cancelled
    if (
      statusText &&
      connectionStatusDiv &&
      connectionStatusDiv.style.display === "block"
    ) {
      statusText.textContent = "Server discovery operation cancelled";
    }

    // Clear any pending network timeouts that might be part of the discovery process
    clearTimeout(reconnectAttemptTimeout);
    reconnectAttemptTimeout = null;

    console.log("Discovery operation cancelled successfully");
  }
}

// Test server connection
testConnectionButton.addEventListener("click", async () => {
  // Cancel any ongoing discovery operations before testing
  cancelOngoingDiscovery();
  await testConnection(settings.serverHost, settings.serverPort);
});

// Function to test server connection
async function testConnection(host, port) {
  // Cancel any ongoing discovery operations
  cancelOngoingDiscovery();

  connectionStatusDiv.style.display = "block";
  statusIcon.className = "status-indicator";
  statusText.textContent = "Testing connection...";

  try {
    // Use the identity endpoint instead of .port for more reliable validation
    const response = await fetch(`http://${host}:${port}/.identity`, {
      signal: AbortSignal.timeout(5000), // 5 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") {
        statusIcon.className = "status-indicator status-disconnected";
        statusText.textContent = `Connection failed: Found a server at ${host}:${port} but it's not the Browser Tools server`;
        serverConnected = false;
        updateConnectionBanner(false, null);
        scheduleReconnectAttempt();
        return false;
      }

      statusIcon.className = "status-indicator status-connected";
      statusText.textContent = `Connected successfully to ${identity.name} v${identity.version} at ${host}:${port}`;
      serverConnected = true;
      updateConnectionBanner(true, identity);

      // Clear any scheduled reconnect attempts
      if (reconnectAttemptTimeout) {
        clearTimeout(reconnectAttemptTimeout);
        reconnectAttemptTimeout = null;
      }

      // Update settings if different port was discovered
      if (parseInt(identity.port, 10) !== port) {
        console.log(`Detected different port: ${identity.port}`);
        settings.serverPort = parseInt(identity.port, 10);
        serverPortInput.value = settings.serverPort;
        saveSettings();
      }

      return true;
    } else {
      statusIcon.className = "status-indicator status-disconnected";
      statusText.textContent = `Connection failed: Server returned ${response.status}`;
      serverConnected = false;

      // Make sure isDiscoveryInProgress is false so the reconnect button will show
      isDiscoveryInProgress = false;

      // Now update the connection banner to show the reconnect button
      updateConnectionBanner(false, null);
      scheduleReconnectAttempt();
      return false;
    }
  } catch (error) {
    statusIcon.className = "status-indicator status-disconnected";
    statusText.textContent = `Connection failed: ${error.message}`;
    serverConnected = false;

    // Make sure isDiscoveryInProgress is false so the reconnect button will show
    isDiscoveryInProgress = false;

    // Now update the connection banner to show the reconnect button
    updateConnectionBanner(false, null);
    scheduleReconnectAttempt();
    return false;
  }
}

// Schedule a reconnect attempt if server isn't found
function scheduleReconnectAttempt() {
  // Clear any existing reconnect timeout
  if (reconnectAttemptTimeout) {
    clearTimeout(reconnectAttemptTimeout);
  }

  // Schedule a reconnect attempt in 30 seconds
  reconnectAttemptTimeout = setTimeout(() => {
    console.log("Attempting to reconnect to server...");
    // Only show minimal UI during auto-reconnect
    discoverServer(true);
  }, 30000); // 30 seconds
}

// Helper function to try connecting to a server
async function tryServerConnection(host, port) {
  try {
    // Check if the discovery process was cancelled
    if (!isDiscoveryInProgress) {
      return false;
    }

    // Create a local timeout that won't abort the entire discovery process
    const controller = new AbortController();
    const timeoutId = setTimeout(() => {
      controller.abort();
    }, 500); // 500ms timeout for each connection attempt

    try {
      // Use identity endpoint for validation
      const response = await fetch(`http://${host}:${port}/.identity`, {
        // Use a local controller for this specific request timeout
        // but also respect the global discovery cancellation
        signal: discoveryController
          ? AbortSignal.any([controller.signal, discoveryController.signal])
          : controller.signal,
      });

      clearTimeout(timeoutId);

      // Check again if discovery was cancelled during the fetch
      if (!isDiscoveryInProgress) {
        return false;
      }

      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(
            `Found a server at ${host}:${port} but it's not the Browser Tools server`
          );
          return false;
        }

        console.log(`Successfully found server at ${host}:${port}`);

        // Update settings with discovered server
        settings.serverHost = host;
        settings.serverPort = parseInt(identity.port, 10);
        serverHostInput.value = settings.serverHost;
        serverPortInput.value = settings.serverPort;
        saveSettings();

        statusIcon.className = "status-indicator status-connected";
        statusText.textContent = `Discovered ${identity.name} v${identity.version} at ${host}:${identity.port}`;

        // Update connection banner with server info
        updateConnectionBanner(true, identity);

        // Update connection status
        serverConnected = true;

        // Clear any scheduled reconnect attempts
        if (reconnectAttemptTimeout) {
          clearTimeout(reconnectAttemptTimeout);
          reconnectAttemptTimeout = null;
        }

        // End the discovery process
        isDiscoveryInProgress = false;

        // Successfully found server
        return true;
      }

      return false;
    } finally {
      clearTimeout(timeoutId);
    }
  } catch (error) {
    // Ignore connection errors during discovery
    // But check if it was an abort (cancellation)
    if (error.name === "AbortError") {
      // Check if this was due to the global discovery cancellation
      if (discoveryController && discoveryController.signal.aborted) {
        console.log("Connection attempt aborted by global cancellation");
        return "aborted";
      }
      // Otherwise it was just a timeout for this specific connection attempt
      return false;
    }
    console.log(`Connection error for ${host}:${port}: ${error.message}`);
    return false;
  }
}

// Server discovery function (extracted to be reusable)
async function discoverServer(quietMode = false) {
  // Cancel any ongoing discovery operations before starting a new one
  cancelOngoingDiscovery();

  // Create a new AbortController for this discovery process
  discoveryController = new AbortController();
  isDiscoveryInProgress = true;

  // In quiet mode, we don't show the connection status until we either succeed or fail completely
  if (!quietMode) {
    connectionStatusDiv.style.display = "block";
    statusIcon.className = "status-indicator";
    statusText.textContent = "Discovering server...";
  }

  // Always update the connection banner
  updateConnectionBanner(false, null);

  try {
    console.log("Starting server discovery process");

    // Add an early cancellation listener that will respond to page navigation/refresh
    discoveryController.signal.addEventListener("abort", () => {
      console.log("Discovery aborted via AbortController signal");
      isDiscoveryInProgress = false;
    });

    // Common IPs to try (in order of likelihood)
    const hosts = ["localhost", "127.0.0.1"];

    // Add the current configured host if it's not already in the list
    if (
      !hosts.includes(settings.serverHost) &&
      settings.serverHost !== "0.0.0.0"
    ) {
      hosts.unshift(settings.serverHost); // Put at the beginning for priority
    }

    // Add common local network IPs
    const commonLocalIps = ["192.168.0.", "192.168.1.", "10.0.0.", "10.0.1."];
    for (const prefix of commonLocalIps) {
      for (let i = 1; i <= 5; i++) {
        // Reduced from 10 to 5 for efficiency
        hosts.push(`${prefix}${i}`);
      }
    }

    // Build port list in a smart order:
    // 1. Start with current configured port
    // 2. Add default port (3025)
    // 3. Add sequential ports around the default (for fallback detection)
    const ports = [];

    // Current configured port gets highest priority
    const configuredPort = parseInt(settings.serverPort, 10);
    ports.push(configuredPort);

    // Add default port if it's not the same as configured
    if (configuredPort !== 3025) {
      ports.push(3025);
    }

    // Add sequential fallback ports (from default up to default+10)
    for (let p = 3026; p <= 3035; p++) {
      if (p !== configuredPort) {
        // Avoid duplicates
        ports.push(p);
      }
    }

    // Remove duplicates
    const uniquePorts = [...new Set(ports)];
    console.log("Will check ports:", uniquePorts);

    // Create a progress indicator
    let progress = 0;
    let totalChecked = 0;

    // Phase 1: Try the most likely combinations first (current host:port and localhost variants)
    console.log("Starting Phase 1: Quick check of high-priority hosts/ports");
    const priorityHosts = hosts.slice(0, 2); // First two hosts are highest priority
    for (const host of priorityHosts) {
      // Check if discovery was cancelled
      if (!isDiscoveryInProgress) {
        console.log("Discovery process was cancelled during Phase 1");
        return false;
      }

      // Try configured port first
      totalChecked++;
      if (!quietMode) {
        statusText.textContent = `Checking ${host}:${uniquePorts[0]}...`;
      }
      console.log(`Checking ${host}:${uniquePorts[0]}...`);
      const result = await tryServerConnection(host, uniquePorts[0]);

      // Check for cancellation or success
      if (result === "aborted" || !isDiscoveryInProgress) {
        console.log("Discovery process was cancelled");
        return false;
      } else if (result === true) {
        console.log("Server found in priority check");
        if (quietMode) {
          // In quiet mode, only show the connection banner but hide the status box
          connectionStatusDiv.style.display = "none";
        }
        return true; // Successfully found server
      }

      // Then try default port if different
      if (uniquePorts.length > 1) {
        // Check if discovery was cancelled
        if (!isDiscoveryInProgress) {
          console.log("Discovery process was cancelled");
          return false;
        }

        totalChecked++;
        if (!quietMode) {
          statusText.textContent = `Checking ${host}:${uniquePorts[1]}...`;
        }
        console.log(`Checking ${host}:${uniquePorts[1]}...`);
        const result = await tryServerConnection(host, uniquePorts[1]);

        // Check for cancellation or success
        if (result === "aborted" || !isDiscoveryInProgress) {
          console.log("Discovery process was cancelled");
          return false;
        } else if (result === true) {
          console.log("Server found in priority check");
          if (quietMode) {
            // In quiet mode, only show the connection banner but hide the status box
            connectionStatusDiv.style.display = "none";
          }
          return true; // Successfully found server
        }
      }
    }

    // If we're in quiet mode and the quick checks failed, show the status now
    // as we move into more intensive scanning
    if (quietMode) {
      connectionStatusDiv.style.display = "block";
      statusIcon.className = "status-indicator";
      statusText.textContent = "Searching for server...";
    }

    // Phase 2: Systematic scan of all combinations
    const totalAttempts = hosts.length * uniquePorts.length;
    console.log(
      `Starting Phase 2: Full scan (${totalAttempts} total combinations)`
    );
    statusText.textContent = `Quick check failed. Starting full scan (${totalChecked}/${totalAttempts})...`;

    // First, scan through all ports on localhost/127.0.0.1 to find fallback ports quickly
    const localHosts = ["localhost", "127.0.0.1"];
    for (const host of localHosts) {
      // Skip the first two ports on localhost if we already checked them in Phase 1
      const portsToCheck = uniquePorts.slice(
        localHosts.includes(host) && priorityHosts.includes(host) ? 2 : 0
      );

      for (const port of portsToCheck) {
        // Check if discovery was cancelled
        if (!isDiscoveryInProgress) {
          console.log("Discovery process was cancelled during local port scan");
          return false;
        }

        // Update progress
        progress++;
        totalChecked++;
        statusText.textContent = `Scanning local ports... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;
        console.log(`Checking ${host}:${port}...`);

        const result = await tryServerConnection(host, port);

        // Check for cancellation or success
        if (result === "aborted" || !isDiscoveryInProgress) {
          console.log("Discovery process was cancelled");
          return false;
        } else if (result === true) {
          console.log(`Server found at ${host}:${port}`);
          return true; // Successfully found server
        }
      }
    }

    // Then scan all the remaining host/port combinations
    for (const host of hosts) {
      // Skip hosts we already checked
      if (localHosts.includes(host)) {
        continue;
      }

      for (const port of uniquePorts) {
        // Check if discovery was cancelled
        if (!isDiscoveryInProgress) {
          console.log("Discovery process was cancelled during remote scan");
          return false;
        }

        // Update progress
        progress++;
        totalChecked++;
        statusText.textContent = `Scanning remote hosts... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;
        console.log(`Checking ${host}:${port}...`);

        const result = await tryServerConnection(host, port);

        // Check for cancellation or success
        if (result === "aborted" || !isDiscoveryInProgress) {
          console.log("Discovery process was cancelled");
          return false;
        } else if (result === true) {
          console.log(`Server found at ${host}:${port}`);
          return true; // Successfully found server
        }
      }
    }

    console.log(
      `Discovery process completed, checked ${totalChecked} combinations, no server found`
    );
    // If we get here, no server was found
    statusIcon.className = "status-indicator status-disconnected";
    statusText.textContent =
      "No server found. Please check server is running and try again.";

    serverConnected = false;

    // End the discovery process first before updating the banner
    isDiscoveryInProgress = false;

    // Update the connection banner to show the reconnect button
    updateConnectionBanner(false, null);

    // Schedule a reconnect attempt
    scheduleReconnectAttempt();

    return false;
  } catch (error) {
    console.error("Error during server discovery:", error);
    statusIcon.className = "status-indicator status-disconnected";
    statusText.textContent = `Error discovering server: ${error.message}`;

    serverConnected = false;

    // End the discovery process first before updating the banner
    isDiscoveryInProgress = false;

    // Update the connection banner to show the reconnect button
    updateConnectionBanner(false, null);

    // Schedule a reconnect attempt
    scheduleReconnectAttempt();

    return false;
  } finally {
    console.log("Discovery process finished");
    // Always clean up, even if there was an error
    if (discoveryController) {
      discoveryController = null;
    }
  }
}

// Bind discover server button to the extracted function
discoverServerButton.addEventListener("click", () => discoverServer(false));

// Screenshot capture functionality
captureScreenshotButton.addEventListener("click", () => {
  captureScreenshotButton.textContent = "Capturing...";

  // Send message to background script to capture screenshot
  chrome.runtime.sendMessage(
    {
      type: "CAPTURE_SCREENSHOT",
      tabId: chrome.devtools.inspectedWindow.tabId,
      screenshotPath: settings.screenshotPath,
    },
    (response) => {
      console.log("Screenshot capture response:", response);
      if (!response) {
        captureScreenshotButton.textContent = "Failed to capture!";
        console.error("Screenshot capture failed: No response received");
      } else if (!response.success) {
        captureScreenshotButton.textContent = "Failed to capture!";
        console.error("Screenshot capture failed:", response.error);
      } else {
        captureScreenshotButton.textContent = `Captured: ${response.title}`;
        console.log("Screenshot captured successfully:", response.path);
      }
      setTimeout(() => {
        captureScreenshotButton.textContent = "Capture Screenshot";
      }, 2000);
    }
  );
});

// Add wipe logs functionality
const wipeLogsButton = document.getElementById("wipe-logs");
wipeLogsButton.addEventListener("click", () => {
  const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/wipelogs`;
  console.log(`Sending wipe request to ${serverUrl}`);

  fetch(serverUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
  })
    .then((response) => response.json())
    .then((result) => {
      console.log("Logs wiped successfully:", result.message);
      wipeLogsButton.textContent = "Logs Wiped!";
      setTimeout(() => {
        wipeLogsButton.textContent = "Wipe All Logs";
      }, 2000);
    })
    .catch((error) => {
      console.error("Failed to wipe logs:", error);
      wipeLogsButton.textContent = "Failed to Wipe Logs";
      setTimeout(() => {
        wipeLogsButton.textContent = "Wipe All Logs";
      }, 2000);
    });
});

```
Page 1/2FirstPrevNextLast