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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/browser-tools-server/browser-connector.ts:
--------------------------------------------------------------------------------

```typescript
   1 | #!/usr/bin/env node
   2 | 
   3 | import express from "express";
   4 | import cors from "cors";
   5 | import bodyParser from "body-parser";
   6 | import { tokenizeAndEstimateCost } from "llm-cost";
   7 | import { WebSocketServer, WebSocket } from "ws";
   8 | import fs from "fs";
   9 | import path from "path";
  10 | import { IncomingMessage } from "http";
  11 | import { Socket } from "net";
  12 | import os from "os";
  13 | import { exec } from "child_process";
  14 | import {
  15 |   runPerformanceAudit,
  16 |   runAccessibilityAudit,
  17 |   runSEOAudit,
  18 |   AuditCategory,
  19 |   LighthouseReport,
  20 | } from "./lighthouse/index.js";
  21 | import * as net from "net";
  22 | import { runBestPracticesAudit } from "./lighthouse/best-practices.js";
  23 | 
  24 | /**
  25 |  * Converts a file path to the appropriate format for the current platform
  26 |  * Handles Windows, WSL, macOS and Linux path formats
  27 |  *
  28 |  * @param inputPath - The path to convert
  29 |  * @returns The converted path appropriate for the current platform
  30 |  */
  31 | function convertPathForCurrentPlatform(inputPath: string): string {
  32 |   const platform = os.platform();
  33 | 
  34 |   // If no path provided, return as is
  35 |   if (!inputPath) return inputPath;
  36 | 
  37 |   console.log(`Converting path "${inputPath}" for platform: ${platform}`);
  38 | 
  39 |   // Windows-specific conversion
  40 |   if (platform === "win32") {
  41 |     // Convert forward slashes to backslashes
  42 |     return inputPath.replace(/\//g, "\\");
  43 |   }
  44 | 
  45 |   // Linux/Mac-specific conversion
  46 |   if (platform === "linux" || platform === "darwin") {
  47 |     // Check if this is a Windows UNC path (starts with \\)
  48 |     if (inputPath.startsWith("\\\\") || inputPath.includes("\\")) {
  49 |       // Check if this is a WSL path (contains wsl.localhost or wsl$)
  50 |       if (inputPath.includes("wsl.localhost") || inputPath.includes("wsl$")) {
  51 |         // Extract the path after the distribution name
  52 |         // Handle both \\wsl.localhost\Ubuntu\path and \\wsl$\Ubuntu\path formats
  53 |         const parts = inputPath.split("\\").filter((part) => part.length > 0);
  54 |         console.log("Path parts:", parts);
  55 | 
  56 |         // Find the index after the distribution name
  57 |         const distNames = [
  58 |           "Ubuntu",
  59 |           "Debian",
  60 |           "kali",
  61 |           "openSUSE",
  62 |           "SLES",
  63 |           "Fedora",
  64 |         ];
  65 | 
  66 |         // Find the distribution name in the path
  67 |         let distIndex = -1;
  68 |         for (const dist of distNames) {
  69 |           const index = parts.findIndex(
  70 |             (part) => part === dist || part.toLowerCase() === dist.toLowerCase()
  71 |           );
  72 |           if (index !== -1) {
  73 |             distIndex = index;
  74 |             break;
  75 |           }
  76 |         }
  77 | 
  78 |         if (distIndex !== -1 && distIndex + 1 < parts.length) {
  79 |           // Reconstruct the path as a native Linux path
  80 |           const linuxPath = "/" + parts.slice(distIndex + 1).join("/");
  81 |           console.log(
  82 |             `Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`
  83 |           );
  84 |           return linuxPath;
  85 |         }
  86 | 
  87 |         // If we couldn't find a distribution name but it's clearly a WSL path,
  88 |         // try to extract everything after wsl.localhost or wsl$
  89 |         const wslIndex = parts.findIndex(
  90 |           (part) =>
  91 |             part === "wsl.localhost" ||
  92 |             part === "wsl$" ||
  93 |             part.toLowerCase() === "wsl.localhost" ||
  94 |             part.toLowerCase() === "wsl$"
  95 |         );
  96 | 
  97 |         if (wslIndex !== -1 && wslIndex + 2 < parts.length) {
  98 |           // Skip the WSL prefix and distribution name
  99 |           const linuxPath = "/" + parts.slice(wslIndex + 2).join("/");
 100 |           console.log(
 101 |             `Converted Windows WSL path "${inputPath}" to Linux path "${linuxPath}"`
 102 |           );
 103 |           return linuxPath;
 104 |         }
 105 |       }
 106 | 
 107 |       // For non-WSL Windows paths, just normalize the slashes
 108 |       const normalizedPath = inputPath
 109 |         .replace(/\\\\/g, "/")
 110 |         .replace(/\\/g, "/");
 111 |       console.log(
 112 |         `Converted Windows UNC path "${inputPath}" to "${normalizedPath}"`
 113 |       );
 114 |       return normalizedPath;
 115 |     }
 116 | 
 117 |     // Handle Windows drive letters (e.g., C:\path\to\file)
 118 |     if (/^[A-Z]:\\/i.test(inputPath)) {
 119 |       // Convert Windows drive path to Linux/Mac compatible path
 120 |       const normalizedPath = inputPath
 121 |         .replace(/^[A-Z]:\\/i, "/")
 122 |         .replace(/\\/g, "/");
 123 |       console.log(
 124 |         `Converted Windows drive path "${inputPath}" to "${normalizedPath}"`
 125 |       );
 126 |       return normalizedPath;
 127 |     }
 128 |   }
 129 | 
 130 |   // Return the original path if no conversion was needed or possible
 131 |   return inputPath;
 132 | }
 133 | 
 134 | // Function to get default downloads folder
 135 | function getDefaultDownloadsFolder(): string {
 136 |   const homeDir = os.homedir();
 137 |   // Downloads folder is typically the same path on Windows, macOS, and Linux
 138 |   const downloadsPath = path.join(homeDir, "Downloads", "mcp-screenshots");
 139 |   return downloadsPath;
 140 | }
 141 | 
 142 | // We store logs in memory
 143 | const consoleLogs: any[] = [];
 144 | const consoleErrors: any[] = [];
 145 | const networkErrors: any[] = [];
 146 | const networkSuccess: any[] = [];
 147 | const allXhr: any[] = [];
 148 | 
 149 | // Store the current URL from the extension
 150 | let currentUrl: string = "";
 151 | 
 152 | // Store the current tab ID from the extension
 153 | let currentTabId: string | number | null = null;
 154 | 
 155 | // Add settings state
 156 | let currentSettings = {
 157 |   logLimit: 50,
 158 |   queryLimit: 30000,
 159 |   showRequestHeaders: false,
 160 |   showResponseHeaders: false,
 161 |   model: "claude-3-sonnet",
 162 |   stringSizeLimit: 500,
 163 |   maxLogSize: 20000,
 164 |   screenshotPath: getDefaultDownloadsFolder(),
 165 |   // Add server host configuration
 166 |   serverHost: process.env.SERVER_HOST || "0.0.0.0", // Default to all interfaces
 167 | };
 168 | 
 169 | // Add new storage for selected element
 170 | let selectedElement: any = null;
 171 | 
 172 | // Add new state for tracking screenshot requests
 173 | interface ScreenshotCallback {
 174 |   resolve: (value: {
 175 |     data: string;
 176 |     path?: string;
 177 |     autoPaste?: boolean;
 178 |   }) => void;
 179 |   reject: (reason: Error) => void;
 180 | }
 181 | 
 182 | const screenshotCallbacks = new Map<string, ScreenshotCallback>();
 183 | 
 184 | // Function to get available port starting with the given port
 185 | async function getAvailablePort(
 186 |   startPort: number,
 187 |   maxAttempts: number = 10
 188 | ): Promise<number> {
 189 |   let currentPort = startPort;
 190 |   let attempts = 0;
 191 | 
 192 |   while (attempts < maxAttempts) {
 193 |     try {
 194 |       // Try to create a server on the current port
 195 |       // We'll use a raw Node.js net server for just testing port availability
 196 |       await new Promise<void>((resolve, reject) => {
 197 |         const testServer = net.createServer();
 198 | 
 199 |         // Handle errors (e.g., port in use)
 200 |         testServer.once("error", (err: any) => {
 201 |           if (err.code === "EADDRINUSE") {
 202 |             console.log(`Port ${currentPort} is in use, trying next port...`);
 203 |             currentPort++;
 204 |             attempts++;
 205 |             resolve(); // Continue to next iteration
 206 |           } else {
 207 |             reject(err); // Different error, propagate it
 208 |           }
 209 |         });
 210 | 
 211 |         // If we can listen, the port is available
 212 |         testServer.once("listening", () => {
 213 |           // Make sure to close the server to release the port
 214 |           testServer.close(() => {
 215 |             console.log(`Found available port: ${currentPort}`);
 216 |             resolve();
 217 |           });
 218 |         });
 219 | 
 220 |         // Try to listen on the current port
 221 |         testServer.listen(currentPort, currentSettings.serverHost);
 222 |       });
 223 | 
 224 |       // If we reach here without incrementing the port, it means the port is available
 225 |       return currentPort;
 226 |     } catch (error: any) {
 227 |       console.error(`Error checking port ${currentPort}:`, error);
 228 |       // For non-EADDRINUSE errors, try the next port
 229 |       currentPort++;
 230 |       attempts++;
 231 |     }
 232 |   }
 233 | 
 234 |   // If we've exhausted all attempts, throw an error
 235 |   throw new Error(
 236 |     `Could not find an available port after ${maxAttempts} attempts starting from ${startPort}`
 237 |   );
 238 | }
 239 | 
 240 | // Start with requested port and find an available one
 241 | const REQUESTED_PORT = parseInt(process.env.PORT || "3025", 10);
 242 | let PORT = REQUESTED_PORT;
 243 | 
 244 | // Create application and initialize middleware
 245 | const app = express();
 246 | app.use(cors());
 247 | // Increase JSON body parser limit to 50MB to handle large screenshots
 248 | app.use(bodyParser.json({ limit: "50mb" }));
 249 | app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
 250 | 
 251 | // Helper to recursively truncate strings in any data structure
 252 | function truncateStringsInData(data: any, maxLength: number): any {
 253 |   if (typeof data === "string") {
 254 |     return data.length > maxLength
 255 |       ? data.substring(0, maxLength) + "... (truncated)"
 256 |       : data;
 257 |   }
 258 | 
 259 |   if (Array.isArray(data)) {
 260 |     return data.map((item) => truncateStringsInData(item, maxLength));
 261 |   }
 262 | 
 263 |   if (typeof data === "object" && data !== null) {
 264 |     const result: any = {};
 265 |     for (const [key, value] of Object.entries(data)) {
 266 |       result[key] = truncateStringsInData(value, maxLength);
 267 |     }
 268 |     return result;
 269 |   }
 270 | 
 271 |   return data;
 272 | }
 273 | 
 274 | // Helper to safely parse and process JSON strings
 275 | function processJsonString(jsonString: string, maxLength: number): string {
 276 |   try {
 277 |     // Try to parse the string as JSON
 278 |     const parsed = JSON.parse(jsonString);
 279 |     // Process any strings within the parsed JSON
 280 |     const processed = truncateStringsInData(parsed, maxLength);
 281 |     // Stringify the processed data
 282 |     return JSON.stringify(processed);
 283 |   } catch (e) {
 284 |     // If it's not valid JSON, treat it as a regular string
 285 |     return truncateStringsInData(jsonString, maxLength);
 286 |   }
 287 | }
 288 | 
 289 | // Helper to process logs based on settings
 290 | function processLogsWithSettings(logs: any[]) {
 291 |   return logs.map((log) => {
 292 |     const processedLog = { ...log };
 293 | 
 294 |     if (log.type === "network-request") {
 295 |       // Handle headers visibility
 296 |       if (!currentSettings.showRequestHeaders) {
 297 |         delete processedLog.requestHeaders;
 298 |       }
 299 |       if (!currentSettings.showResponseHeaders) {
 300 |         delete processedLog.responseHeaders;
 301 |       }
 302 |     }
 303 | 
 304 |     return processedLog;
 305 |   });
 306 | }
 307 | 
 308 | // Helper to calculate size of a log entry
 309 | function calculateLogSize(log: any): number {
 310 |   return JSON.stringify(log).length;
 311 | }
 312 | 
 313 | // Helper to truncate logs based on character limit
 314 | function truncateLogsToQueryLimit(logs: any[]): any[] {
 315 |   if (logs.length === 0) return logs;
 316 | 
 317 |   // First process logs according to current settings
 318 |   const processedLogs = processLogsWithSettings(logs);
 319 | 
 320 |   let currentSize = 0;
 321 |   const result = [];
 322 | 
 323 |   for (const log of processedLogs) {
 324 |     const logSize = calculateLogSize(log);
 325 | 
 326 |     // Check if adding this log would exceed the limit
 327 |     if (currentSize + logSize > currentSettings.queryLimit) {
 328 |       console.log(
 329 |         `Reached query limit (${currentSize}/${currentSettings.queryLimit}), truncating logs`
 330 |       );
 331 |       break;
 332 |     }
 333 | 
 334 |     // Add log and update size
 335 |     result.push(log);
 336 |     currentSize += logSize;
 337 |     console.log(`Added log of size ${logSize}, total size now: ${currentSize}`);
 338 |   }
 339 | 
 340 |   return result;
 341 | }
 342 | 
 343 | // Endpoint for the extension to POST data
 344 | app.post("/extension-log", (req, res) => {
 345 |   console.log("\n=== Received Extension Log ===");
 346 |   console.log("Request body:", {
 347 |     dataType: req.body.data?.type,
 348 |     timestamp: req.body.data?.timestamp,
 349 |     hasSettings: !!req.body.settings,
 350 |   });
 351 | 
 352 |   const { data, settings } = req.body;
 353 | 
 354 |   // Update settings if provided
 355 |   if (settings) {
 356 |     console.log("Updating settings:", settings);
 357 |     currentSettings = {
 358 |       ...currentSettings,
 359 |       ...settings,
 360 |     };
 361 |   }
 362 | 
 363 |   if (!data) {
 364 |     console.log("Warning: No data received in log request");
 365 |     res.status(400).json({ status: "error", message: "No data provided" });
 366 |     return;
 367 |   }
 368 | 
 369 |   console.log(`Processing ${data.type} log entry`);
 370 | 
 371 |   switch (data.type) {
 372 |     case "page-navigated":
 373 |       // Handle page navigation event via HTTP POST
 374 |       // Note: This is also handled in the WebSocket message handler
 375 |       // as the extension may send navigation events through either channel
 376 |       console.log("Received page navigation event with URL:", data.url);
 377 |       currentUrl = data.url;
 378 | 
 379 |       // Also update the tab ID if provided
 380 |       if (data.tabId) {
 381 |         console.log("Updating tab ID from page navigation event:", data.tabId);
 382 |         currentTabId = data.tabId;
 383 |       }
 384 | 
 385 |       console.log("Updated current URL:", currentUrl);
 386 |       break;
 387 |     case "console-log":
 388 |       console.log("Adding console log:", {
 389 |         level: data.level,
 390 |         message:
 391 |           data.message?.substring(0, 100) +
 392 |           (data.message?.length > 100 ? "..." : ""),
 393 |         timestamp: data.timestamp,
 394 |       });
 395 |       consoleLogs.push(data);
 396 |       if (consoleLogs.length > currentSettings.logLimit) {
 397 |         console.log(
 398 |           `Console logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`
 399 |         );
 400 |         consoleLogs.shift();
 401 |       }
 402 |       break;
 403 |     case "console-error":
 404 |       console.log("Adding console error:", {
 405 |         level: data.level,
 406 |         message:
 407 |           data.message?.substring(0, 100) +
 408 |           (data.message?.length > 100 ? "..." : ""),
 409 |         timestamp: data.timestamp,
 410 |       });
 411 |       consoleErrors.push(data);
 412 |       if (consoleErrors.length > currentSettings.logLimit) {
 413 |         console.log(
 414 |           `Console errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`
 415 |         );
 416 |         consoleErrors.shift();
 417 |       }
 418 |       break;
 419 |     case "network-request":
 420 |       const logEntry = {
 421 |         url: data.url,
 422 |         method: data.method,
 423 |         status: data.status,
 424 |         timestamp: data.timestamp,
 425 |       };
 426 |       console.log("Adding network request:", logEntry);
 427 | 
 428 |       // Route network requests based on status code
 429 |       if (data.status >= 400) {
 430 |         networkErrors.push(data);
 431 |         if (networkErrors.length > currentSettings.logLimit) {
 432 |           console.log(
 433 |             `Network errors exceeded limit (${currentSettings.logLimit}), removing oldest entry`
 434 |           );
 435 |           networkErrors.shift();
 436 |         }
 437 |       } else {
 438 |         networkSuccess.push(data);
 439 |         if (networkSuccess.length > currentSettings.logLimit) {
 440 |           console.log(
 441 |             `Network success logs exceeded limit (${currentSettings.logLimit}), removing oldest entry`
 442 |           );
 443 |           networkSuccess.shift();
 444 |         }
 445 |       }
 446 |       break;
 447 |     case "selected-element":
 448 |       console.log("Updating selected element:", {
 449 |         tagName: data.element?.tagName,
 450 |         id: data.element?.id,
 451 |         className: data.element?.className,
 452 |       });
 453 |       selectedElement = data.element;
 454 |       break;
 455 |     default:
 456 |       console.log("Unknown log type:", data.type);
 457 |   }
 458 | 
 459 |   console.log("Current log counts:", {
 460 |     consoleLogs: consoleLogs.length,
 461 |     consoleErrors: consoleErrors.length,
 462 |     networkErrors: networkErrors.length,
 463 |     networkSuccess: networkSuccess.length,
 464 |   });
 465 |   console.log("=== End Extension Log ===\n");
 466 | 
 467 |   res.json({ status: "ok" });
 468 | });
 469 | 
 470 | // Update GET endpoints to use the new function
 471 | app.get("/console-logs", (req, res) => {
 472 |   const truncatedLogs = truncateLogsToQueryLimit(consoleLogs);
 473 |   res.json(truncatedLogs);
 474 | });
 475 | 
 476 | app.get("/console-errors", (req, res) => {
 477 |   const truncatedLogs = truncateLogsToQueryLimit(consoleErrors);
 478 |   res.json(truncatedLogs);
 479 | });
 480 | 
 481 | app.get("/network-errors", (req, res) => {
 482 |   const truncatedLogs = truncateLogsToQueryLimit(networkErrors);
 483 |   res.json(truncatedLogs);
 484 | });
 485 | 
 486 | app.get("/network-success", (req, res) => {
 487 |   const truncatedLogs = truncateLogsToQueryLimit(networkSuccess);
 488 |   res.json(truncatedLogs);
 489 | });
 490 | 
 491 | app.get("/all-xhr", (req, res) => {
 492 |   // Merge and sort network success and error logs by timestamp
 493 |   const mergedLogs = [...networkSuccess, ...networkErrors].sort(
 494 |     (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
 495 |   );
 496 |   const truncatedLogs = truncateLogsToQueryLimit(mergedLogs);
 497 |   res.json(truncatedLogs);
 498 | });
 499 | 
 500 | // Add new endpoint for selected element
 501 | app.post("/selected-element", (req, res) => {
 502 |   const { data } = req.body;
 503 |   selectedElement = data;
 504 |   res.json({ status: "ok" });
 505 | });
 506 | 
 507 | app.get("/selected-element", (req, res) => {
 508 |   res.json(selectedElement || { message: "No element selected" });
 509 | });
 510 | 
 511 | app.get("/.port", (req, res) => {
 512 |   res.send(PORT.toString());
 513 | });
 514 | 
 515 | // Add new identity endpoint with a unique signature
 516 | app.get("/.identity", (req, res) => {
 517 |   res.json({
 518 |     port: PORT,
 519 |     name: "browser-tools-server",
 520 |     version: "1.2.0",
 521 |     signature: "mcp-browser-connector-24x7",
 522 |   });
 523 | });
 524 | 
 525 | // Add function to clear all logs
 526 | function clearAllLogs() {
 527 |   console.log("Wiping all logs...");
 528 |   consoleLogs.length = 0;
 529 |   consoleErrors.length = 0;
 530 |   networkErrors.length = 0;
 531 |   networkSuccess.length = 0;
 532 |   allXhr.length = 0;
 533 |   selectedElement = null;
 534 |   console.log("All logs have been wiped");
 535 | }
 536 | 
 537 | // Add endpoint to wipe logs
 538 | app.post("/wipelogs", (req, res) => {
 539 |   clearAllLogs();
 540 |   res.json({ status: "ok", message: "All logs cleared successfully" });
 541 | });
 542 | 
 543 | // Add endpoint for the extension to report the current URL
 544 | app.post("/current-url", (req, res) => {
 545 |   console.log(
 546 |     "Received current URL update request:",
 547 |     JSON.stringify(req.body, null, 2)
 548 |   );
 549 | 
 550 |   if (req.body && req.body.url) {
 551 |     const oldUrl = currentUrl;
 552 |     currentUrl = req.body.url;
 553 | 
 554 |     // Update the current tab ID if provided
 555 |     if (req.body.tabId) {
 556 |       const oldTabId = currentTabId;
 557 |       currentTabId = req.body.tabId;
 558 |       console.log(`Updated current tab ID: ${oldTabId} -> ${currentTabId}`);
 559 |     }
 560 | 
 561 |     // Log the source of the update if provided
 562 |     const source = req.body.source || "unknown";
 563 |     const tabId = req.body.tabId || "unknown";
 564 |     const timestamp = req.body.timestamp
 565 |       ? new Date(req.body.timestamp).toISOString()
 566 |       : "unknown";
 567 | 
 568 |     console.log(
 569 |       `Updated current URL via dedicated endpoint: ${oldUrl} -> ${currentUrl}`
 570 |     );
 571 |     console.log(
 572 |       `URL update details: source=${source}, tabId=${tabId}, timestamp=${timestamp}`
 573 |     );
 574 | 
 575 |     res.json({
 576 |       status: "ok",
 577 |       url: currentUrl,
 578 |       tabId: currentTabId,
 579 |       previousUrl: oldUrl,
 580 |       updated: oldUrl !== currentUrl,
 581 |     });
 582 |   } else {
 583 |     console.log("No URL provided in current-url request");
 584 |     res.status(400).json({ status: "error", message: "No URL provided" });
 585 |   }
 586 | });
 587 | 
 588 | // Add endpoint to get the current URL
 589 | app.get("/current-url", (req, res) => {
 590 |   console.log("Current URL requested, returning:", currentUrl);
 591 |   res.json({ url: currentUrl });
 592 | });
 593 | 
 594 | interface ScreenshotMessage {
 595 |   type: "screenshot-data" | "screenshot-error";
 596 |   data?: string;
 597 |   path?: string;
 598 |   error?: string;
 599 |   autoPaste?: boolean;
 600 | }
 601 | 
 602 | export class BrowserConnector {
 603 |   private wss: WebSocketServer;
 604 |   private activeConnection: WebSocket | null = null;
 605 |   private app: express.Application;
 606 |   private server: any;
 607 |   private urlRequestCallbacks: Map<string, (url: string) => void> = new Map();
 608 | 
 609 |   constructor(app: express.Application, server: any) {
 610 |     this.app = app;
 611 |     this.server = server;
 612 | 
 613 |     // Initialize WebSocket server using the existing HTTP server
 614 |     this.wss = new WebSocketServer({
 615 |       noServer: true,
 616 |       path: "/extension-ws",
 617 |     });
 618 | 
 619 |     // Register the capture-screenshot endpoint
 620 |     this.app.post(
 621 |       "/capture-screenshot",
 622 |       async (req: express.Request, res: express.Response) => {
 623 |         console.log(
 624 |           "Browser Connector: Received request to /capture-screenshot endpoint"
 625 |         );
 626 |         console.log("Browser Connector: Request body:", req.body);
 627 |         console.log(
 628 |           "Browser Connector: Active WebSocket connection:",
 629 |           !!this.activeConnection
 630 |         );
 631 |         await this.captureScreenshot(req, res);
 632 |       }
 633 |     );
 634 | 
 635 |     // Set up accessibility audit endpoint
 636 |     this.setupAccessibilityAudit();
 637 | 
 638 |     // Set up performance audit endpoint
 639 |     this.setupPerformanceAudit();
 640 | 
 641 |     // Set up SEO audit endpoint
 642 |     this.setupSEOAudit();
 643 | 
 644 |     // Set up Best Practices audit endpoint
 645 |     this.setupBestPracticesAudit();
 646 | 
 647 |     // Handle upgrade requests for WebSocket
 648 |     this.server.on(
 649 |       "upgrade",
 650 |       (request: IncomingMessage, socket: Socket, head: Buffer) => {
 651 |         if (request.url === "/extension-ws") {
 652 |           this.wss.handleUpgrade(request, socket, head, (ws: WebSocket) => {
 653 |             this.wss.emit("connection", ws, request);
 654 |           });
 655 |         }
 656 |       }
 657 |     );
 658 | 
 659 |     this.wss.on("connection", (ws: WebSocket) => {
 660 |       console.log("Chrome extension connected via WebSocket");
 661 |       this.activeConnection = ws;
 662 | 
 663 |       ws.on("message", (message: string | Buffer | ArrayBuffer | Buffer[]) => {
 664 |         try {
 665 |           const data = JSON.parse(message.toString());
 666 |           // Log message without the base64 data
 667 |           console.log("Received WebSocket message:", {
 668 |             ...data,
 669 |             data: data.data ? "[base64 data]" : undefined,
 670 |           });
 671 | 
 672 |           // Handle URL response
 673 |           if (data.type === "current-url-response" && data.url) {
 674 |             console.log("Received current URL from browser:", data.url);
 675 |             currentUrl = data.url;
 676 | 
 677 |             // Also update the tab ID if provided
 678 |             if (data.tabId) {
 679 |               console.log(
 680 |                 "Updating tab ID from WebSocket message:",
 681 |                 data.tabId
 682 |               );
 683 |               currentTabId = data.tabId;
 684 |             }
 685 | 
 686 |             // Call the callback if exists
 687 |             if (
 688 |               data.requestId &&
 689 |               this.urlRequestCallbacks.has(data.requestId)
 690 |             ) {
 691 |               const callback = this.urlRequestCallbacks.get(data.requestId);
 692 |               if (callback) callback(data.url);
 693 |               this.urlRequestCallbacks.delete(data.requestId);
 694 |             }
 695 |           }
 696 |           // Handle page navigation event via WebSocket
 697 |           // Note: This is intentionally duplicated from the HTTP handler in /extension-log
 698 |           // as the extension may send navigation events through either channel
 699 |           if (data.type === "page-navigated" && data.url) {
 700 |             console.log("Page navigated to:", data.url);
 701 |             currentUrl = data.url;
 702 | 
 703 |             // Also update the tab ID if provided
 704 |             if (data.tabId) {
 705 |               console.log(
 706 |                 "Updating tab ID from page navigation event:",
 707 |                 data.tabId
 708 |               );
 709 |               currentTabId = data.tabId;
 710 |             }
 711 |           }
 712 |           // Handle screenshot response
 713 |           if (data.type === "screenshot-data" && data.data) {
 714 |             console.log("Received screenshot data");
 715 |             console.log("Screenshot path from extension:", data.path);
 716 |             console.log("Auto-paste setting from extension:", data.autoPaste);
 717 |             // Get the most recent callback since we're not using requestId anymore
 718 |             const callbacks = Array.from(screenshotCallbacks.values());
 719 |             if (callbacks.length > 0) {
 720 |               const callback = callbacks[0];
 721 |               console.log("Found callback, resolving promise");
 722 |               // Pass both the data, path and autoPaste to the resolver
 723 |               callback.resolve({
 724 |                 data: data.data,
 725 |                 path: data.path,
 726 |                 autoPaste: data.autoPaste,
 727 |               });
 728 |               screenshotCallbacks.clear(); // Clear all callbacks
 729 |             } else {
 730 |               console.log("No callbacks found for screenshot");
 731 |             }
 732 |           }
 733 |           // Handle screenshot error
 734 |           else if (data.type === "screenshot-error") {
 735 |             console.log("Received screenshot error:", data.error);
 736 |             const callbacks = Array.from(screenshotCallbacks.values());
 737 |             if (callbacks.length > 0) {
 738 |               const callback = callbacks[0];
 739 |               callback.reject(
 740 |                 new Error(data.error || "Screenshot capture failed")
 741 |               );
 742 |               screenshotCallbacks.clear(); // Clear all callbacks
 743 |             }
 744 |           } else {
 745 |             console.log("Unhandled message type:", data.type);
 746 |           }
 747 |         } catch (error) {
 748 |           console.error("Error processing WebSocket message:", error);
 749 |         }
 750 |       });
 751 | 
 752 |       ws.on("close", () => {
 753 |         console.log("Chrome extension disconnected");
 754 |         if (this.activeConnection === ws) {
 755 |           this.activeConnection = null;
 756 |         }
 757 |       });
 758 |     });
 759 | 
 760 |     // Add screenshot endpoint
 761 |     this.app.post(
 762 |       "/screenshot",
 763 |       (req: express.Request, res: express.Response): void => {
 764 |         console.log(
 765 |           "Browser Connector: Received request to /screenshot endpoint"
 766 |         );
 767 |         console.log("Browser Connector: Request body:", req.body);
 768 |         try {
 769 |           console.log("Received screenshot capture request");
 770 |           const { data, path: outputPath } = req.body;
 771 | 
 772 |           if (!data) {
 773 |             console.log("Screenshot request missing data");
 774 |             res.status(400).json({ error: "Missing screenshot data" });
 775 |             return;
 776 |           }
 777 | 
 778 |           // Use provided path or default to downloads folder
 779 |           const targetPath = outputPath || getDefaultDownloadsFolder();
 780 |           console.log(`Using screenshot path: ${targetPath}`);
 781 | 
 782 |           // Remove the data:image/png;base64, prefix
 783 |           const base64Data = data.replace(/^data:image\/png;base64,/, "");
 784 | 
 785 |           // Create the full directory path if it doesn't exist
 786 |           fs.mkdirSync(targetPath, { recursive: true });
 787 |           console.log(`Created/verified directory: ${targetPath}`);
 788 | 
 789 |           // Generate a unique filename using timestamp
 790 |           const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
 791 |           const filename = `screenshot-${timestamp}.png`;
 792 |           const fullPath = path.join(targetPath, filename);
 793 |           console.log(`Saving screenshot to: ${fullPath}`);
 794 | 
 795 |           // Write the file
 796 |           fs.writeFileSync(fullPath, base64Data, "base64");
 797 |           console.log("Screenshot saved successfully");
 798 | 
 799 |           res.json({
 800 |             path: fullPath,
 801 |             filename: filename,
 802 |           });
 803 |         } catch (error: unknown) {
 804 |           console.error("Error saving screenshot:", error);
 805 |           if (error instanceof Error) {
 806 |             res.status(500).json({ error: error.message });
 807 |           } else {
 808 |             res.status(500).json({ error: "An unknown error occurred" });
 809 |           }
 810 |         }
 811 |       }
 812 |     );
 813 |   }
 814 | 
 815 |   private async handleScreenshot(req: express.Request, res: express.Response) {
 816 |     if (!this.activeConnection) {
 817 |       return res.status(503).json({ error: "Chrome extension not connected" });
 818 |     }
 819 | 
 820 |     try {
 821 |       const result = await new Promise((resolve, reject) => {
 822 |         // Set up one-time message handler for this screenshot request
 823 |         const messageHandler = (
 824 |           message: string | Buffer | ArrayBuffer | Buffer[]
 825 |         ) => {
 826 |           try {
 827 |             const response: ScreenshotMessage = JSON.parse(message.toString());
 828 | 
 829 |             if (response.type === "screenshot-error") {
 830 |               reject(new Error(response.error));
 831 |               return;
 832 |             }
 833 | 
 834 |             if (
 835 |               response.type === "screenshot-data" &&
 836 |               response.data &&
 837 |               response.path
 838 |             ) {
 839 |               // Remove the data:image/png;base64, prefix
 840 |               const base64Data = response.data.replace(
 841 |                 /^data:image\/png;base64,/,
 842 |                 ""
 843 |               );
 844 | 
 845 |               // Ensure the directory exists
 846 |               const dir = path.dirname(response.path);
 847 |               fs.mkdirSync(dir, { recursive: true });
 848 | 
 849 |               // Generate a unique filename using timestamp
 850 |               const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
 851 |               const filename = `screenshot-${timestamp}.png`;
 852 |               const fullPath = path.join(response.path, filename);
 853 | 
 854 |               // Write the file
 855 |               fs.writeFileSync(fullPath, base64Data, "base64");
 856 |               resolve({
 857 |                 path: fullPath,
 858 |                 filename: filename,
 859 |               });
 860 |             }
 861 |           } catch (error) {
 862 |             reject(error);
 863 |           } finally {
 864 |             this.activeConnection?.removeListener("message", messageHandler);
 865 |           }
 866 |         };
 867 | 
 868 |         // Add temporary message handler
 869 |         this.activeConnection?.on("message", messageHandler);
 870 | 
 871 |         // Request screenshot
 872 |         this.activeConnection?.send(
 873 |           JSON.stringify({ type: "take-screenshot" })
 874 |         );
 875 | 
 876 |         // Set timeout
 877 |         setTimeout(() => {
 878 |           this.activeConnection?.removeListener("message", messageHandler);
 879 |           reject(new Error("Screenshot timeout"));
 880 |         }, 30000); // 30 second timeout
 881 |       });
 882 | 
 883 |       res.json(result);
 884 |     } catch (error: unknown) {
 885 |       if (error instanceof Error) {
 886 |         res.status(500).json({ error: error.message });
 887 |       } else {
 888 |         res.status(500).json({ error: "An unknown error occurred" });
 889 |       }
 890 |     }
 891 |   }
 892 | 
 893 |   // Updated method to get URL for audits with improved connection tracking and waiting
 894 |   private async getUrlForAudit(): Promise<string | null> {
 895 |     try {
 896 |       console.log("getUrlForAudit called");
 897 | 
 898 |       // Use the stored URL if available immediately
 899 |       if (currentUrl && currentUrl !== "" && currentUrl !== "about:blank") {
 900 |         console.log(`Using existing URL immediately: ${currentUrl}`);
 901 |         return currentUrl;
 902 |       }
 903 | 
 904 |       // Wait for a URL to become available (retry loop)
 905 |       console.log("No valid URL available yet, waiting for navigation...");
 906 | 
 907 |       // Wait up to 10 seconds for a URL to be set (20 attempts x 500ms)
 908 |       const maxAttempts = 50;
 909 |       const waitTime = 500; // ms
 910 | 
 911 |       for (let attempt = 0; attempt < maxAttempts; attempt++) {
 912 |         // Check if URL is available now
 913 |         if (currentUrl && currentUrl !== "" && currentUrl !== "about:blank") {
 914 |           console.log(`URL became available after waiting: ${currentUrl}`);
 915 |           return currentUrl;
 916 |         }
 917 | 
 918 |         // Wait before checking again
 919 |         console.log(
 920 |           `Waiting for URL (attempt ${attempt + 1}/${maxAttempts})...`
 921 |         );
 922 |         await new Promise((resolve) => setTimeout(resolve, waitTime));
 923 |       }
 924 | 
 925 |       // If we reach here, no URL became available after waiting
 926 |       console.log("Timed out waiting for URL, returning null");
 927 |       return null;
 928 |     } catch (error) {
 929 |       console.error("Error in getUrlForAudit:", error);
 930 |       return null; // Return null to trigger an error
 931 |     }
 932 |   }
 933 | 
 934 |   // Public method to check if there's an active connection
 935 |   public hasActiveConnection(): boolean {
 936 |     return this.activeConnection !== null;
 937 |   }
 938 | 
 939 |   // Add new endpoint for programmatic screenshot capture
 940 |   async captureScreenshot(req: express.Request, res: express.Response) {
 941 |     console.log("Browser Connector: Starting captureScreenshot method");
 942 |     console.log("Browser Connector: Request headers:", req.headers);
 943 |     console.log("Browser Connector: Request method:", req.method);
 944 | 
 945 |     if (!this.activeConnection) {
 946 |       console.log(
 947 |         "Browser Connector: No active WebSocket connection to Chrome extension"
 948 |       );
 949 |       return res.status(503).json({ error: "Chrome extension not connected" });
 950 |     }
 951 | 
 952 |     try {
 953 |       console.log("Browser Connector: Starting screenshot capture...");
 954 |       const requestId = Date.now().toString();
 955 |       console.log("Browser Connector: Generated requestId:", requestId);
 956 | 
 957 |       // Create promise that will resolve when we get the screenshot data
 958 |       const screenshotPromise = new Promise<{
 959 |         data: string;
 960 |         path?: string;
 961 |         autoPaste?: boolean;
 962 |       }>((resolve, reject) => {
 963 |         console.log(
 964 |           `Browser Connector: Setting up screenshot callback for requestId: ${requestId}`
 965 |         );
 966 |         // Store callback in map
 967 |         screenshotCallbacks.set(requestId, { resolve, reject });
 968 |         console.log(
 969 |           "Browser Connector: Current callbacks:",
 970 |           Array.from(screenshotCallbacks.keys())
 971 |         );
 972 | 
 973 |         // Set timeout to clean up if we don't get a response
 974 |         setTimeout(() => {
 975 |           if (screenshotCallbacks.has(requestId)) {
 976 |             console.log(
 977 |               `Browser Connector: Screenshot capture timed out for requestId: ${requestId}`
 978 |             );
 979 |             screenshotCallbacks.delete(requestId);
 980 |             reject(
 981 |               new Error(
 982 |                 "Screenshot capture timed out - no response from Chrome extension"
 983 |               )
 984 |             );
 985 |           }
 986 |         }, 10000);
 987 |       });
 988 | 
 989 |       // Send screenshot request to extension
 990 |       const message = JSON.stringify({
 991 |         type: "take-screenshot",
 992 |         requestId: requestId,
 993 |       });
 994 |       console.log(
 995 |         `Browser Connector: Sending WebSocket message to extension:`,
 996 |         message
 997 |       );
 998 |       this.activeConnection.send(message);
 999 | 
1000 |       // Wait for screenshot data
1001 |       console.log("Browser Connector: Waiting for screenshot data...");
1002 |       const {
1003 |         data: base64Data,
1004 |         path: customPath,
1005 |         autoPaste,
1006 |       } = await screenshotPromise;
1007 |       console.log("Browser Connector: Received screenshot data, saving...");
1008 |       console.log("Browser Connector: Custom path from extension:", customPath);
1009 |       console.log("Browser Connector: Auto-paste setting:", autoPaste);
1010 | 
1011 |       // Always prioritize the path from the Chrome extension
1012 |       let targetPath = customPath;
1013 | 
1014 |       // If no path provided by extension, fall back to defaults
1015 |       if (!targetPath) {
1016 |         targetPath =
1017 |           currentSettings.screenshotPath || getDefaultDownloadsFolder();
1018 |       }
1019 | 
1020 |       // Convert the path for the current platform
1021 |       targetPath = convertPathForCurrentPlatform(targetPath);
1022 | 
1023 |       console.log(`Browser Connector: Using path: ${targetPath}`);
1024 | 
1025 |       if (!base64Data) {
1026 |         throw new Error("No screenshot data received from Chrome extension");
1027 |       }
1028 | 
1029 |       try {
1030 |         fs.mkdirSync(targetPath, { recursive: true });
1031 |         console.log(`Browser Connector: Created directory: ${targetPath}`);
1032 |       } catch (err) {
1033 |         console.error(
1034 |           `Browser Connector: Error creating directory: ${targetPath}`,
1035 |           err
1036 |         );
1037 |         throw new Error(
1038 |           `Failed to create screenshot directory: ${
1039 |             err instanceof Error ? err.message : String(err)
1040 |           }`
1041 |         );
1042 |       }
1043 | 
1044 |       const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
1045 |       const filename = `screenshot-${timestamp}.png`;
1046 |       const fullPath = path.join(targetPath, filename);
1047 |       console.log(`Browser Connector: Full screenshot path: ${fullPath}`);
1048 | 
1049 |       // Remove the data:image/png;base64, prefix if present
1050 |       const cleanBase64 = base64Data.replace(/^data:image\/png;base64,/, "");
1051 | 
1052 |       // Save the file
1053 |       try {
1054 |         fs.writeFileSync(fullPath, cleanBase64, "base64");
1055 |         console.log(`Browser Connector: Screenshot saved to: ${fullPath}`);
1056 |       } catch (err) {
1057 |         console.error(
1058 |           `Browser Connector: Error saving screenshot to: ${fullPath}`,
1059 |           err
1060 |         );
1061 |         throw new Error(
1062 |           `Failed to save screenshot: ${
1063 |             err instanceof Error ? err.message : String(err)
1064 |           }`
1065 |         );
1066 |       }
1067 | 
1068 |       // Check if running on macOS before executing AppleScript
1069 |       if (os.platform() === "darwin" && autoPaste === true) {
1070 |         console.log(
1071 |           "Browser Connector: Running on macOS with auto-paste enabled, executing AppleScript to paste into Cursor"
1072 |         );
1073 | 
1074 |         // Create the AppleScript to copy the image to clipboard and paste into Cursor
1075 |         // This version is more robust and includes debugging
1076 |         const appleScript = `
1077 |           -- Set path to the screenshot
1078 |           set imagePath to "${fullPath}"
1079 |           
1080 |           -- Copy the image to clipboard
1081 |           try
1082 |             set the clipboard to (read (POSIX file imagePath) as «class PNGf»)
1083 |           on error errMsg
1084 |             log "Error copying image to clipboard: " & errMsg
1085 |             return "Failed to copy image to clipboard: " & errMsg
1086 |           end try
1087 |           
1088 |           -- Activate Cursor application
1089 |           try
1090 |             tell application "Cursor"
1091 |               activate
1092 |             end tell
1093 |           on error errMsg
1094 |             log "Error activating Cursor: " & errMsg
1095 |             return "Failed to activate Cursor: " & errMsg
1096 |           end try
1097 |           
1098 |           -- Wait for the application to fully activate
1099 |           delay 3
1100 |           
1101 |           -- Try to interact with Cursor
1102 |           try
1103 |             tell application "System Events"
1104 |               tell process "Cursor"
1105 |                 -- Get the frontmost window
1106 |                 if (count of windows) is 0 then
1107 |                   return "No windows found in Cursor"
1108 |                 end if
1109 |                 
1110 |                 set cursorWindow to window 1
1111 |                 
1112 |                 -- Try Method 1: Look for elements of class "Text Area"
1113 |                 set foundElements to {}
1114 |                 
1115 |                 -- Try different selectors to find the text input area
1116 |                 try
1117 |                   -- Try with class
1118 |                   set textAreas to UI elements of cursorWindow whose class is "Text Area"
1119 |                   if (count of textAreas) > 0 then
1120 |                     set foundElements to textAreas
1121 |                   end if
1122 |                 end try
1123 |                 
1124 |                 if (count of foundElements) is 0 then
1125 |                   try
1126 |                     -- Try with AXTextField role
1127 |                     set textFields to UI elements of cursorWindow whose role is "AXTextField"
1128 |                     if (count of textFields) > 0 then
1129 |                       set foundElements to textFields
1130 |                     end if
1131 |                   end try
1132 |                 end if
1133 |                 
1134 |                 if (count of foundElements) is 0 then
1135 |                   try
1136 |                     -- Try with AXTextArea role in nested elements
1137 |                     set allElements to UI elements of cursorWindow
1138 |                     repeat with anElement in allElements
1139 |                       try
1140 |                         set childElements to UI elements of anElement
1141 |                         repeat with aChild in childElements
1142 |                           try
1143 |                             if role of aChild is "AXTextArea" or role of aChild is "AXTextField" then
1144 |                               set end of foundElements to aChild
1145 |                             end if
1146 |                           end try
1147 |                         end repeat
1148 |                       end try
1149 |                     end repeat
1150 |                   end try
1151 |                 end if
1152 |                 
1153 |                 -- If no elements found with specific attributes, try a broader approach
1154 |                 if (count of foundElements) is 0 then
1155 |                   -- Just try to use the Command+V shortcut on the active window
1156 |                    -- This assumes Cursor already has focus on the right element
1157 |                     keystroke "v" using command down
1158 |                     delay 1
1159 |                     keystroke "here is the screenshot"
1160 |                     delay 1
1161 |                    -- Try multiple methods to press Enter
1162 |                    key code 36 -- Use key code for Return key
1163 |                    delay 0.5
1164 |                    keystroke return -- Use keystroke return as alternative
1165 |                    return "Used fallback method: Command+V on active window"
1166 |                 else
1167 |                   -- We found a potential text input element
1168 |                   set inputElement to item 1 of foundElements
1169 |                   
1170 |                   -- Try to focus and paste
1171 |                   try
1172 |                     set focused of inputElement to true
1173 |                     delay 0.5
1174 |                     
1175 |                     -- Paste the image
1176 |                     keystroke "v" using command down
1177 |                     delay 1
1178 |                     
1179 |                     -- Type the text
1180 |                     keystroke "here is the screenshot"
1181 |                     delay 1
1182 |                     -- Try multiple methods to press Enter
1183 |                     key code 36 -- Use key code for Return key
1184 |                     delay 0.5
1185 |                     keystroke return -- Use keystroke return as alternative
1186 |                     return "Successfully pasted screenshot into Cursor text element"
1187 |                   on error errMsg
1188 |                     log "Error interacting with found element: " & errMsg
1189 |                     -- Fallback to just sending the key commands
1190 |                     keystroke "v" using command down
1191 |                     delay 1
1192 |                     keystroke "here is the screenshot"
1193 |                     delay 1
1194 |                     -- Try multiple methods to press Enter
1195 |                     key code 36 -- Use key code for Return key
1196 |                     delay 0.5
1197 |                     keystroke return -- Use keystroke return as alternative
1198 |                     return "Used fallback after element focus error: " & errMsg
1199 |                   end try
1200 |                 end if
1201 |               end tell
1202 |             end tell
1203 |           on error errMsg
1204 |             log "Error in System Events block: " & errMsg
1205 |             return "Failed in System Events: " & errMsg
1206 |           end try
1207 |         `;
1208 | 
1209 |         // Execute the AppleScript
1210 |         exec(`osascript -e '${appleScript}'`, (error, stdout, stderr) => {
1211 |           if (error) {
1212 |             console.error(
1213 |               `Browser Connector: Error executing AppleScript: ${error.message}`
1214 |             );
1215 |             console.error(`Browser Connector: stderr: ${stderr}`);
1216 |             // Don't fail the response; log the error and proceed
1217 |           } else {
1218 |             console.log(`Browser Connector: AppleScript executed successfully`);
1219 |             console.log(`Browser Connector: stdout: ${stdout}`);
1220 |           }
1221 |         });
1222 |       } else {
1223 |         if (os.platform() === "darwin" && !autoPaste) {
1224 |           console.log(
1225 |             `Browser Connector: Running on macOS but auto-paste is disabled, skipping AppleScript execution`
1226 |           );
1227 |         } else {
1228 |           console.log(
1229 |             `Browser Connector: Not running on macOS, skipping AppleScript execution`
1230 |           );
1231 |         }
1232 |       }
1233 | 
1234 |       res.json({
1235 |         path: fullPath,
1236 |         filename: filename,
1237 |       });
1238 |     } catch (error) {
1239 |       const errorMessage =
1240 |         error instanceof Error ? error.message : String(error);
1241 |       console.error(
1242 |         "Browser Connector: Error capturing screenshot:",
1243 |         errorMessage
1244 |       );
1245 |       res.status(500).json({
1246 |         error: errorMessage,
1247 |       });
1248 |     }
1249 |   }
1250 | 
1251 |   // Add shutdown method
1252 |   public shutdown() {
1253 |     return new Promise<void>((resolve) => {
1254 |       console.log("Shutting down WebSocket server...");
1255 | 
1256 |       // Send close message to client if connection is active
1257 |       if (
1258 |         this.activeConnection &&
1259 |         this.activeConnection.readyState === WebSocket.OPEN
1260 |       ) {
1261 |         console.log("Notifying client to close connection...");
1262 |         try {
1263 |           this.activeConnection.send(
1264 |             JSON.stringify({ type: "server-shutdown" })
1265 |           );
1266 |         } catch (err) {
1267 |           console.error("Error sending shutdown message to client:", err);
1268 |         }
1269 |       }
1270 | 
1271 |       // Set a timeout to force close after 2 seconds
1272 |       const forceCloseTimeout = setTimeout(() => {
1273 |         console.log("Force closing connections after timeout...");
1274 |         if (this.activeConnection) {
1275 |           this.activeConnection.terminate(); // Force close the connection
1276 |           this.activeConnection = null;
1277 |         }
1278 |         this.wss.close();
1279 |         resolve();
1280 |       }, 2000);
1281 | 
1282 |       // Close active WebSocket connection if exists
1283 |       if (this.activeConnection) {
1284 |         this.activeConnection.close(1000, "Server shutting down");
1285 |         this.activeConnection = null;
1286 |       }
1287 | 
1288 |       // Close WebSocket server
1289 |       this.wss.close(() => {
1290 |         clearTimeout(forceCloseTimeout);
1291 |         console.log("WebSocket server closed gracefully");
1292 |         resolve();
1293 |       });
1294 |     });
1295 |   }
1296 | 
1297 |   // Sets up the accessibility audit endpoint
1298 |   private setupAccessibilityAudit() {
1299 |     this.setupAuditEndpoint(
1300 |       AuditCategory.ACCESSIBILITY,
1301 |       "/accessibility-audit",
1302 |       runAccessibilityAudit
1303 |     );
1304 |   }
1305 | 
1306 |   // Sets up the performance audit endpoint
1307 |   private setupPerformanceAudit() {
1308 |     this.setupAuditEndpoint(
1309 |       AuditCategory.PERFORMANCE,
1310 |       "/performance-audit",
1311 |       runPerformanceAudit
1312 |     );
1313 |   }
1314 | 
1315 |   // Set up SEO audit endpoint
1316 |   private setupSEOAudit() {
1317 |     this.setupAuditEndpoint(AuditCategory.SEO, "/seo-audit", runSEOAudit);
1318 |   }
1319 | 
1320 |   // Add a setup method for Best Practices audit
1321 |   private setupBestPracticesAudit() {
1322 |     this.setupAuditEndpoint(
1323 |       AuditCategory.BEST_PRACTICES,
1324 |       "/best-practices-audit",
1325 |       runBestPracticesAudit
1326 |     );
1327 |   }
1328 | 
1329 |   /**
1330 |    * Generic method to set up an audit endpoint
1331 |    * @param auditType The type of audit (accessibility, performance, SEO)
1332 |    * @param endpoint The endpoint path
1333 |    * @param auditFunction The audit function to call
1334 |    */
1335 |   private setupAuditEndpoint(
1336 |     auditType: string,
1337 |     endpoint: string,
1338 |     auditFunction: (url: string) => Promise<LighthouseReport>
1339 |   ) {
1340 |     // Add server identity validation endpoint
1341 |     this.app.get("/.identity", (req, res) => {
1342 |       res.json({
1343 |         signature: "mcp-browser-connector-24x7",
1344 |         version: "1.2.0",
1345 |       });
1346 |     });
1347 | 
1348 |     this.app.post(endpoint, async (req: any, res: any) => {
1349 |       try {
1350 |         console.log(`${auditType} audit request received`);
1351 | 
1352 |         // Get URL using our helper method
1353 |         const url = await this.getUrlForAudit();
1354 | 
1355 |         if (!url) {
1356 |           console.log(`No URL available for ${auditType} audit`);
1357 |           return res.status(400).json({
1358 |             error: `URL is required for ${auditType} audit. Make sure you navigate to a page in the browser first, and the browser-tool extension tab is open.`,
1359 |           });
1360 |         }
1361 | 
1362 |         // If we're using the stored URL (not from request body), log it now
1363 |         if (!req.body?.url && url === currentUrl) {
1364 |           console.log(`Using stored URL for ${auditType} audit:`, url);
1365 |         }
1366 | 
1367 |         // Check if we're using the default URL
1368 |         if (url === "about:blank") {
1369 |           console.log(`Cannot run ${auditType} audit on about:blank`);
1370 |           return res.status(400).json({
1371 |             error: `Cannot run ${auditType} audit on about:blank`,
1372 |           });
1373 |         }
1374 | 
1375 |         console.log(`Preparing to run ${auditType} audit for: ${url}`);
1376 | 
1377 |         // Run the audit using the provided function
1378 |         try {
1379 |           const result = await auditFunction(url);
1380 | 
1381 |           console.log(`${auditType} audit completed successfully`);
1382 |           // Return the results
1383 |           res.json(result);
1384 |         } catch (auditError) {
1385 |           console.error(`${auditType} audit failed:`, auditError);
1386 |           const errorMessage =
1387 |             auditError instanceof Error
1388 |               ? auditError.message
1389 |               : String(auditError);
1390 |           res.status(500).json({
1391 |             error: `Failed to run ${auditType} audit: ${errorMessage}`,
1392 |           });
1393 |         }
1394 |       } catch (error) {
1395 |         console.error(`Error in ${auditType} audit endpoint:`, error);
1396 |         const errorMessage =
1397 |           error instanceof Error ? error.message : String(error);
1398 |         res.status(500).json({
1399 |           error: `Error in ${auditType} audit endpoint: ${errorMessage}`,
1400 |         });
1401 |       }
1402 |     });
1403 |   }
1404 | }
1405 | 
1406 | // Use an async IIFE to allow for async/await in the initial setup
1407 | (async () => {
1408 |   try {
1409 |     console.log(`Starting Browser Tools Server...`);
1410 |     console.log(`Requested port: ${REQUESTED_PORT}`);
1411 | 
1412 |     // Find an available port
1413 |     try {
1414 |       PORT = await getAvailablePort(REQUESTED_PORT);
1415 | 
1416 |       if (PORT !== REQUESTED_PORT) {
1417 |         console.log(`\n====================================`);
1418 |         console.log(`NOTICE: Requested port ${REQUESTED_PORT} was in use.`);
1419 |         console.log(`Using port ${PORT} instead.`);
1420 |         console.log(`====================================\n`);
1421 |       }
1422 |     } catch (portError) {
1423 |       console.error(`Failed to find an available port:`, portError);
1424 |       process.exit(1);
1425 |     }
1426 | 
1427 |     // Create the server with the available port
1428 |     const server = app.listen(PORT, currentSettings.serverHost, () => {
1429 |       console.log(`\n=== Browser Tools Server Started ===`);
1430 |       console.log(
1431 |         `Aggregator listening on http://${currentSettings.serverHost}:${PORT}`
1432 |       );
1433 | 
1434 |       if (PORT !== REQUESTED_PORT) {
1435 |         console.log(
1436 |           `NOTE: Using fallback port ${PORT} instead of requested port ${REQUESTED_PORT}`
1437 |         );
1438 |       }
1439 | 
1440 |       // Log all available network interfaces for easier discovery
1441 |       const networkInterfaces = os.networkInterfaces();
1442 |       console.log("\nAvailable on the following network addresses:");
1443 | 
1444 |       Object.keys(networkInterfaces).forEach((interfaceName) => {
1445 |         const interfaces = networkInterfaces[interfaceName];
1446 |         if (interfaces) {
1447 |           interfaces.forEach((iface) => {
1448 |             if (!iface.internal && iface.family === "IPv4") {
1449 |               console.log(`  - http://${iface.address}:${PORT}`);
1450 |             }
1451 |           });
1452 |         }
1453 |       });
1454 | 
1455 |       console.log(`\nFor local access use: http://localhost:${PORT}`);
1456 |     });
1457 | 
1458 |     // Handle server startup errors
1459 |     server.on("error", (err: any) => {
1460 |       if (err.code === "EADDRINUSE") {
1461 |         console.error(
1462 |           `ERROR: Port ${PORT} is still in use, despite our checks!`
1463 |         );
1464 |         console.error(
1465 |           `This might indicate another process started using this port after our check.`
1466 |         );
1467 |       } else {
1468 |         console.error(`Server error:`, err);
1469 |       }
1470 |       process.exit(1);
1471 |     });
1472 | 
1473 |     // Initialize the browser connector with the existing app AND server
1474 |     const browserConnector = new BrowserConnector(app, server);
1475 | 
1476 |     // Handle shutdown gracefully with improved error handling
1477 |     process.on("SIGINT", async () => {
1478 |       console.log("\nReceived SIGINT signal. Starting graceful shutdown...");
1479 | 
1480 |       try {
1481 |         // First shutdown WebSocket connections
1482 |         await browserConnector.shutdown();
1483 | 
1484 |         // Then close the HTTP server
1485 |         await new Promise<void>((resolve, reject) => {
1486 |           server.close((err) => {
1487 |             if (err) {
1488 |               console.error("Error closing HTTP server:", err);
1489 |               reject(err);
1490 |             } else {
1491 |               console.log("HTTP server closed successfully");
1492 |               resolve();
1493 |             }
1494 |           });
1495 |         });
1496 | 
1497 |         // Clear all logs
1498 |         clearAllLogs();
1499 | 
1500 |         console.log("Shutdown completed successfully");
1501 |         process.exit(0);
1502 |       } catch (error) {
1503 |         console.error("Error during shutdown:", error);
1504 |         // Force exit in case of error
1505 |         process.exit(1);
1506 |       }
1507 |     });
1508 | 
1509 |     // Also handle SIGTERM
1510 |     process.on("SIGTERM", () => {
1511 |       console.log("\nReceived SIGTERM signal");
1512 |       process.emit("SIGINT");
1513 |     });
1514 |   } catch (error) {
1515 |     console.error("Failed to start server:", error);
1516 |     process.exit(1);
1517 |   }
1518 | })().catch((err) => {
1519 |   console.error("Unhandled error during server startup:", err);
1520 |   process.exit(1);
1521 | });
1522 | 
```
Page 3/3FirstPrevNextLast