#
tokens: 43697/50000 4/25 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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/puppeteer-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import fs from "fs";
  2 | import puppeteer from "puppeteer-core";
  3 | import path from "path";
  4 | import os from "os";
  5 | import { execSync } from "child_process";
  6 | import * as ChromeLauncher from "chrome-launcher";
  7 | // ===== Configuration Types and Defaults =====
  8 | 
  9 | /**
 10 |  * Configuration interface for the Puppeteer service
 11 |  */
 12 | export interface PuppeteerServiceConfig {
 13 |   // Browser preferences
 14 |   preferredBrowsers?: string[]; // Order of browser preference ("chrome", "edge", "brave", "firefox")
 15 |   customBrowserPaths?: { [key: string]: string }; // Custom browser executable paths
 16 | 
 17 |   // Connection settings
 18 |   debugPorts?: number[]; // Ports to try when connecting to existing browsers
 19 |   connectionTimeout?: number; // Timeout for connection attempts in ms
 20 |   maxRetries?: number; // Maximum number of retries for connections
 21 | 
 22 |   // Browser cleanup settings
 23 |   browserCleanupTimeout?: number; // Timeout before closing inactive browsers (ms)
 24 | 
 25 |   // Performance settings
 26 |   blockResourceTypes?: string[]; // Resource types to block for performance
 27 | }
 28 | 
 29 | // Default configuration values
 30 | const DEFAULT_CONFIG: PuppeteerServiceConfig = {
 31 |   preferredBrowsers: ["chrome", "edge", "brave", "firefox"],
 32 |   debugPorts: [9222, 9223, 9224, 9225],
 33 |   connectionTimeout: 10000,
 34 |   maxRetries: 3,
 35 |   browserCleanupTimeout: 60000,
 36 |   blockResourceTypes: ["image", "font", "media"],
 37 | };
 38 | 
 39 | // Browser support notes:
 40 | // - Chrome/Chromium: Fully supported (primary target)
 41 | // - Edge: Fully supported (Chromium-based)
 42 | // - Brave: Fully supported (Chromium-based)
 43 | // - Firefox: Partially supported (some features may not work)
 44 | // - Safari: Not supported by Puppeteer
 45 | 
 46 | // ===== Global State =====
 47 | 
 48 | // Current active configuration
 49 | let currentConfig: PuppeteerServiceConfig = { ...DEFAULT_CONFIG };
 50 | 
 51 | // Browser instance management
 52 | let headlessBrowserInstance: puppeteer.Browser | null = null;
 53 | let launchedBrowserWSEndpoint: string | null = null;
 54 | 
 55 | // Cleanup management
 56 | let browserCleanupTimeout: NodeJS.Timeout | null = null;
 57 | let BROWSER_CLEANUP_TIMEOUT = 60000; // 60 seconds default
 58 | 
 59 | // Cache for browser executable paths
 60 | let detectedBrowserPath: string | null = null;
 61 | 
 62 | // ===== Configuration Functions =====
 63 | 
 64 | /**
 65 |  * Configure the Puppeteer service with custom settings
 66 |  * @param config Partial configuration to override defaults
 67 |  */
 68 | export function configurePuppeteerService(
 69 |   config: Partial<PuppeteerServiceConfig>
 70 | ): void {
 71 |   currentConfig = { ...DEFAULT_CONFIG, ...config };
 72 | 
 73 |   // Update the timeout if it was changed
 74 |   if (
 75 |     config.browserCleanupTimeout &&
 76 |     config.browserCleanupTimeout !== BROWSER_CLEANUP_TIMEOUT
 77 |   ) {
 78 |     BROWSER_CLEANUP_TIMEOUT = config.browserCleanupTimeout;
 79 |   }
 80 | 
 81 |   console.log("Puppeteer service configured:", currentConfig);
 82 | }
 83 | 
 84 | // ===== Browser Management =====
 85 | 
 86 | /**
 87 |  * Get or create a headless browser instance
 88 |  * @returns Promise resolving to a browser instance
 89 |  */
 90 | async function getHeadlessBrowserInstance(): Promise<puppeteer.Browser> {
 91 |   console.log("Browser instance request started");
 92 | 
 93 |   // Cancel any scheduled cleanup
 94 |   cancelScheduledCleanup();
 95 | 
 96 |   // Try to reuse existing browser
 97 |   if (headlessBrowserInstance) {
 98 |     try {
 99 |       const pages = await headlessBrowserInstance.pages();
100 |       console.log(
101 |         `Reusing existing headless browser with ${pages.length} pages`
102 |       );
103 |       return headlessBrowserInstance;
104 |     } catch (error) {
105 |       console.log(
106 |         "Existing browser instance is no longer valid, creating a new one"
107 |       );
108 |       headlessBrowserInstance = null;
109 |       launchedBrowserWSEndpoint = null;
110 |     }
111 |   }
112 | 
113 |   // Create a new browser instance
114 |   return launchNewBrowser();
115 | }
116 | 
117 | /**
118 |  * Launches a new browser instance
119 |  * @returns Promise resolving to a browser instance
120 |  */
121 | async function launchNewBrowser(): Promise<puppeteer.Browser> {
122 |   console.log("Creating new headless browser instance");
123 | 
124 |   // Setup temporary user data directory
125 |   const userDataDir = createTempUserDataDir();
126 |   let browser: puppeteer.Browser | null = null;
127 | 
128 |   try {
129 |     // Configure launch options
130 |     const launchOptions = configureLaunchOptions(userDataDir);
131 | 
132 |     // Set custom browser executable
133 |     await setCustomBrowserExecutable(launchOptions);
134 | 
135 |     // Launch the browser
136 |     console.log(
137 |       "Launching browser with options:",
138 |       JSON.stringify({
139 |         headless: launchOptions.headless,
140 |         executablePath: launchOptions.executablePath,
141 |       })
142 |     );
143 | 
144 |     browser = await puppeteer.launch(launchOptions);
145 | 
146 |     // Store references to the browser instance
147 |     launchedBrowserWSEndpoint = browser.wsEndpoint();
148 |     headlessBrowserInstance = browser;
149 | 
150 |     // Setup cleanup handlers
151 |     setupBrowserCleanupHandlers(browser, userDataDir);
152 | 
153 |     console.log("Browser ready");
154 |     return browser;
155 |   } catch (error) {
156 |     console.error("Failed to launch browser:", error);
157 | 
158 |     // Clean up resources
159 |     if (browser) {
160 |       try {
161 |         await browser.close();
162 |       } catch (closeError) {
163 |         console.error("Error closing browser:", closeError);
164 |       }
165 |       headlessBrowserInstance = null;
166 |       launchedBrowserWSEndpoint = null;
167 |     }
168 | 
169 |     // Clean up the temporary directory
170 |     try {
171 |       fs.rmSync(userDataDir, { recursive: true, force: true });
172 |     } catch (fsError) {
173 |       console.error("Error removing temporary directory:", fsError);
174 |     }
175 | 
176 |     throw error;
177 |   }
178 | }
179 | 
180 | /**
181 |  * Creates a temporary user data directory for the browser
182 |  * @returns Path to the created directory
183 |  */
184 | function createTempUserDataDir(): string {
185 |   const tempDir = os.tmpdir();
186 |   const uniqueId = `${Date.now().toString()}-${Math.random()
187 |     .toString(36)
188 |     .substring(2)}`;
189 |   const userDataDir = path.join(tempDir, `browser-debug-profile-${uniqueId}`);
190 |   fs.mkdirSync(userDataDir, { recursive: true });
191 |   console.log(`Using temporary user data directory: ${userDataDir}`);
192 |   return userDataDir;
193 | }
194 | 
195 | /**
196 |  * Configures browser launch options
197 |  * @param userDataDir Path to the user data directory
198 |  * @returns Launch options object
199 |  */
200 | function configureLaunchOptions(userDataDir: string): any {
201 |   const launchOptions: any = {
202 |     args: [
203 |       "--remote-debugging-port=0", // Use dynamic port
204 |       `--user-data-dir=${userDataDir}`,
205 |       "--no-first-run",
206 |       "--no-default-browser-check",
207 |       "--disable-dev-shm-usage",
208 |       "--disable-extensions",
209 |       "--disable-component-extensions-with-background-pages",
210 |       "--disable-background-networking",
211 |       "--disable-backgrounding-occluded-windows",
212 |       "--disable-default-apps",
213 |       "--disable-sync",
214 |       "--disable-translate",
215 |       "--metrics-recording-only",
216 |       "--no-pings",
217 |       "--safebrowsing-disable-auto-update",
218 |     ],
219 |   };
220 | 
221 |   // Add headless mode (using any to bypass type checking issues)
222 |   launchOptions.headless = "new";
223 | 
224 |   return launchOptions;
225 | }
226 | 
227 | /**
228 |  * Sets a custom browser executable path if configured
229 |  * @param launchOptions Launch options object to modify
230 |  */
231 | async function setCustomBrowserExecutable(launchOptions: any): Promise<void> {
232 |   // First, try to use a custom browser path from configuration
233 |   if (
234 |     currentConfig.customBrowserPaths &&
235 |     Object.keys(currentConfig.customBrowserPaths).length > 0
236 |   ) {
237 |     const preferredBrowsers = currentConfig.preferredBrowsers || [
238 |       "chrome",
239 |       "edge",
240 |       "brave",
241 |       "firefox",
242 |     ];
243 | 
244 |     for (const browser of preferredBrowsers) {
245 |       if (
246 |         currentConfig.customBrowserPaths[browser] &&
247 |         fs.existsSync(currentConfig.customBrowserPaths[browser])
248 |       ) {
249 |         launchOptions.executablePath =
250 |           currentConfig.customBrowserPaths[browser];
251 | 
252 |         // Set product to firefox if using Firefox browser
253 |         if (browser === "firefox") {
254 |           launchOptions.product = "firefox";
255 |         }
256 | 
257 |         console.log(
258 |           `Using custom ${browser} path: ${launchOptions.executablePath}`
259 |         );
260 |         return;
261 |       }
262 |     }
263 |   }
264 | 
265 |   // If no custom path is found, use cached path or detect a new one
266 |   try {
267 |     if (detectedBrowserPath && fs.existsSync(detectedBrowserPath)) {
268 |       console.log(`Using cached browser path: ${detectedBrowserPath}`);
269 |       launchOptions.executablePath = detectedBrowserPath;
270 | 
271 |       // Check if the detected browser is Firefox
272 |       if (detectedBrowserPath.includes("firefox")) {
273 |         launchOptions.product = "firefox";
274 |         console.log("Setting product to firefox for Firefox browser");
275 |       }
276 |     } else {
277 |       detectedBrowserPath = await findBrowserExecutablePath();
278 |       launchOptions.executablePath = detectedBrowserPath;
279 | 
280 |       // Check if the detected browser is Firefox
281 |       if (detectedBrowserPath.includes("firefox")) {
282 |         launchOptions.product = "firefox";
283 |         console.log("Setting product to firefox for Firefox browser");
284 |       }
285 | 
286 |       console.log(
287 |         `Using detected browser path: ${launchOptions.executablePath}`
288 |       );
289 |     }
290 |   } catch (error) {
291 |     console.error("Failed to detect browser executable path:", error);
292 |     throw new Error(
293 |       "No browser executable path found. Please specify a custom browser path in the configuration."
294 |     );
295 |   }
296 | }
297 | 
298 | /**
299 |  * Find a browser executable path on the current system
300 |  * @returns Path to a browser executable
301 |  */
302 | async function findBrowserExecutablePath(): Promise<string> {
303 |   // Try to use chrome-launcher (most reliable method)
304 |   try {
305 |     console.log("Attempting to find Chrome using chrome-launcher...");
306 | 
307 |     // Launch Chrome using chrome-launcher
308 |     const chrome = await ChromeLauncher.launch({
309 |       chromeFlags: ["--headless"],
310 |       handleSIGINT: false,
311 |     });
312 | 
313 |     // chrome-launcher stores the Chrome executable path differently than Puppeteer
314 |     // Let's try different approaches to get it
315 | 
316 |     // First check if we can access it directly
317 |     let chromePath = "";
318 | 
319 |     // Chrome version data often contains the path
320 |     if (chrome.process && chrome.process.spawnfile) {
321 |       chromePath = chrome.process.spawnfile;
322 |       console.log("Found Chrome path from process.spawnfile");
323 |     } else {
324 |       // Try to get the Chrome path from chrome-launcher
325 |       // In newer versions, it's directly accessible
326 |       console.log("Trying to determine Chrome path using other methods");
327 | 
328 |       // This will actually return the real Chrome path for us
329 |       // chrome-launcher has this inside but doesn't expose it directly
330 |       const possiblePaths = [
331 |         process.env.CHROME_PATH,
332 |         // Common paths by OS
333 |         ...(process.platform === "darwin"
334 |           ? ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"]
335 |           : process.platform === "win32"
336 |           ? [
337 |               `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`,
338 |               `${process.env["PROGRAMFILES(X86)"]}\\Google\\Chrome\\Application\\chrome.exe`,
339 |             ]
340 |           : ["/usr/bin/google-chrome"]),
341 |       ].filter(Boolean);
342 | 
343 |       // Use the first valid path
344 |       for (const p of possiblePaths) {
345 |         if (p && fs.existsSync(p)) {
346 |           chromePath = p;
347 |           console.log("Found Chrome path from common locations");
348 |           break;
349 |         }
350 |       }
351 |     }
352 | 
353 |     // Always kill the Chrome instance we just launched
354 |     await chrome.kill();
355 | 
356 |     if (chromePath) {
357 |       console.log(`Chrome found via chrome-launcher: ${chromePath}`);
358 |       return chromePath;
359 |     } else {
360 |       console.log("Chrome launched but couldn't determine executable path");
361 |     }
362 |   } catch (error) {
363 |     // Check if it's a ChromeNotInstalledError
364 |     const errorMessage = error instanceof Error ? error.message : String(error);
365 |     if (
366 |       errorMessage.includes("No Chrome installations found") ||
367 |       (error as any)?.code === "ERR_LAUNCHER_NOT_INSTALLED"
368 |     ) {
369 |       console.log("Chrome not installed. Falling back to manual detection");
370 |     } else {
371 |       console.error("Failed to find Chrome using chrome-launcher:", error);
372 |       console.log("Falling back to manual detection");
373 |     }
374 |   }
375 | 
376 |   // If chrome-launcher failed, use manual detection
377 | 
378 |   const platform = process.platform;
379 |   const preferredBrowsers = currentConfig.preferredBrowsers || [
380 |     "chrome",
381 |     "edge",
382 |     "brave",
383 |     "firefox",
384 |   ];
385 | 
386 |   console.log(`Attempting to detect browser executable path on ${platform}...`);
387 | 
388 |   // Platform-specific detection strategies
389 |   if (platform === "win32") {
390 |     // Windows - try registry detection for Chrome
391 |     let registryPath = null;
392 |     try {
393 |       console.log("Checking Windows registry for Chrome...");
394 |       // Try HKLM first
395 |       const regOutput = execSync(
396 |         'reg query "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe" /ve',
397 |         { encoding: "utf8" }
398 |       );
399 | 
400 |       // Extract path from registry output
401 |       const match = regOutput.match(/REG_(?:SZ|EXPAND_SZ)\s+([^\s]+)/i);
402 |       if (match && match[1]) {
403 |         registryPath = match[1].replace(/\\"/g, "");
404 |         // Verify the path exists
405 |         if (fs.existsSync(registryPath)) {
406 |           console.log(`Found Chrome via HKLM registry: ${registryPath}`);
407 |           return registryPath;
408 |         }
409 |       }
410 |     } catch (e) {
411 |       // Try HKCU if HKLM fails
412 |       try {
413 |         console.log("Checking user registry for Chrome...");
414 |         const regOutput = execSync(
415 |           'reg query "HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe" /ve',
416 |           { encoding: "utf8" }
417 |         );
418 | 
419 |         // Extract path from registry output
420 |         const match = regOutput.match(/REG_(?:SZ|EXPAND_SZ)\s+([^\s]+)/i);
421 |         if (match && match[1]) {
422 |           registryPath = match[1].replace(/\\"/g, "");
423 |           // Verify the path exists
424 |           if (fs.existsSync(registryPath)) {
425 |             console.log(`Found Chrome via HKCU registry: ${registryPath}`);
426 |             return registryPath;
427 |           }
428 |         }
429 |       } catch (innerError) {
430 |         console.log(
431 |           "Failed to find Chrome via registry, continuing with path checks"
432 |         );
433 |       }
434 |     }
435 | 
436 |     // Try to find Chrome through BLBeacon registry key (version info)
437 |     try {
438 |       console.log("Checking Chrome BLBeacon registry...");
439 |       const regOutput = execSync(
440 |         'reg query "HKEY_CURRENT_USER\\Software\\Google\\Chrome\\BLBeacon" /v version',
441 |         { encoding: "utf8" }
442 |       );
443 | 
444 |       if (regOutput) {
445 |         // If BLBeacon exists, Chrome is likely installed in the default location
446 |         const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
447 |         const programFilesX86 =
448 |           process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
449 | 
450 |         const defaultChromePaths = [
451 |           path.join(programFiles, "Google\\Chrome\\Application\\chrome.exe"),
452 |           path.join(programFilesX86, "Google\\Chrome\\Application\\chrome.exe"),
453 |         ];
454 | 
455 |         for (const chromePath of defaultChromePaths) {
456 |           if (fs.existsSync(chromePath)) {
457 |             console.log(
458 |               `Found Chrome via BLBeacon registry hint: ${chromePath}`
459 |             );
460 |             return chromePath;
461 |           }
462 |         }
463 |       }
464 |     } catch (e) {
465 |       console.log("Failed to find Chrome via BLBeacon registry");
466 |     }
467 | 
468 |     // Continue with regular path checks
469 |     const programFiles = process.env.PROGRAMFILES || "C:\\Program Files";
470 |     const programFilesX86 =
471 |       process.env["PROGRAMFILES(X86)"] || "C:\\Program Files (x86)";
472 | 
473 |     // Common Windows browser paths
474 |     const winBrowserPaths = {
475 |       chrome: [
476 |         path.join(programFiles, "Google\\Chrome\\Application\\chrome.exe"),
477 |         path.join(programFilesX86, "Google\\Chrome\\Application\\chrome.exe"),
478 |       ],
479 |       edge: [
480 |         path.join(programFiles, "Microsoft\\Edge\\Application\\msedge.exe"),
481 |         path.join(programFilesX86, "Microsoft\\Edge\\Application\\msedge.exe"),
482 |       ],
483 |       brave: [
484 |         path.join(
485 |           programFiles,
486 |           "BraveSoftware\\Brave-Browser\\Application\\brave.exe"
487 |         ),
488 |         path.join(
489 |           programFilesX86,
490 |           "BraveSoftware\\Brave-Browser\\Application\\brave.exe"
491 |         ),
492 |       ],
493 |       firefox: [
494 |         path.join(programFiles, "Mozilla Firefox\\firefox.exe"),
495 |         path.join(programFilesX86, "Mozilla Firefox\\firefox.exe"),
496 |       ],
497 |     };
498 | 
499 |     // Check each browser in preferred order
500 |     for (const browser of preferredBrowsers) {
501 |       const paths =
502 |         winBrowserPaths[browser as keyof typeof winBrowserPaths] || [];
503 |       for (const browserPath of paths) {
504 |         if (fs.existsSync(browserPath)) {
505 |           console.log(`Found ${browser} at ${browserPath}`);
506 |           return browserPath;
507 |         }
508 |       }
509 |     }
510 |   } else if (platform === "darwin") {
511 |     // macOS browser paths
512 |     const macBrowserPaths = {
513 |       chrome: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"],
514 |       edge: ["/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"],
515 |       brave: ["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"],
516 |       firefox: ["/Applications/Firefox.app/Contents/MacOS/firefox"],
517 |       safari: ["/Applications/Safari.app/Contents/MacOS/Safari"],
518 |     };
519 | 
520 |     // Check each browser in preferred order
521 |     for (const browser of preferredBrowsers) {
522 |       const paths =
523 |         macBrowserPaths[browser as keyof typeof macBrowserPaths] || [];
524 |       for (const browserPath of paths) {
525 |         if (fs.existsSync(browserPath)) {
526 |           console.log(`Found ${browser} at ${browserPath}`);
527 |           // Safari is detected but not supported by Puppeteer
528 |           if (browser === "safari") {
529 |             console.log(
530 |               "Safari detected but not supported by Puppeteer. Continuing search..."
531 |             );
532 |             continue;
533 |           }
534 |           return browserPath;
535 |         }
536 |       }
537 |     }
538 |   } else if (platform === "linux") {
539 |     // Linux browser commands
540 |     const linuxBrowserCommands = {
541 |       chrome: ["google-chrome", "chromium", "chromium-browser"],
542 |       edge: ["microsoft-edge"],
543 |       brave: ["brave-browser"],
544 |       firefox: ["firefox"],
545 |     };
546 | 
547 |     // Check each browser in preferred order
548 |     for (const browser of preferredBrowsers) {
549 |       const commands =
550 |         linuxBrowserCommands[browser as keyof typeof linuxBrowserCommands] ||
551 |         [];
552 |       for (const cmd of commands) {
553 |         try {
554 |           // Use more universal commands for Linux to find executables
555 |           // command -v works in most shells, fallback to which or type
556 |           const browserPath = execSync(
557 |             `command -v ${cmd} || which ${cmd} || type -p ${cmd} 2>/dev/null`,
558 |             { encoding: "utf8" }
559 |           ).trim();
560 | 
561 |           if (browserPath && fs.existsSync(browserPath)) {
562 |             console.log(`Found ${browser} at ${browserPath}`);
563 |             return browserPath;
564 |           }
565 |         } catch (e) {
566 |           // Command not found, continue to next
567 |         }
568 |       }
569 |     }
570 | 
571 |     // Additional check for unusual locations on Linux
572 |     const alternativeLocations = [
573 |       "/usr/bin/google-chrome",
574 |       "/usr/bin/chromium",
575 |       "/usr/bin/chromium-browser",
576 |       "/snap/bin/chromium",
577 |       "/snap/bin/google-chrome",
578 |       "/opt/google/chrome/chrome",
579 |     ];
580 | 
581 |     for (const location of alternativeLocations) {
582 |       if (fs.existsSync(location)) {
583 |         console.log(`Found browser at alternative location: ${location}`);
584 |         return location;
585 |       }
586 |     }
587 |   }
588 | 
589 |   throw new Error(
590 |     `No browser executable found for platform ${platform}. Please specify a custom browser path.`
591 |   );
592 | }
593 | 
594 | /**
595 |  * Sets up cleanup handlers for the browser instance
596 |  * @param browser Browser instance
597 |  * @param userDataDir Path to the user data directory to clean up
598 |  */
599 | function setupBrowserCleanupHandlers(
600 |   browser: puppeteer.Browser,
601 |   userDataDir: string
602 | ): void {
603 |   browser.on("disconnected", () => {
604 |     console.log(`Browser disconnected. Scheduling cleanup for: ${userDataDir}`);
605 | 
606 |     // Clear any existing cleanup timeout when browser is disconnected
607 |     cancelScheduledCleanup();
608 | 
609 |     // Delayed cleanup to avoid conflicts with potential new browser instances
610 |     setTimeout(() => {
611 |       // Only remove the directory if no new browser has been launched
612 |       if (!headlessBrowserInstance) {
613 |         console.log(`Cleaning up temporary directory: ${userDataDir}`);
614 |         try {
615 |           fs.rmSync(userDataDir, { recursive: true, force: true });
616 |           console.log(`Successfully removed directory: ${userDataDir}`);
617 |         } catch (error) {
618 |           console.error(`Failed to remove directory ${userDataDir}:`, error);
619 |         }
620 |       } else {
621 |         console.log(
622 |           `Skipping cleanup for ${userDataDir} as new browser instance is active`
623 |         );
624 |       }
625 |     }, 5000); // 5-second delay for cleanup
626 | 
627 |     // Reset browser instance variables
628 |     launchedBrowserWSEndpoint = null;
629 |     headlessBrowserInstance = null;
630 |   });
631 | }
632 | 
633 | // ===== Cleanup Management =====
634 | 
635 | /**
636 |  * Cancels any scheduled browser cleanup
637 |  */
638 | function cancelScheduledCleanup(): void {
639 |   if (browserCleanupTimeout) {
640 |     console.log("Cancelling scheduled browser cleanup");
641 |     clearTimeout(browserCleanupTimeout);
642 |     browserCleanupTimeout = null;
643 |   }
644 | }
645 | 
646 | /**
647 |  * Schedules automatic cleanup of the browser instance after inactivity
648 |  */
649 | export function scheduleBrowserCleanup(): void {
650 |   // Clear any existing timeout first
651 |   cancelScheduledCleanup();
652 | 
653 |   // Only schedule cleanup if we have an active browser instance
654 |   if (headlessBrowserInstance) {
655 |     console.log(
656 |       `Scheduling browser cleanup in ${BROWSER_CLEANUP_TIMEOUT / 1000} seconds`
657 |     );
658 | 
659 |     browserCleanupTimeout = setTimeout(() => {
660 |       console.log("Executing scheduled browser cleanup");
661 |       if (headlessBrowserInstance) {
662 |         console.log("Closing headless browser instance");
663 |         headlessBrowserInstance.close();
664 |         headlessBrowserInstance = null;
665 |         launchedBrowserWSEndpoint = null;
666 |       }
667 |       browserCleanupTimeout = null;
668 |     }, BROWSER_CLEANUP_TIMEOUT);
669 |   }
670 | }
671 | 
672 | // ===== Public Browser Connection API =====
673 | 
674 | /**
675 |  * Connects to a headless browser for web operations
676 |  * @param url The URL to navigate to
677 |  * @param options Connection and emulation options
678 |  * @returns Promise resolving to browser, port, and page objects
679 |  */
680 | export async function connectToHeadlessBrowser(
681 |   url: string,
682 |   options: {
683 |     blockResources?: boolean;
684 |     customResourceBlockList?: string[];
685 |     emulateDevice?: "mobile" | "tablet" | "desktop";
686 |     emulateNetworkCondition?: "slow3G" | "fast3G" | "4G" | "offline";
687 |     viewport?: { width: number; height: number };
688 |     locale?: string;
689 |     timezoneId?: string;
690 |     userAgent?: string;
691 |     waitForSelector?: string;
692 |     waitForTimeout?: number;
693 |     cookies?: Array<{
694 |       name: string;
695 |       value: string;
696 |       domain?: string;
697 |       path?: string;
698 |     }>;
699 |     headers?: Record<string, string>;
700 |   } = {}
701 | ): Promise<{
702 |   browser: puppeteer.Browser;
703 |   port: number;
704 |   page: puppeteer.Page;
705 | }> {
706 |   console.log(
707 |     `Connecting to headless browser for ${url}${
708 |       options.blockResources ? " (blocking non-essential resources)" : ""
709 |     }`
710 |   );
711 | 
712 |   try {
713 |     // Validate URL format
714 |     try {
715 |       new URL(url);
716 |     } catch (e) {
717 |       throw new Error(`Invalid URL format: ${url}`);
718 |     }
719 | 
720 |     // Get or create a browser instance
721 |     const browser = await getHeadlessBrowserInstance();
722 | 
723 |     if (!launchedBrowserWSEndpoint) {
724 |       throw new Error("Failed to retrieve WebSocket endpoint for browser");
725 |     }
726 | 
727 |     // Extract port from WebSocket endpoint
728 |     const port = parseInt(
729 |       launchedBrowserWSEndpoint.split(":")[2].split("/")[0]
730 |     );
731 | 
732 |     // Always create a new page for each audit to avoid request interception conflicts
733 |     console.log("Creating a new page for this audit");
734 |     const page = await browser.newPage();
735 | 
736 |     // Set a longer timeout for navigation
737 |     const navigationTimeout = 10000; // 10 seconds
738 |     page.setDefaultNavigationTimeout(navigationTimeout);
739 | 
740 |     // Navigate to the URL
741 |     console.log(`Navigating to ${url}`);
742 |     await page.goto(url, {
743 |       waitUntil: "networkidle2", // Wait until there are no more network connections for at least 500ms
744 |       timeout: navigationTimeout,
745 |     });
746 | 
747 |     // Set custom headers if provided
748 |     if (options.headers && Object.keys(options.headers).length > 0) {
749 |       await page.setExtraHTTPHeaders(options.headers);
750 |       console.log("Set custom HTTP headers");
751 |     }
752 | 
753 |     // Set cookies if provided
754 |     if (options.cookies && options.cookies.length > 0) {
755 |       const urlObj = new URL(url);
756 |       const cookiesWithDomain = options.cookies.map((cookie) => ({
757 |         ...cookie,
758 |         domain: cookie.domain || urlObj.hostname,
759 |         path: cookie.path || "/",
760 |       }));
761 |       await page.setCookie(...cookiesWithDomain);
762 |       console.log(`Set ${options.cookies.length} cookies`);
763 |     }
764 | 
765 |     // Set custom viewport if specified
766 |     if (options.viewport) {
767 |       await page.setViewport(options.viewport);
768 |       console.log(
769 |         `Set viewport to ${options.viewport.width}x${options.viewport.height}`
770 |       );
771 |     } else if (options.emulateDevice) {
772 |       // Set common device emulation presets
773 |       let viewport;
774 |       let userAgent = options.userAgent;
775 | 
776 |       switch (options.emulateDevice) {
777 |         case "mobile":
778 |           viewport = {
779 |             width: 375,
780 |             height: 667,
781 |             isMobile: true,
782 |             hasTouch: true,
783 |           };
784 |           userAgent =
785 |             userAgent ||
786 |             "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X)";
787 |           break;
788 |         case "tablet":
789 |           viewport = {
790 |             width: 768,
791 |             height: 1024,
792 |             isMobile: true,
793 |             hasTouch: true,
794 |           };
795 |           userAgent =
796 |             userAgent || "Mozilla/5.0 (iPad; CPU OS 13_2_3 like Mac OS X)";
797 |           break;
798 |         case "desktop":
799 |         default:
800 |           viewport = {
801 |             width: 1280,
802 |             height: 800,
803 |             isMobile: false,
804 |             hasTouch: false,
805 |           };
806 |           break;
807 |       }
808 | 
809 |       await page.setViewport(viewport);
810 |       if (userAgent) await page.setUserAgent(userAgent);
811 | 
812 |       console.log(`Emulating ${options.emulateDevice} device`);
813 |     }
814 | 
815 |     // Set locale and timezone if provided
816 |     if (options.locale) {
817 |       await page.evaluateOnNewDocument((locale) => {
818 |         Object.defineProperty(navigator, "language", { get: () => locale });
819 |         Object.defineProperty(navigator, "languages", { get: () => [locale] });
820 |       }, options.locale);
821 |       console.log(`Set locale to ${options.locale}`);
822 |     }
823 | 
824 |     if (options.timezoneId) {
825 |       await page.emulateTimezone(options.timezoneId);
826 |       console.log(`Set timezone to ${options.timezoneId}`);
827 |     }
828 | 
829 |     // Emulate network conditions if specified
830 |     if (options.emulateNetworkCondition) {
831 |       // Define network condition types that match puppeteer's expected format
832 |       interface PuppeteerNetworkConditions {
833 |         offline: boolean;
834 |         latency?: number;
835 |         download?: number;
836 |         upload?: number;
837 |       }
838 | 
839 |       let networkConditions: PuppeteerNetworkConditions;
840 | 
841 |       switch (options.emulateNetworkCondition) {
842 |         case "slow3G":
843 |           networkConditions = {
844 |             offline: false,
845 |             latency: 400,
846 |             download: (500 * 1024) / 8,
847 |             upload: (500 * 1024) / 8,
848 |           };
849 |           break;
850 |         case "fast3G":
851 |           networkConditions = {
852 |             offline: false,
853 |             latency: 150,
854 |             download: (1.5 * 1024 * 1024) / 8,
855 |             upload: (750 * 1024) / 8,
856 |           };
857 |           break;
858 |         case "4G":
859 |           networkConditions = {
860 |             offline: false,
861 |             latency: 50,
862 |             download: (4 * 1024 * 1024) / 8,
863 |             upload: (2 * 1024 * 1024) / 8,
864 |           };
865 |           break;
866 |         case "offline":
867 |           networkConditions = { offline: true };
868 |           break;
869 |         default:
870 |           networkConditions = { offline: false };
871 |       }
872 | 
873 |       // @ts-ignore - Property might not be in types but is supported
874 |       await page.emulateNetworkConditions(networkConditions);
875 |       console.log(
876 |         `Emulating ${options.emulateNetworkCondition} network conditions`
877 |       );
878 |     }
879 | 
880 |     // Check if we should block resources based on the options
881 |     if (options.blockResources) {
882 |       const resourceTypesToBlock = options.customResourceBlockList ||
883 |         currentConfig.blockResourceTypes || ["image", "font", "media"];
884 | 
885 |       await page.setRequestInterception(true);
886 |       page.on("request", (request) => {
887 |         // Block unnecessary resources to speed up loading
888 |         const resourceType = request.resourceType();
889 |         if (resourceTypesToBlock.includes(resourceType)) {
890 |           request.abort();
891 |         } else {
892 |           request.continue();
893 |         }
894 |       });
895 | 
896 |       console.log(
897 |         `Blocking resource types: ${resourceTypesToBlock.join(", ")}`
898 |       );
899 |     }
900 | 
901 |     // Wait for a specific selector if requested
902 |     if (options.waitForSelector) {
903 |       try {
904 |         console.log(`Waiting for selector: ${options.waitForSelector}`);
905 |         await page.waitForSelector(options.waitForSelector, {
906 |           timeout: options.waitForTimeout || 30000,
907 |         });
908 |       } catch (selectorError: any) {
909 |         console.warn(
910 |           `Failed to find selector "${options.waitForSelector}": ${selectorError.message}`
911 |         );
912 |         // Continue anyway, don't fail the whole operation
913 |       }
914 |     }
915 | 
916 |     return { browser, port, page };
917 |   } catch (error) {
918 |     console.error("Failed to connect to headless browser:", error);
919 |     throw new Error(
920 |       `Failed to connect to headless browser: ${
921 |         error instanceof Error ? error.message : String(error)
922 |       }`
923 |     );
924 |   }
925 | }
926 | 
```

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

```javascript
  1 | // Store settings
  2 | let settings = {
  3 |   logLimit: 50,
  4 |   queryLimit: 30000,
  5 |   stringSizeLimit: 500,
  6 |   showRequestHeaders: false,
  7 |   showResponseHeaders: false,
  8 |   maxLogSize: 20000,
  9 |   screenshotPath: "",
 10 |   // Add server connection settings
 11 |   serverHost: "localhost",
 12 |   serverPort: 3025,
 13 |   allowAutoPaste: false, // Default auto-paste setting
 14 | };
 15 | 
 16 | // Track connection status
 17 | let serverConnected = false;
 18 | let reconnectAttemptTimeout = null;
 19 | // Add a flag to track ongoing discovery operations
 20 | let isDiscoveryInProgress = false;
 21 | // Add an AbortController to cancel fetch operations
 22 | let discoveryController = null;
 23 | 
 24 | // Load saved settings on startup
 25 | chrome.storage.local.get(["browserConnectorSettings"], (result) => {
 26 |   if (result.browserConnectorSettings) {
 27 |     settings = { ...settings, ...result.browserConnectorSettings };
 28 |     updateUIFromSettings();
 29 |   }
 30 | 
 31 |   // Create connection status banner at the top
 32 |   createConnectionBanner();
 33 | 
 34 |   // Automatically discover server on panel load with quiet mode enabled
 35 |   discoverServer(true);
 36 | });
 37 | 
 38 | // Add listener for connection status updates from background script (page refresh events)
 39 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
 40 |   if (message.type === "CONNECTION_STATUS_UPDATE") {
 41 |     console.log(
 42 |       `Received connection status update: ${
 43 |         message.isConnected ? "Connected" : "Disconnected"
 44 |       }`
 45 |     );
 46 | 
 47 |     // Update UI based on connection status
 48 |     if (message.isConnected) {
 49 |       // If already connected, just maintain the current state
 50 |       if (!serverConnected) {
 51 |         // Connection was re-established, update UI
 52 |         serverConnected = true;
 53 |         updateConnectionBanner(true, {
 54 |           name: "Browser Tools Server",
 55 |           version: "reconnected",
 56 |           host: settings.serverHost,
 57 |           port: settings.serverPort,
 58 |         });
 59 |       }
 60 |     } else {
 61 |       // Connection lost, update UI to show disconnected
 62 |       serverConnected = false;
 63 |       updateConnectionBanner(false, null);
 64 |     }
 65 |   }
 66 | 
 67 |   if (message.type === "INITIATE_AUTO_DISCOVERY") {
 68 |     console.log(
 69 |       `Initiating auto-discovery after page refresh (reason: ${message.reason})`
 70 |     );
 71 | 
 72 |     // For page refreshes or if forceRestart is set to true, always cancel any ongoing discovery and restart
 73 |     if (message.reason === "page_refresh" || message.forceRestart === true) {
 74 |       // Cancel any ongoing discovery operation
 75 |       cancelOngoingDiscovery();
 76 | 
 77 |       // Update UI to indicate we're starting a fresh scan
 78 |       if (connectionStatusDiv) {
 79 |         connectionStatusDiv.style.display = "block";
 80 |         if (statusIcon) statusIcon.className = "status-indicator";
 81 |         if (statusText)
 82 |           statusText.textContent =
 83 |             "Page refreshed. Restarting server discovery...";
 84 |       }
 85 | 
 86 |       // Always update the connection banner when a page refresh occurs
 87 |       updateConnectionBanner(false, null);
 88 | 
 89 |       // Start a new discovery process with quiet mode
 90 |       console.log("Starting fresh discovery after page refresh");
 91 |       discoverServer(true);
 92 |     }
 93 |     // For other types of auto-discovery requests, only start if not already in progress
 94 |     else if (!isDiscoveryInProgress) {
 95 |       // Use quiet mode for auto-discovery to minimize UI changes
 96 |       discoverServer(true);
 97 |     }
 98 |   }
 99 | 
100 |   // Handle successful server validation
101 |   if (message.type === "SERVER_VALIDATION_SUCCESS") {
102 |     console.log(
103 |       `Server validation successful: ${message.serverHost}:${message.serverPort}`
104 |     );
105 | 
106 |     // Update the connection status banner
107 |     serverConnected = true;
108 |     updateConnectionBanner(true, message.serverInfo);
109 | 
110 |     // If we were showing the connection status dialog, we can hide it now
111 |     if (connectionStatusDiv && connectionStatusDiv.style.display === "block") {
112 |       connectionStatusDiv.style.display = "none";
113 |     }
114 |   }
115 | 
116 |   // Handle failed server validation
117 |   if (message.type === "SERVER_VALIDATION_FAILED") {
118 |     console.log(
119 |       `Server validation failed: ${message.reason} - ${message.serverHost}:${message.serverPort}`
120 |     );
121 | 
122 |     // Update the connection status
123 |     serverConnected = false;
124 |     updateConnectionBanner(false, null);
125 | 
126 |     // Start auto-discovery if this was a page refresh validation
127 |     if (
128 |       message.reason === "connection_error" ||
129 |       message.reason === "http_error"
130 |     ) {
131 |       // If we're not already trying to discover the server, start the process
132 |       if (!isDiscoveryInProgress) {
133 |         console.log("Starting auto-discovery after validation failure");
134 |         discoverServer(true);
135 |       }
136 |     }
137 |   }
138 | 
139 |   // Handle successful WebSocket connection
140 |   if (message.type === "WEBSOCKET_CONNECTED") {
141 |     console.log(
142 |       `WebSocket connected to ${message.serverHost}:${message.serverPort}`
143 |     );
144 | 
145 |     // Update connection status if it wasn't already connected
146 |     if (!serverConnected) {
147 |       serverConnected = true;
148 |       updateConnectionBanner(true, {
149 |         name: "Browser Tools Server",
150 |         version: "connected via WebSocket",
151 |         host: message.serverHost,
152 |         port: message.serverPort,
153 |       });
154 |     }
155 |   }
156 | });
157 | 
158 | // Create connection status banner
159 | function createConnectionBanner() {
160 |   // Check if banner already exists
161 |   if (document.getElementById("connection-banner")) {
162 |     return;
163 |   }
164 | 
165 |   // Create the banner
166 |   const banner = document.createElement("div");
167 |   banner.id = "connection-banner";
168 |   banner.style.cssText = `
169 |     padding: 6px 0px; 
170 |     margin-bottom: 4px;
171 |     width: 40%; 
172 |     display: flex; 
173 |     flex-direction: column;
174 |     align-items: flex-start; 
175 |     background-color:rgba(0,0,0,0);
176 |     border-radius: 11px;
177 |     font-size: 11px;
178 |     font-weight: 500;
179 |     color: #ffffff;
180 |   `;
181 | 
182 |   // Create reconnect button (now placed at the top)
183 |   const reconnectButton = document.createElement("button");
184 |   reconnectButton.id = "banner-reconnect-btn";
185 |   reconnectButton.textContent = "Reconnect";
186 |   reconnectButton.style.cssText = `
187 |     background-color: #333333;
188 |     color: #ffffff;
189 |     border: 1px solid #444444;
190 |     border-radius: 3px;
191 |     padding: 2px 8px;
192 |     font-size: 10px;
193 |     cursor: pointer;
194 |     margin-bottom: 6px;
195 |     align-self: flex-start;
196 |     display: none;
197 |     transition: background-color 0.2s;
198 |   `;
199 |   reconnectButton.addEventListener("mouseover", () => {
200 |     reconnectButton.style.backgroundColor = "#444444";
201 |   });
202 |   reconnectButton.addEventListener("mouseout", () => {
203 |     reconnectButton.style.backgroundColor = "#333333";
204 |   });
205 |   reconnectButton.addEventListener("click", () => {
206 |     // Hide the button while reconnecting
207 |     reconnectButton.style.display = "none";
208 |     reconnectButton.textContent = "Reconnecting...";
209 | 
210 |     // Update UI to show searching state
211 |     updateConnectionBanner(false, null);
212 | 
213 |     // Try to discover server
214 |     discoverServer(false);
215 |   });
216 | 
217 |   // Create a container for the status indicator and text
218 |   const statusContainer = document.createElement("div");
219 |   statusContainer.style.cssText = `
220 |     display: flex;
221 |     align-items: center;
222 |     width: 100%;
223 |   `;
224 | 
225 |   // Create status indicator
226 |   const indicator = document.createElement("div");
227 |   indicator.id = "banner-status-indicator";
228 |   indicator.style.cssText = `
229 |     width: 6px; 
230 |     height: 6px; 
231 |     position: relative;
232 |     top: 1px;
233 |     border-radius: 50%; 
234 |     background-color: #ccc; 
235 |     margin-right: 8px; 
236 |     flex-shrink: 0;
237 |     transition: background-color 0.3s ease;
238 |   `;
239 | 
240 |   // Create status text
241 |   const statusText = document.createElement("div");
242 |   statusText.id = "banner-status-text";
243 |   statusText.textContent = "Searching for server...";
244 |   statusText.style.cssText =
245 |     "flex-grow: 1; font-weight: 400; letter-spacing: 0.1px; font-size: 11px;";
246 | 
247 |   // Add elements to statusContainer
248 |   statusContainer.appendChild(indicator);
249 |   statusContainer.appendChild(statusText);
250 | 
251 |   // Add elements to banner - reconnect button first, then status container
252 |   banner.appendChild(reconnectButton);
253 |   banner.appendChild(statusContainer);
254 | 
255 |   // Add banner to the beginning of the document body
256 |   // This ensures it's the very first element
257 |   document.body.prepend(banner);
258 | 
259 |   // Set initial state
260 |   updateConnectionBanner(false, null);
261 | }
262 | 
263 | // Update the connection banner with current status
264 | function updateConnectionBanner(connected, serverInfo) {
265 |   const indicator = document.getElementById("banner-status-indicator");
266 |   const statusText = document.getElementById("banner-status-text");
267 |   const banner = document.getElementById("connection-banner");
268 |   const reconnectButton = document.getElementById("banner-reconnect-btn");
269 | 
270 |   if (!indicator || !statusText || !banner || !reconnectButton) return;
271 | 
272 |   if (connected && serverInfo) {
273 |     // Connected state with server info
274 |     indicator.style.backgroundColor = "#4CAF50"; // Green indicator
275 |     statusText.style.color = "#ffffff"; // White text for contrast on black
276 |     statusText.textContent = `Connected to ${serverInfo.name} v${serverInfo.version} at ${settings.serverHost}:${settings.serverPort}`;
277 | 
278 |     // Hide reconnect button when connected
279 |     reconnectButton.style.display = "none";
280 |   } else if (connected) {
281 |     // Connected without server info
282 |     indicator.style.backgroundColor = "#4CAF50"; // Green indicator
283 |     statusText.style.color = "#ffffff"; // White text for contrast on black
284 |     statusText.textContent = `Connected to server at ${settings.serverHost}:${settings.serverPort}`;
285 | 
286 |     // Hide reconnect button when connected
287 |     reconnectButton.style.display = "none";
288 |   } else {
289 |     // Disconnected state
290 |     indicator.style.backgroundColor = "#F44336"; // Red indicator
291 |     statusText.style.color = "#ffffff"; // White text for contrast on black
292 | 
293 |     // Only show "searching" message if discovery is in progress
294 |     if (isDiscoveryInProgress) {
295 |       statusText.textContent = "Not connected to server. Searching...";
296 |       // Hide reconnect button while actively searching
297 |       reconnectButton.style.display = "none";
298 |     } else {
299 |       statusText.textContent = "Not connected to server.";
300 |       // Show reconnect button above status message when disconnected and not searching
301 |       reconnectButton.style.display = "block";
302 |       reconnectButton.textContent = "Reconnect";
303 |     }
304 |   }
305 | }
306 | 
307 | // Initialize UI elements
308 | const logLimitInput = document.getElementById("log-limit");
309 | const queryLimitInput = document.getElementById("query-limit");
310 | const stringSizeLimitInput = document.getElementById("string-size-limit");
311 | const showRequestHeadersCheckbox = document.getElementById(
312 |   "show-request-headers"
313 | );
314 | const showResponseHeadersCheckbox = document.getElementById(
315 |   "show-response-headers"
316 | );
317 | const maxLogSizeInput = document.getElementById("max-log-size");
318 | const screenshotPathInput = document.getElementById("screenshot-path");
319 | const captureScreenshotButton = document.getElementById("capture-screenshot");
320 | 
321 | // Server connection UI elements
322 | const serverHostInput = document.getElementById("server-host");
323 | const serverPortInput = document.getElementById("server-port");
324 | const discoverServerButton = document.getElementById("discover-server");
325 | const testConnectionButton = document.getElementById("test-connection");
326 | const connectionStatusDiv = document.getElementById("connection-status");
327 | const statusIcon = document.getElementById("status-icon");
328 | const statusText = document.getElementById("status-text");
329 | 
330 | // Initialize collapsible advanced settings
331 | const advancedSettingsHeader = document.getElementById(
332 |   "advanced-settings-header"
333 | );
334 | const advancedSettingsContent = document.getElementById(
335 |   "advanced-settings-content"
336 | );
337 | const chevronIcon = advancedSettingsHeader.querySelector(".chevron");
338 | 
339 | advancedSettingsHeader.addEventListener("click", () => {
340 |   advancedSettingsContent.classList.toggle("visible");
341 |   chevronIcon.classList.toggle("open");
342 | });
343 | 
344 | // Get all inputs by ID
345 | const allowAutoPasteCheckbox = document.getElementById("allow-auto-paste");
346 | 
347 | // Update UI from settings
348 | function updateUIFromSettings() {
349 |   logLimitInput.value = settings.logLimit;
350 |   queryLimitInput.value = settings.queryLimit;
351 |   stringSizeLimitInput.value = settings.stringSizeLimit;
352 |   showRequestHeadersCheckbox.checked = settings.showRequestHeaders;
353 |   showResponseHeadersCheckbox.checked = settings.showResponseHeaders;
354 |   maxLogSizeInput.value = settings.maxLogSize;
355 |   screenshotPathInput.value = settings.screenshotPath;
356 |   serverHostInput.value = settings.serverHost;
357 |   serverPortInput.value = settings.serverPort;
358 |   allowAutoPasteCheckbox.checked = settings.allowAutoPaste;
359 | }
360 | 
361 | // Save settings
362 | function saveSettings() {
363 |   chrome.storage.local.set({ browserConnectorSettings: settings });
364 |   // Notify devtools.js about settings change
365 |   chrome.runtime.sendMessage({
366 |     type: "SETTINGS_UPDATED",
367 |     settings,
368 |   });
369 | }
370 | 
371 | // Add event listeners for all inputs
372 | logLimitInput.addEventListener("change", (e) => {
373 |   settings.logLimit = parseInt(e.target.value, 10);
374 |   saveSettings();
375 | });
376 | 
377 | queryLimitInput.addEventListener("change", (e) => {
378 |   settings.queryLimit = parseInt(e.target.value, 10);
379 |   saveSettings();
380 | });
381 | 
382 | stringSizeLimitInput.addEventListener("change", (e) => {
383 |   settings.stringSizeLimit = parseInt(e.target.value, 10);
384 |   saveSettings();
385 | });
386 | 
387 | showRequestHeadersCheckbox.addEventListener("change", (e) => {
388 |   settings.showRequestHeaders = e.target.checked;
389 |   saveSettings();
390 | });
391 | 
392 | showResponseHeadersCheckbox.addEventListener("change", (e) => {
393 |   settings.showResponseHeaders = e.target.checked;
394 |   saveSettings();
395 | });
396 | 
397 | maxLogSizeInput.addEventListener("change", (e) => {
398 |   settings.maxLogSize = parseInt(e.target.value, 10);
399 |   saveSettings();
400 | });
401 | 
402 | screenshotPathInput.addEventListener("change", (e) => {
403 |   settings.screenshotPath = e.target.value;
404 |   saveSettings();
405 | });
406 | 
407 | // Add event listeners for server settings
408 | serverHostInput.addEventListener("change", (e) => {
409 |   settings.serverHost = e.target.value;
410 |   saveSettings();
411 |   // Automatically test connection when host is changed
412 |   testConnection(settings.serverHost, settings.serverPort);
413 | });
414 | 
415 | serverPortInput.addEventListener("change", (e) => {
416 |   settings.serverPort = parseInt(e.target.value, 10);
417 |   saveSettings();
418 |   // Automatically test connection when port is changed
419 |   testConnection(settings.serverHost, settings.serverPort);
420 | });
421 | 
422 | // Add event listener for auto-paste checkbox
423 | allowAutoPasteCheckbox.addEventListener("change", (e) => {
424 |   settings.allowAutoPaste = e.target.checked;
425 |   saveSettings();
426 | });
427 | 
428 | // Function to cancel any ongoing discovery operations
429 | function cancelOngoingDiscovery() {
430 |   if (isDiscoveryInProgress) {
431 |     console.log("Cancelling ongoing discovery operation");
432 | 
433 |     // Abort any fetch requests in progress
434 |     if (discoveryController) {
435 |       try {
436 |         discoveryController.abort();
437 |       } catch (error) {
438 |         console.error("Error aborting discovery controller:", error);
439 |       }
440 |       discoveryController = null;
441 |     }
442 | 
443 |     // Reset the discovery status
444 |     isDiscoveryInProgress = false;
445 | 
446 |     // Update UI to indicate the operation was cancelled
447 |     if (
448 |       statusText &&
449 |       connectionStatusDiv &&
450 |       connectionStatusDiv.style.display === "block"
451 |     ) {
452 |       statusText.textContent = "Server discovery operation cancelled";
453 |     }
454 | 
455 |     // Clear any pending network timeouts that might be part of the discovery process
456 |     clearTimeout(reconnectAttemptTimeout);
457 |     reconnectAttemptTimeout = null;
458 | 
459 |     console.log("Discovery operation cancelled successfully");
460 |   }
461 | }
462 | 
463 | // Test server connection
464 | testConnectionButton.addEventListener("click", async () => {
465 |   // Cancel any ongoing discovery operations before testing
466 |   cancelOngoingDiscovery();
467 |   await testConnection(settings.serverHost, settings.serverPort);
468 | });
469 | 
470 | // Function to test server connection
471 | async function testConnection(host, port) {
472 |   // Cancel any ongoing discovery operations
473 |   cancelOngoingDiscovery();
474 | 
475 |   connectionStatusDiv.style.display = "block";
476 |   statusIcon.className = "status-indicator";
477 |   statusText.textContent = "Testing connection...";
478 | 
479 |   try {
480 |     // Use the identity endpoint instead of .port for more reliable validation
481 |     const response = await fetch(`http://${host}:${port}/.identity`, {
482 |       signal: AbortSignal.timeout(5000), // 5 second timeout
483 |     });
484 | 
485 |     if (response.ok) {
486 |       const identity = await response.json();
487 | 
488 |       // Verify this is actually our server by checking the signature
489 |       if (identity.signature !== "mcp-browser-connector-24x7") {
490 |         statusIcon.className = "status-indicator status-disconnected";
491 |         statusText.textContent = `Connection failed: Found a server at ${host}:${port} but it's not the Browser Tools server`;
492 |         serverConnected = false;
493 |         updateConnectionBanner(false, null);
494 |         scheduleReconnectAttempt();
495 |         return false;
496 |       }
497 | 
498 |       statusIcon.className = "status-indicator status-connected";
499 |       statusText.textContent = `Connected successfully to ${identity.name} v${identity.version} at ${host}:${port}`;
500 |       serverConnected = true;
501 |       updateConnectionBanner(true, identity);
502 | 
503 |       // Clear any scheduled reconnect attempts
504 |       if (reconnectAttemptTimeout) {
505 |         clearTimeout(reconnectAttemptTimeout);
506 |         reconnectAttemptTimeout = null;
507 |       }
508 | 
509 |       // Update settings if different port was discovered
510 |       if (parseInt(identity.port, 10) !== port) {
511 |         console.log(`Detected different port: ${identity.port}`);
512 |         settings.serverPort = parseInt(identity.port, 10);
513 |         serverPortInput.value = settings.serverPort;
514 |         saveSettings();
515 |       }
516 | 
517 |       return true;
518 |     } else {
519 |       statusIcon.className = "status-indicator status-disconnected";
520 |       statusText.textContent = `Connection failed: Server returned ${response.status}`;
521 |       serverConnected = false;
522 | 
523 |       // Make sure isDiscoveryInProgress is false so the reconnect button will show
524 |       isDiscoveryInProgress = false;
525 | 
526 |       // Now update the connection banner to show the reconnect button
527 |       updateConnectionBanner(false, null);
528 |       scheduleReconnectAttempt();
529 |       return false;
530 |     }
531 |   } catch (error) {
532 |     statusIcon.className = "status-indicator status-disconnected";
533 |     statusText.textContent = `Connection failed: ${error.message}`;
534 |     serverConnected = false;
535 | 
536 |     // Make sure isDiscoveryInProgress is false so the reconnect button will show
537 |     isDiscoveryInProgress = false;
538 | 
539 |     // Now update the connection banner to show the reconnect button
540 |     updateConnectionBanner(false, null);
541 |     scheduleReconnectAttempt();
542 |     return false;
543 |   }
544 | }
545 | 
546 | // Schedule a reconnect attempt if server isn't found
547 | function scheduleReconnectAttempt() {
548 |   // Clear any existing reconnect timeout
549 |   if (reconnectAttemptTimeout) {
550 |     clearTimeout(reconnectAttemptTimeout);
551 |   }
552 | 
553 |   // Schedule a reconnect attempt in 30 seconds
554 |   reconnectAttemptTimeout = setTimeout(() => {
555 |     console.log("Attempting to reconnect to server...");
556 |     // Only show minimal UI during auto-reconnect
557 |     discoverServer(true);
558 |   }, 30000); // 30 seconds
559 | }
560 | 
561 | // Helper function to try connecting to a server
562 | async function tryServerConnection(host, port) {
563 |   try {
564 |     // Check if the discovery process was cancelled
565 |     if (!isDiscoveryInProgress) {
566 |       return false;
567 |     }
568 | 
569 |     // Create a local timeout that won't abort the entire discovery process
570 |     const controller = new AbortController();
571 |     const timeoutId = setTimeout(() => {
572 |       controller.abort();
573 |     }, 500); // 500ms timeout for each connection attempt
574 | 
575 |     try {
576 |       // Use identity endpoint for validation
577 |       const response = await fetch(`http://${host}:${port}/.identity`, {
578 |         // Use a local controller for this specific request timeout
579 |         // but also respect the global discovery cancellation
580 |         signal: discoveryController
581 |           ? AbortSignal.any([controller.signal, discoveryController.signal])
582 |           : controller.signal,
583 |       });
584 | 
585 |       clearTimeout(timeoutId);
586 | 
587 |       // Check again if discovery was cancelled during the fetch
588 |       if (!isDiscoveryInProgress) {
589 |         return false;
590 |       }
591 | 
592 |       if (response.ok) {
593 |         const identity = await response.json();
594 | 
595 |         // Verify this is actually our server by checking the signature
596 |         if (identity.signature !== "mcp-browser-connector-24x7") {
597 |           console.log(
598 |             `Found a server at ${host}:${port} but it's not the Browser Tools server`
599 |           );
600 |           return false;
601 |         }
602 | 
603 |         console.log(`Successfully found server at ${host}:${port}`);
604 | 
605 |         // Update settings with discovered server
606 |         settings.serverHost = host;
607 |         settings.serverPort = parseInt(identity.port, 10);
608 |         serverHostInput.value = settings.serverHost;
609 |         serverPortInput.value = settings.serverPort;
610 |         saveSettings();
611 | 
612 |         statusIcon.className = "status-indicator status-connected";
613 |         statusText.textContent = `Discovered ${identity.name} v${identity.version} at ${host}:${identity.port}`;
614 | 
615 |         // Update connection banner with server info
616 |         updateConnectionBanner(true, identity);
617 | 
618 |         // Update connection status
619 |         serverConnected = true;
620 | 
621 |         // Clear any scheduled reconnect attempts
622 |         if (reconnectAttemptTimeout) {
623 |           clearTimeout(reconnectAttemptTimeout);
624 |           reconnectAttemptTimeout = null;
625 |         }
626 | 
627 |         // End the discovery process
628 |         isDiscoveryInProgress = false;
629 | 
630 |         // Successfully found server
631 |         return true;
632 |       }
633 | 
634 |       return false;
635 |     } finally {
636 |       clearTimeout(timeoutId);
637 |     }
638 |   } catch (error) {
639 |     // Ignore connection errors during discovery
640 |     // But check if it was an abort (cancellation)
641 |     if (error.name === "AbortError") {
642 |       // Check if this was due to the global discovery cancellation
643 |       if (discoveryController && discoveryController.signal.aborted) {
644 |         console.log("Connection attempt aborted by global cancellation");
645 |         return "aborted";
646 |       }
647 |       // Otherwise it was just a timeout for this specific connection attempt
648 |       return false;
649 |     }
650 |     console.log(`Connection error for ${host}:${port}: ${error.message}`);
651 |     return false;
652 |   }
653 | }
654 | 
655 | // Server discovery function (extracted to be reusable)
656 | async function discoverServer(quietMode = false) {
657 |   // Cancel any ongoing discovery operations before starting a new one
658 |   cancelOngoingDiscovery();
659 | 
660 |   // Create a new AbortController for this discovery process
661 |   discoveryController = new AbortController();
662 |   isDiscoveryInProgress = true;
663 | 
664 |   // In quiet mode, we don't show the connection status until we either succeed or fail completely
665 |   if (!quietMode) {
666 |     connectionStatusDiv.style.display = "block";
667 |     statusIcon.className = "status-indicator";
668 |     statusText.textContent = "Discovering server...";
669 |   }
670 | 
671 |   // Always update the connection banner
672 |   updateConnectionBanner(false, null);
673 | 
674 |   try {
675 |     console.log("Starting server discovery process");
676 | 
677 |     // Add an early cancellation listener that will respond to page navigation/refresh
678 |     discoveryController.signal.addEventListener("abort", () => {
679 |       console.log("Discovery aborted via AbortController signal");
680 |       isDiscoveryInProgress = false;
681 |     });
682 | 
683 |     // Common IPs to try (in order of likelihood)
684 |     const hosts = ["localhost", "127.0.0.1"];
685 | 
686 |     // Add the current configured host if it's not already in the list
687 |     if (
688 |       !hosts.includes(settings.serverHost) &&
689 |       settings.serverHost !== "0.0.0.0"
690 |     ) {
691 |       hosts.unshift(settings.serverHost); // Put at the beginning for priority
692 |     }
693 | 
694 |     // Add common local network IPs
695 |     const commonLocalIps = ["192.168.0.", "192.168.1.", "10.0.0.", "10.0.1."];
696 |     for (const prefix of commonLocalIps) {
697 |       for (let i = 1; i <= 5; i++) {
698 |         // Reduced from 10 to 5 for efficiency
699 |         hosts.push(`${prefix}${i}`);
700 |       }
701 |     }
702 | 
703 |     // Build port list in a smart order:
704 |     // 1. Start with current configured port
705 |     // 2. Add default port (3025)
706 |     // 3. Add sequential ports around the default (for fallback detection)
707 |     const ports = [];
708 | 
709 |     // Current configured port gets highest priority
710 |     const configuredPort = parseInt(settings.serverPort, 10);
711 |     ports.push(configuredPort);
712 | 
713 |     // Add default port if it's not the same as configured
714 |     if (configuredPort !== 3025) {
715 |       ports.push(3025);
716 |     }
717 | 
718 |     // Add sequential fallback ports (from default up to default+10)
719 |     for (let p = 3026; p <= 3035; p++) {
720 |       if (p !== configuredPort) {
721 |         // Avoid duplicates
722 |         ports.push(p);
723 |       }
724 |     }
725 | 
726 |     // Remove duplicates
727 |     const uniquePorts = [...new Set(ports)];
728 |     console.log("Will check ports:", uniquePorts);
729 | 
730 |     // Create a progress indicator
731 |     let progress = 0;
732 |     let totalChecked = 0;
733 | 
734 |     // Phase 1: Try the most likely combinations first (current host:port and localhost variants)
735 |     console.log("Starting Phase 1: Quick check of high-priority hosts/ports");
736 |     const priorityHosts = hosts.slice(0, 2); // First two hosts are highest priority
737 |     for (const host of priorityHosts) {
738 |       // Check if discovery was cancelled
739 |       if (!isDiscoveryInProgress) {
740 |         console.log("Discovery process was cancelled during Phase 1");
741 |         return false;
742 |       }
743 | 
744 |       // Try configured port first
745 |       totalChecked++;
746 |       if (!quietMode) {
747 |         statusText.textContent = `Checking ${host}:${uniquePorts[0]}...`;
748 |       }
749 |       console.log(`Checking ${host}:${uniquePorts[0]}...`);
750 |       const result = await tryServerConnection(host, uniquePorts[0]);
751 | 
752 |       // Check for cancellation or success
753 |       if (result === "aborted" || !isDiscoveryInProgress) {
754 |         console.log("Discovery process was cancelled");
755 |         return false;
756 |       } else if (result === true) {
757 |         console.log("Server found in priority check");
758 |         if (quietMode) {
759 |           // In quiet mode, only show the connection banner but hide the status box
760 |           connectionStatusDiv.style.display = "none";
761 |         }
762 |         return true; // Successfully found server
763 |       }
764 | 
765 |       // Then try default port if different
766 |       if (uniquePorts.length > 1) {
767 |         // Check if discovery was cancelled
768 |         if (!isDiscoveryInProgress) {
769 |           console.log("Discovery process was cancelled");
770 |           return false;
771 |         }
772 | 
773 |         totalChecked++;
774 |         if (!quietMode) {
775 |           statusText.textContent = `Checking ${host}:${uniquePorts[1]}...`;
776 |         }
777 |         console.log(`Checking ${host}:${uniquePorts[1]}...`);
778 |         const result = await tryServerConnection(host, uniquePorts[1]);
779 | 
780 |         // Check for cancellation or success
781 |         if (result === "aborted" || !isDiscoveryInProgress) {
782 |           console.log("Discovery process was cancelled");
783 |           return false;
784 |         } else if (result === true) {
785 |           console.log("Server found in priority check");
786 |           if (quietMode) {
787 |             // In quiet mode, only show the connection banner but hide the status box
788 |             connectionStatusDiv.style.display = "none";
789 |           }
790 |           return true; // Successfully found server
791 |         }
792 |       }
793 |     }
794 | 
795 |     // If we're in quiet mode and the quick checks failed, show the status now
796 |     // as we move into more intensive scanning
797 |     if (quietMode) {
798 |       connectionStatusDiv.style.display = "block";
799 |       statusIcon.className = "status-indicator";
800 |       statusText.textContent = "Searching for server...";
801 |     }
802 | 
803 |     // Phase 2: Systematic scan of all combinations
804 |     const totalAttempts = hosts.length * uniquePorts.length;
805 |     console.log(
806 |       `Starting Phase 2: Full scan (${totalAttempts} total combinations)`
807 |     );
808 |     statusText.textContent = `Quick check failed. Starting full scan (${totalChecked}/${totalAttempts})...`;
809 | 
810 |     // First, scan through all ports on localhost/127.0.0.1 to find fallback ports quickly
811 |     const localHosts = ["localhost", "127.0.0.1"];
812 |     for (const host of localHosts) {
813 |       // Skip the first two ports on localhost if we already checked them in Phase 1
814 |       const portsToCheck = uniquePorts.slice(
815 |         localHosts.includes(host) && priorityHosts.includes(host) ? 2 : 0
816 |       );
817 | 
818 |       for (const port of portsToCheck) {
819 |         // Check if discovery was cancelled
820 |         if (!isDiscoveryInProgress) {
821 |           console.log("Discovery process was cancelled during local port scan");
822 |           return false;
823 |         }
824 | 
825 |         // Update progress
826 |         progress++;
827 |         totalChecked++;
828 |         statusText.textContent = `Scanning local ports... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;
829 |         console.log(`Checking ${host}:${port}...`);
830 | 
831 |         const result = await tryServerConnection(host, port);
832 | 
833 |         // Check for cancellation or success
834 |         if (result === "aborted" || !isDiscoveryInProgress) {
835 |           console.log("Discovery process was cancelled");
836 |           return false;
837 |         } else if (result === true) {
838 |           console.log(`Server found at ${host}:${port}`);
839 |           return true; // Successfully found server
840 |         }
841 |       }
842 |     }
843 | 
844 |     // Then scan all the remaining host/port combinations
845 |     for (const host of hosts) {
846 |       // Skip hosts we already checked
847 |       if (localHosts.includes(host)) {
848 |         continue;
849 |       }
850 | 
851 |       for (const port of uniquePorts) {
852 |         // Check if discovery was cancelled
853 |         if (!isDiscoveryInProgress) {
854 |           console.log("Discovery process was cancelled during remote scan");
855 |           return false;
856 |         }
857 | 
858 |         // Update progress
859 |         progress++;
860 |         totalChecked++;
861 |         statusText.textContent = `Scanning remote hosts... (${totalChecked}/${totalAttempts}) - Trying ${host}:${port}`;
862 |         console.log(`Checking ${host}:${port}...`);
863 | 
864 |         const result = await tryServerConnection(host, port);
865 | 
866 |         // Check for cancellation or success
867 |         if (result === "aborted" || !isDiscoveryInProgress) {
868 |           console.log("Discovery process was cancelled");
869 |           return false;
870 |         } else if (result === true) {
871 |           console.log(`Server found at ${host}:${port}`);
872 |           return true; // Successfully found server
873 |         }
874 |       }
875 |     }
876 | 
877 |     console.log(
878 |       `Discovery process completed, checked ${totalChecked} combinations, no server found`
879 |     );
880 |     // If we get here, no server was found
881 |     statusIcon.className = "status-indicator status-disconnected";
882 |     statusText.textContent =
883 |       "No server found. Please check server is running and try again.";
884 | 
885 |     serverConnected = false;
886 | 
887 |     // End the discovery process first before updating the banner
888 |     isDiscoveryInProgress = false;
889 | 
890 |     // Update the connection banner to show the reconnect button
891 |     updateConnectionBanner(false, null);
892 | 
893 |     // Schedule a reconnect attempt
894 |     scheduleReconnectAttempt();
895 | 
896 |     return false;
897 |   } catch (error) {
898 |     console.error("Error during server discovery:", error);
899 |     statusIcon.className = "status-indicator status-disconnected";
900 |     statusText.textContent = `Error discovering server: ${error.message}`;
901 | 
902 |     serverConnected = false;
903 | 
904 |     // End the discovery process first before updating the banner
905 |     isDiscoveryInProgress = false;
906 | 
907 |     // Update the connection banner to show the reconnect button
908 |     updateConnectionBanner(false, null);
909 | 
910 |     // Schedule a reconnect attempt
911 |     scheduleReconnectAttempt();
912 | 
913 |     return false;
914 |   } finally {
915 |     console.log("Discovery process finished");
916 |     // Always clean up, even if there was an error
917 |     if (discoveryController) {
918 |       discoveryController = null;
919 |     }
920 |   }
921 | }
922 | 
923 | // Bind discover server button to the extracted function
924 | discoverServerButton.addEventListener("click", () => discoverServer(false));
925 | 
926 | // Screenshot capture functionality
927 | captureScreenshotButton.addEventListener("click", () => {
928 |   captureScreenshotButton.textContent = "Capturing...";
929 | 
930 |   // Send message to background script to capture screenshot
931 |   chrome.runtime.sendMessage(
932 |     {
933 |       type: "CAPTURE_SCREENSHOT",
934 |       tabId: chrome.devtools.inspectedWindow.tabId,
935 |       screenshotPath: settings.screenshotPath,
936 |     },
937 |     (response) => {
938 |       console.log("Screenshot capture response:", response);
939 |       if (!response) {
940 |         captureScreenshotButton.textContent = "Failed to capture!";
941 |         console.error("Screenshot capture failed: No response received");
942 |       } else if (!response.success) {
943 |         captureScreenshotButton.textContent = "Failed to capture!";
944 |         console.error("Screenshot capture failed:", response.error);
945 |       } else {
946 |         captureScreenshotButton.textContent = `Captured: ${response.title}`;
947 |         console.log("Screenshot captured successfully:", response.path);
948 |       }
949 |       setTimeout(() => {
950 |         captureScreenshotButton.textContent = "Capture Screenshot";
951 |       }, 2000);
952 |     }
953 |   );
954 | });
955 | 
956 | // Add wipe logs functionality
957 | const wipeLogsButton = document.getElementById("wipe-logs");
958 | wipeLogsButton.addEventListener("click", () => {
959 |   const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/wipelogs`;
960 |   console.log(`Sending wipe request to ${serverUrl}`);
961 | 
962 |   fetch(serverUrl, {
963 |     method: "POST",
964 |     headers: { "Content-Type": "application/json" },
965 |   })
966 |     .then((response) => response.json())
967 |     .then((result) => {
968 |       console.log("Logs wiped successfully:", result.message);
969 |       wipeLogsButton.textContent = "Logs Wiped!";
970 |       setTimeout(() => {
971 |         wipeLogsButton.textContent = "Wipe All Logs";
972 |       }, 2000);
973 |     })
974 |     .catch((error) => {
975 |       console.error("Failed to wipe logs:", error);
976 |       wipeLogsButton.textContent = "Failed to Wipe Logs";
977 |       setTimeout(() => {
978 |         wipeLogsButton.textContent = "Wipe All Logs";
979 |       }, 2000);
980 |     });
981 | });
982 | 
```

--------------------------------------------------------------------------------
/chrome-extension/devtools.js:
--------------------------------------------------------------------------------

```javascript
   1 | // devtools.js
   2 | 
   3 | // Store settings with defaults
   4 | let settings = {
   5 |   logLimit: 50,
   6 |   queryLimit: 30000,
   7 |   stringSizeLimit: 500,
   8 |   maxLogSize: 20000,
   9 |   showRequestHeaders: false,
  10 |   showResponseHeaders: false,
  11 |   screenshotPath: "", // Add new setting for screenshot path
  12 |   serverHost: "localhost", // Default server host
  13 |   serverPort: 3025, // Default server port
  14 |   allowAutoPaste: false, // Default auto-paste setting
  15 | };
  16 | 
  17 | // Keep track of debugger state
  18 | let isDebuggerAttached = false;
  19 | let attachDebuggerRetries = 0;
  20 | const currentTabId = chrome.devtools.inspectedWindow.tabId;
  21 | const MAX_ATTACH_RETRIES = 3;
  22 | const ATTACH_RETRY_DELAY = 1000; // 1 second
  23 | 
  24 | // Load saved settings on startup
  25 | chrome.storage.local.get(["browserConnectorSettings"], (result) => {
  26 |   if (result.browserConnectorSettings) {
  27 |     settings = { ...settings, ...result.browserConnectorSettings };
  28 |   }
  29 | });
  30 | 
  31 | // Listen for settings updates
  32 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  33 |   if (message.type === "SETTINGS_UPDATED") {
  34 |     settings = message.settings;
  35 | 
  36 |     // If server settings changed and we have a WebSocket, reconnect
  37 |     if (
  38 |       ws &&
  39 |       (message.settings.serverHost !== settings.serverHost ||
  40 |         message.settings.serverPort !== settings.serverPort)
  41 |     ) {
  42 |       console.log("Server settings changed, reconnecting WebSocket...");
  43 |       setupWebSocket();
  44 |     }
  45 |   }
  46 | 
  47 |   // Handle connection status updates from page refreshes
  48 |   if (message.type === "CONNECTION_STATUS_UPDATE") {
  49 |     console.log(
  50 |       `DevTools received connection status update: ${
  51 |         message.isConnected ? "Connected" : "Disconnected"
  52 |       }`
  53 |     );
  54 | 
  55 |     // If connection is lost, try to reestablish WebSocket only if we had a previous connection
  56 |     if (!message.isConnected && ws) {
  57 |       console.log(
  58 |         "Connection lost after page refresh, will attempt to reconnect WebSocket"
  59 |       );
  60 | 
  61 |       // Only reconnect if we actually have a WebSocket that might be stale
  62 |       if (
  63 |         ws &&
  64 |         (ws.readyState === WebSocket.CLOSED ||
  65 |           ws.readyState === WebSocket.CLOSING)
  66 |       ) {
  67 |         console.log("WebSocket is already closed or closing, will reconnect");
  68 |         setupWebSocket();
  69 |       }
  70 |     }
  71 |   }
  72 | 
  73 |   // Handle auto-discovery requests after page refreshes
  74 |   if (message.type === "INITIATE_AUTO_DISCOVERY") {
  75 |     console.log(
  76 |       `DevTools initiating WebSocket reconnect after page refresh (reason: ${message.reason})`
  77 |     );
  78 | 
  79 |     // For page refreshes with forceRestart, we should always reconnect if our current connection is not working
  80 |     if (
  81 |       (message.reason === "page_refresh" || message.forceRestart === true) &&
  82 |       (!ws || ws.readyState !== WebSocket.OPEN)
  83 |     ) {
  84 |       console.log(
  85 |         "Page refreshed and WebSocket not open - forcing reconnection"
  86 |       );
  87 | 
  88 |       // Close existing WebSocket if any
  89 |       if (ws) {
  90 |         console.log("Closing existing WebSocket due to page refresh");
  91 |         intentionalClosure = true; // Mark as intentional to prevent auto-reconnect
  92 |         try {
  93 |           ws.close();
  94 |         } catch (e) {
  95 |           console.error("Error closing WebSocket:", e);
  96 |         }
  97 |         ws = null;
  98 |         intentionalClosure = false; // Reset flag
  99 |       }
 100 | 
 101 |       // Clear any pending reconnect timeouts
 102 |       if (wsReconnectTimeout) {
 103 |         clearTimeout(wsReconnectTimeout);
 104 |         wsReconnectTimeout = null;
 105 |       }
 106 | 
 107 |       // Try to reestablish the WebSocket connection
 108 |       setupWebSocket();
 109 |     }
 110 |   }
 111 | });
 112 | 
 113 | // Utility to recursively truncate strings in any data structure
 114 | function truncateStringsInData(data, maxLength, depth = 0, path = "") {
 115 |   // Add depth limit to prevent circular references
 116 |   if (depth > 100) {
 117 |     console.warn("Max depth exceeded at path:", path);
 118 |     return "[MAX_DEPTH_EXCEEDED]";
 119 |   }
 120 | 
 121 |   console.log(`Processing at path: ${path}, type:`, typeof data);
 122 | 
 123 |   if (typeof data === "string") {
 124 |     if (data.length > maxLength) {
 125 |       console.log(
 126 |         `Truncating string at path ${path} from ${data.length} to ${maxLength}`
 127 |       );
 128 |       return data.substring(0, maxLength) + "... (truncated)";
 129 |     }
 130 |     return data;
 131 |   }
 132 | 
 133 |   if (Array.isArray(data)) {
 134 |     console.log(`Processing array at path ${path} with length:`, data.length);
 135 |     return data.map((item, index) =>
 136 |       truncateStringsInData(item, maxLength, depth + 1, `${path}[${index}]`)
 137 |     );
 138 |   }
 139 | 
 140 |   if (typeof data === "object" && data !== null) {
 141 |     console.log(
 142 |       `Processing object at path ${path} with keys:`,
 143 |       Object.keys(data)
 144 |     );
 145 |     const result = {};
 146 |     for (const [key, value] of Object.entries(data)) {
 147 |       try {
 148 |         result[key] = truncateStringsInData(
 149 |           value,
 150 |           maxLength,
 151 |           depth + 1,
 152 |           path ? `${path}.${key}` : key
 153 |         );
 154 |       } catch (e) {
 155 |         console.error(`Error processing key ${key} at path ${path}:`, e);
 156 |         result[key] = "[ERROR_PROCESSING]";
 157 |       }
 158 |     }
 159 |     return result;
 160 |   }
 161 | 
 162 |   return data;
 163 | }
 164 | 
 165 | // Helper to calculate the size of an object
 166 | function calculateObjectSize(obj) {
 167 |   return JSON.stringify(obj).length;
 168 | }
 169 | 
 170 | // Helper to process array of objects with size limit
 171 | function processArrayWithSizeLimit(array, maxTotalSize, processFunc) {
 172 |   let currentSize = 0;
 173 |   const result = [];
 174 | 
 175 |   for (const item of array) {
 176 |     // Process the item first
 177 |     const processedItem = processFunc(item);
 178 |     const itemSize = calculateObjectSize(processedItem);
 179 | 
 180 |     // Check if adding this item would exceed the limit
 181 |     if (currentSize + itemSize > maxTotalSize) {
 182 |       console.log(
 183 |         `Reached size limit (${currentSize}/${maxTotalSize}), truncating array`
 184 |       );
 185 |       break;
 186 |     }
 187 | 
 188 |     // Add item and update size
 189 |     result.push(processedItem);
 190 |     currentSize += itemSize;
 191 |     console.log(
 192 |       `Added item of size ${itemSize}, total size now: ${currentSize}`
 193 |     );
 194 |   }
 195 | 
 196 |   return result;
 197 | }
 198 | 
 199 | // Modified processJsonString to handle arrays with size limit
 200 | function processJsonString(jsonString, maxLength) {
 201 |   console.log("Processing string of length:", jsonString?.length);
 202 |   try {
 203 |     let parsed;
 204 |     try {
 205 |       parsed = JSON.parse(jsonString);
 206 |       console.log(
 207 |         "Successfully parsed as JSON, structure:",
 208 |         JSON.stringify(Object.keys(parsed))
 209 |       );
 210 |     } catch (e) {
 211 |       console.log("Not valid JSON, treating as string");
 212 |       return truncateStringsInData(jsonString, maxLength, 0, "root");
 213 |     }
 214 | 
 215 |     // If it's an array, process with size limit
 216 |     if (Array.isArray(parsed)) {
 217 |       console.log("Processing array of objects with size limit");
 218 |       const processed = processArrayWithSizeLimit(
 219 |         parsed,
 220 |         settings.maxLogSize,
 221 |         (item) => truncateStringsInData(item, maxLength, 0, "root")
 222 |       );
 223 |       const result = JSON.stringify(processed);
 224 |       console.log(
 225 |         `Processed array: ${parsed.length} -> ${processed.length} items`
 226 |       );
 227 |       return result;
 228 |     }
 229 | 
 230 |     // Otherwise process as before
 231 |     const processed = truncateStringsInData(parsed, maxLength, 0, "root");
 232 |     const result = JSON.stringify(processed);
 233 |     console.log("Processed JSON string length:", result.length);
 234 |     return result;
 235 |   } catch (e) {
 236 |     console.error("Error in processJsonString:", e);
 237 |     return jsonString.substring(0, maxLength) + "... (truncated)";
 238 |   }
 239 | }
 240 | 
 241 | // Helper to send logs to browser-connector
 242 | async function sendToBrowserConnector(logData) {
 243 |   if (!logData) {
 244 |     console.error("No log data provided to sendToBrowserConnector");
 245 |     return;
 246 |   }
 247 | 
 248 |   // First, ensure we're connecting to the right server
 249 |   if (!(await validateServerIdentity())) {
 250 |     console.error(
 251 |       "Cannot send logs: Not connected to a valid browser tools server"
 252 |     );
 253 |     return;
 254 |   }
 255 | 
 256 |   console.log("Sending log data to browser connector:", {
 257 |     type: logData.type,
 258 |     timestamp: logData.timestamp,
 259 |   });
 260 | 
 261 |   // Process any string fields that might contain JSON
 262 |   const processedData = { ...logData };
 263 | 
 264 |   if (logData.type === "network-request") {
 265 |     console.log("Processing network request");
 266 |     if (processedData.requestBody) {
 267 |       console.log(
 268 |         "Request body size before:",
 269 |         processedData.requestBody.length
 270 |       );
 271 |       processedData.requestBody = processJsonString(
 272 |         processedData.requestBody,
 273 |         settings.stringSizeLimit
 274 |       );
 275 |       console.log("Request body size after:", processedData.requestBody.length);
 276 |     }
 277 |     if (processedData.responseBody) {
 278 |       console.log(
 279 |         "Response body size before:",
 280 |         processedData.responseBody.length
 281 |       );
 282 |       processedData.responseBody = processJsonString(
 283 |         processedData.responseBody,
 284 |         settings.stringSizeLimit
 285 |       );
 286 |       console.log(
 287 |         "Response body size after:",
 288 |         processedData.responseBody.length
 289 |       );
 290 |     }
 291 |   } else if (
 292 |     logData.type === "console-log" ||
 293 |     logData.type === "console-error"
 294 |   ) {
 295 |     console.log("Processing console message");
 296 |     if (processedData.message) {
 297 |       console.log("Message size before:", processedData.message.length);
 298 |       processedData.message = processJsonString(
 299 |         processedData.message,
 300 |         settings.stringSizeLimit
 301 |       );
 302 |       console.log("Message size after:", processedData.message.length);
 303 |     }
 304 |   }
 305 | 
 306 |   // Add settings to the request
 307 |   const payload = {
 308 |     data: {
 309 |       ...processedData,
 310 |       timestamp: Date.now(),
 311 |     },
 312 |     settings: {
 313 |       logLimit: settings.logLimit,
 314 |       queryLimit: settings.queryLimit,
 315 |       showRequestHeaders: settings.showRequestHeaders,
 316 |       showResponseHeaders: settings.showResponseHeaders,
 317 |     },
 318 |   };
 319 | 
 320 |   const finalPayloadSize = JSON.stringify(payload).length;
 321 |   console.log("Final payload size:", finalPayloadSize);
 322 | 
 323 |   if (finalPayloadSize > 1000000) {
 324 |     console.warn("Warning: Large payload detected:", finalPayloadSize);
 325 |     console.warn(
 326 |       "Payload preview:",
 327 |       JSON.stringify(payload).substring(0, 1000) + "..."
 328 |     );
 329 |   }
 330 | 
 331 |   const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/extension-log`;
 332 |   console.log(`Sending log to ${serverUrl}`);
 333 | 
 334 |   fetch(serverUrl, {
 335 |     method: "POST",
 336 |     headers: { "Content-Type": "application/json" },
 337 |     body: JSON.stringify(payload),
 338 |   })
 339 |     .then((response) => {
 340 |       if (!response.ok) {
 341 |         throw new Error(`HTTP error ${response.status}`);
 342 |       }
 343 |       return response.json();
 344 |     })
 345 |     .then((data) => {
 346 |       console.log("Log sent successfully:", data);
 347 |     })
 348 |     .catch((error) => {
 349 |       console.error("Error sending log:", error);
 350 |     });
 351 | }
 352 | 
 353 | // Validate server identity
 354 | async function validateServerIdentity() {
 355 |   try {
 356 |     console.log(
 357 |       `Validating server identity at ${settings.serverHost}:${settings.serverPort}...`
 358 |     );
 359 | 
 360 |     // Use fetch with a timeout to prevent long-hanging requests
 361 |     const response = await fetch(
 362 |       `http://${settings.serverHost}:${settings.serverPort}/.identity`,
 363 |       {
 364 |         signal: AbortSignal.timeout(3000), // 3 second timeout
 365 |       }
 366 |     );
 367 | 
 368 |     if (!response.ok) {
 369 |       console.error(
 370 |         `Server identity validation failed: HTTP ${response.status}`
 371 |       );
 372 | 
 373 |       // Notify about the connection failure
 374 |       chrome.runtime.sendMessage({
 375 |         type: "SERVER_VALIDATION_FAILED",
 376 |         reason: "http_error",
 377 |         status: response.status,
 378 |         serverHost: settings.serverHost,
 379 |         serverPort: settings.serverPort,
 380 |       });
 381 | 
 382 |       return false;
 383 |     }
 384 | 
 385 |     const identity = await response.json();
 386 | 
 387 |     // Validate signature
 388 |     if (identity.signature !== "mcp-browser-connector-24x7") {
 389 |       console.error("Server identity validation failed: Invalid signature");
 390 | 
 391 |       // Notify about the invalid signature
 392 |       chrome.runtime.sendMessage({
 393 |         type: "SERVER_VALIDATION_FAILED",
 394 |         reason: "invalid_signature",
 395 |         serverHost: settings.serverHost,
 396 |         serverPort: settings.serverPort,
 397 |       });
 398 | 
 399 |       return false;
 400 |     }
 401 | 
 402 |     console.log(
 403 |       `Server identity confirmed: ${identity.name} v${identity.version}`
 404 |     );
 405 | 
 406 |     // Notify about successful validation
 407 |     chrome.runtime.sendMessage({
 408 |       type: "SERVER_VALIDATION_SUCCESS",
 409 |       serverInfo: identity,
 410 |       serverHost: settings.serverHost,
 411 |       serverPort: settings.serverPort,
 412 |     });
 413 | 
 414 |     return true;
 415 |   } catch (error) {
 416 |     console.error("Server identity validation failed:", error);
 417 | 
 418 |     // Notify about the connection error
 419 |     chrome.runtime.sendMessage({
 420 |       type: "SERVER_VALIDATION_FAILED",
 421 |       reason: "connection_error",
 422 |       error: error.message,
 423 |       serverHost: settings.serverHost,
 424 |       serverPort: settings.serverPort,
 425 |     });
 426 | 
 427 |     return false;
 428 |   }
 429 | }
 430 | 
 431 | // Function to clear logs on the server
 432 | function wipeLogs() {
 433 |   console.log("Wiping all logs...");
 434 | 
 435 |   const serverUrl = `http://${settings.serverHost}:${settings.serverPort}/wipelogs`;
 436 |   console.log(`Sending wipe request to ${serverUrl}`);
 437 | 
 438 |   fetch(serverUrl, {
 439 |     method: "POST",
 440 |     headers: { "Content-Type": "application/json" },
 441 |   })
 442 |     .then((response) => {
 443 |       if (!response.ok) {
 444 |         throw new Error(`HTTP error ${response.status}`);
 445 |       }
 446 |       return response.json();
 447 |     })
 448 |     .then((data) => {
 449 |       console.log("Logs wiped successfully:", data);
 450 |     })
 451 |     .catch((error) => {
 452 |       console.error("Error wiping logs:", error);
 453 |     });
 454 | }
 455 | 
 456 | // Listen for page refreshes
 457 | chrome.devtools.network.onNavigated.addListener((url) => {
 458 |   console.log("Page navigated/refreshed - wiping logs");
 459 |   wipeLogs();
 460 | 
 461 |   // Send the new URL to the server
 462 |   if (ws && ws.readyState === WebSocket.OPEN && url) {
 463 |     console.log(
 464 |       "Chrome Extension: Sending page-navigated event with URL:",
 465 |       url
 466 |     );
 467 |     ws.send(
 468 |       JSON.stringify({
 469 |         type: "page-navigated",
 470 |         url: url,
 471 |         tabId: chrome.devtools.inspectedWindow.tabId,
 472 |         timestamp: Date.now(),
 473 |       })
 474 |     );
 475 |   }
 476 | });
 477 | 
 478 | // 1) Listen for network requests
 479 | chrome.devtools.network.onRequestFinished.addListener((request) => {
 480 |   if (request._resourceType === "xhr" || request._resourceType === "fetch") {
 481 |     request.getContent((responseBody) => {
 482 |       const entry = {
 483 |         type: "network-request",
 484 |         url: request.request.url,
 485 |         method: request.request.method,
 486 |         status: request.response.status,
 487 |         requestHeaders: request.request.headers,
 488 |         responseHeaders: request.response.headers,
 489 |         requestBody: request.request.postData?.text ?? "",
 490 |         responseBody: responseBody ?? "",
 491 |       };
 492 |       sendToBrowserConnector(entry);
 493 |     });
 494 |   }
 495 | });
 496 | 
 497 | // Helper function to attach debugger
 498 | async function attachDebugger() {
 499 |   // First check if we're already attached to this tab
 500 |   chrome.debugger.getTargets((targets) => {
 501 |     const isAlreadyAttached = targets.some(
 502 |       (target) => target.tabId === currentTabId && target.attached
 503 |     );
 504 | 
 505 |     if (isAlreadyAttached) {
 506 |       console.log("Found existing debugger attachment, detaching first...");
 507 |       // Force detach first to ensure clean state
 508 |       chrome.debugger.detach({ tabId: currentTabId }, () => {
 509 |         // Ignore any errors during detach
 510 |         if (chrome.runtime.lastError) {
 511 |           console.log("Error during forced detach:", chrome.runtime.lastError);
 512 |         }
 513 |         // Now proceed with fresh attachment
 514 |         performAttach();
 515 |       });
 516 |     } else {
 517 |       // No existing attachment, proceed directly
 518 |       performAttach();
 519 |     }
 520 |   });
 521 | }
 522 | 
 523 | function performAttach() {
 524 |   console.log("Performing debugger attachment to tab:", currentTabId);
 525 |   chrome.debugger.attach({ tabId: currentTabId }, "1.3", () => {
 526 |     if (chrome.runtime.lastError) {
 527 |       console.error("Failed to attach debugger:", chrome.runtime.lastError);
 528 |       isDebuggerAttached = false;
 529 |       return;
 530 |     }
 531 | 
 532 |     isDebuggerAttached = true;
 533 |     console.log("Debugger successfully attached");
 534 | 
 535 |     // Add the event listener when attaching
 536 |     chrome.debugger.onEvent.addListener(consoleMessageListener);
 537 | 
 538 |     chrome.debugger.sendCommand(
 539 |       { tabId: currentTabId },
 540 |       "Runtime.enable",
 541 |       {},
 542 |       () => {
 543 |         if (chrome.runtime.lastError) {
 544 |           console.error("Failed to enable runtime:", chrome.runtime.lastError);
 545 |           return;
 546 |         }
 547 |         console.log("Runtime API successfully enabled");
 548 |       }
 549 |     );
 550 |   });
 551 | }
 552 | 
 553 | // Helper function to detach debugger
 554 | function detachDebugger() {
 555 |   // Remove the event listener first
 556 |   chrome.debugger.onEvent.removeListener(consoleMessageListener);
 557 | 
 558 |   // Check if debugger is actually attached before trying to detach
 559 |   chrome.debugger.getTargets((targets) => {
 560 |     const isStillAttached = targets.some(
 561 |       (target) => target.tabId === currentTabId && target.attached
 562 |     );
 563 | 
 564 |     if (!isStillAttached) {
 565 |       console.log("Debugger already detached");
 566 |       isDebuggerAttached = false;
 567 |       return;
 568 |     }
 569 | 
 570 |     chrome.debugger.detach({ tabId: currentTabId }, () => {
 571 |       if (chrome.runtime.lastError) {
 572 |         console.warn(
 573 |           "Warning during debugger detach:",
 574 |           chrome.runtime.lastError
 575 |         );
 576 |       }
 577 |       isDebuggerAttached = false;
 578 |       console.log("Debugger detached");
 579 |     });
 580 |   });
 581 | }
 582 | 
 583 | // Move the console message listener outside the panel creation
 584 | const consoleMessageListener = (source, method, params) => {
 585 |   // Only process events for our tab
 586 |   if (source.tabId !== currentTabId) {
 587 |     return;
 588 |   }
 589 | 
 590 |   if (method === "Runtime.exceptionThrown") {
 591 |     const entry = {
 592 |       type: "console-error",
 593 |       message:
 594 |         params.exceptionDetails.exception?.description ||
 595 |         JSON.stringify(params.exceptionDetails),
 596 |       level: "error",
 597 |       timestamp: Date.now(),
 598 |     };
 599 |     console.log("Sending runtime exception:", entry);
 600 |     sendToBrowserConnector(entry);
 601 |   }
 602 | 
 603 |   if (method === "Runtime.consoleAPICalled") {
 604 |     // Process all arguments from the console call
 605 |     let formattedMessage = "";
 606 |     const args = params.args || [];
 607 | 
 608 |     // Extract all arguments and combine them
 609 |     if (args.length > 0) {
 610 |       // Try to build a meaningful representation of all arguments
 611 |       try {
 612 |         formattedMessage = args
 613 |           .map((arg) => {
 614 |             // Handle different types of arguments
 615 |             if (arg.type === "string") {
 616 |               return arg.value;
 617 |             } else if (arg.type === "object" && arg.preview) {
 618 |               // For objects, include their preview or description
 619 |               return JSON.stringify(arg.preview);
 620 |             } else if (arg.description) {
 621 |               // Some objects have descriptions
 622 |               return arg.description;
 623 |             } else {
 624 |               // Fallback for other types
 625 |               return arg.value || arg.description || JSON.stringify(arg);
 626 |             }
 627 |           })
 628 |           .join(" ");
 629 |       } catch (e) {
 630 |         // Fallback if processing fails
 631 |         console.error("Failed to process console arguments:", e);
 632 |         formattedMessage =
 633 |           args[0]?.value || "Unable to process console arguments";
 634 |       }
 635 |     }
 636 | 
 637 |     const entry = {
 638 |       type: params.type === "error" ? "console-error" : "console-log",
 639 |       level: params.type,
 640 |       message: formattedMessage,
 641 |       timestamp: Date.now(),
 642 |     };
 643 |     console.log("Sending console entry:", entry);
 644 |     sendToBrowserConnector(entry);
 645 |   }
 646 | };
 647 | 
 648 | // 2) Use DevTools Protocol to capture console logs
 649 | chrome.devtools.panels.create("BrowserToolsMCP", "", "panel.html", (panel) => {
 650 |   // Initial attach - we'll keep the debugger attached as long as DevTools is open
 651 |   attachDebugger();
 652 | 
 653 |   // Handle panel showing
 654 |   panel.onShown.addListener((panelWindow) => {
 655 |     if (!isDebuggerAttached) {
 656 |       attachDebugger();
 657 |     }
 658 |   });
 659 | });
 660 | 
 661 | // Clean up when DevTools closes
 662 | window.addEventListener("unload", () => {
 663 |   // Detach debugger
 664 |   detachDebugger();
 665 | 
 666 |   // Set intentional closure flag before closing
 667 |   intentionalClosure = true;
 668 | 
 669 |   if (ws) {
 670 |     try {
 671 |       ws.close();
 672 |     } catch (e) {
 673 |       console.error("Error closing WebSocket during unload:", e);
 674 |     }
 675 |     ws = null;
 676 |   }
 677 | 
 678 |   if (wsReconnectTimeout) {
 679 |     clearTimeout(wsReconnectTimeout);
 680 |     wsReconnectTimeout = null;
 681 |   }
 682 | 
 683 |   if (heartbeatInterval) {
 684 |     clearInterval(heartbeatInterval);
 685 |     heartbeatInterval = null;
 686 |   }
 687 | });
 688 | 
 689 | // Function to capture and send element data
 690 | function captureAndSendElement() {
 691 |   chrome.devtools.inspectedWindow.eval(
 692 |     `(function() {
 693 |       const el = $0;  // $0 is the currently selected element in DevTools
 694 |       if (!el) return null;
 695 | 
 696 |       const rect = el.getBoundingClientRect();
 697 | 
 698 |       return {
 699 |         tagName: el.tagName,
 700 |         id: el.id,
 701 |         className: el.className,
 702 |         textContent: el.textContent?.substring(0, 100),
 703 |         attributes: Array.from(el.attributes).map(attr => ({
 704 |           name: attr.name,
 705 |           value: attr.value
 706 |         })),
 707 |         dimensions: {
 708 |           width: rect.width,
 709 |           height: rect.height,
 710 |           top: rect.top,
 711 |           left: rect.left
 712 |         },
 713 |         innerHTML: el.innerHTML.substring(0, 500)
 714 |       };
 715 |     })()`,
 716 |     (result, isException) => {
 717 |       if (isException || !result) return;
 718 | 
 719 |       console.log("Element selected:", result);
 720 | 
 721 |       // Send to browser connector
 722 |       sendToBrowserConnector({
 723 |         type: "selected-element",
 724 |         timestamp: Date.now(),
 725 |         element: result,
 726 |       });
 727 |     }
 728 |   );
 729 | }
 730 | 
 731 | // Listen for element selection in the Elements panel
 732 | chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
 733 |   captureAndSendElement();
 734 | });
 735 | 
 736 | // WebSocket connection management
 737 | let ws = null;
 738 | let wsReconnectTimeout = null;
 739 | let heartbeatInterval = null;
 740 | const WS_RECONNECT_DELAY = 5000; // 5 seconds
 741 | const HEARTBEAT_INTERVAL = 30000; // 30 seconds
 742 | // Add a flag to track if we need to reconnect after identity validation
 743 | let reconnectAfterValidation = false;
 744 | // Track if we're intentionally closing the connection
 745 | let intentionalClosure = false;
 746 | 
 747 | // Function to send a heartbeat to keep the WebSocket connection alive
 748 | function sendHeartbeat() {
 749 |   if (ws && ws.readyState === WebSocket.OPEN) {
 750 |     console.log("Chrome Extension: Sending WebSocket heartbeat");
 751 |     ws.send(JSON.stringify({ type: "heartbeat" }));
 752 |   }
 753 | }
 754 | 
 755 | async function setupWebSocket() {
 756 |   // Clear any pending timeouts
 757 |   if (wsReconnectTimeout) {
 758 |     clearTimeout(wsReconnectTimeout);
 759 |     wsReconnectTimeout = null;
 760 |   }
 761 | 
 762 |   if (heartbeatInterval) {
 763 |     clearInterval(heartbeatInterval);
 764 |     heartbeatInterval = null;
 765 |   }
 766 | 
 767 |   // Close existing WebSocket if any
 768 |   if (ws) {
 769 |     // Set flag to indicate this is an intentional closure
 770 |     intentionalClosure = true;
 771 |     try {
 772 |       ws.close();
 773 |     } catch (e) {
 774 |       console.error("Error closing existing WebSocket:", e);
 775 |     }
 776 |     ws = null;
 777 |     intentionalClosure = false; // Reset flag
 778 |   }
 779 | 
 780 |   // Validate server identity before connecting
 781 |   console.log("Validating server identity before WebSocket connection...");
 782 |   const isValid = await validateServerIdentity();
 783 | 
 784 |   if (!isValid) {
 785 |     console.error(
 786 |       "Cannot establish WebSocket: Not connected to a valid browser tools server"
 787 |     );
 788 |     // Set flag to indicate we need to reconnect after a page refresh check
 789 |     reconnectAfterValidation = true;
 790 | 
 791 |     // Try again after delay
 792 |     wsReconnectTimeout = setTimeout(() => {
 793 |       console.log("Attempting to reconnect WebSocket after validation failure");
 794 |       setupWebSocket();
 795 |     }, WS_RECONNECT_DELAY);
 796 |     return;
 797 |   }
 798 | 
 799 |   // Reset reconnect flag since validation succeeded
 800 |   reconnectAfterValidation = false;
 801 | 
 802 |   const wsUrl = `ws://${settings.serverHost}:${settings.serverPort}/extension-ws`;
 803 |   console.log(`Connecting to WebSocket at ${wsUrl}`);
 804 | 
 805 |   try {
 806 |     ws = new WebSocket(wsUrl);
 807 | 
 808 |     ws.onopen = () => {
 809 |       console.log(`Chrome Extension: WebSocket connected to ${wsUrl}`);
 810 | 
 811 |       // Start heartbeat to keep connection alive
 812 |       heartbeatInterval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
 813 | 
 814 |       // Notify that connection is successful
 815 |       chrome.runtime.sendMessage({
 816 |         type: "WEBSOCKET_CONNECTED",
 817 |         serverHost: settings.serverHost,
 818 |         serverPort: settings.serverPort,
 819 |       });
 820 | 
 821 |       // Send the current URL to the server right after connection
 822 |       // This ensures the server has the URL even if no navigation occurs
 823 |       chrome.runtime.sendMessage(
 824 |         {
 825 |           type: "GET_CURRENT_URL",
 826 |           tabId: chrome.devtools.inspectedWindow.tabId,
 827 |         },
 828 |         (response) => {
 829 |           if (chrome.runtime.lastError) {
 830 |             console.error(
 831 |               "Chrome Extension: Error getting URL from background on connection:",
 832 |               chrome.runtime.lastError
 833 |             );
 834 | 
 835 |             // If normal method fails, try fallback to chrome.tabs API directly
 836 |             tryFallbackGetUrl();
 837 |             return;
 838 |           }
 839 | 
 840 |           if (response && response.url) {
 841 |             console.log(
 842 |               "Chrome Extension: Sending initial URL to server:",
 843 |               response.url
 844 |             );
 845 | 
 846 |             // Send the URL to the server via the background script
 847 |             chrome.runtime.sendMessage({
 848 |               type: "UPDATE_SERVER_URL",
 849 |               tabId: chrome.devtools.inspectedWindow.tabId,
 850 |               url: response.url,
 851 |               source: "initial_connection",
 852 |             });
 853 |           } else {
 854 |             // If response exists but no URL, try fallback
 855 |             tryFallbackGetUrl();
 856 |           }
 857 |         }
 858 |       );
 859 | 
 860 |       // Fallback method to get URL directly
 861 |       function tryFallbackGetUrl() {
 862 |         console.log("Chrome Extension: Trying fallback method to get URL");
 863 | 
 864 |         // Try to get the URL directly using the tabs API
 865 |         chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
 866 |           if (chrome.runtime.lastError) {
 867 |             console.error(
 868 |               "Chrome Extension: Fallback URL retrieval failed:",
 869 |               chrome.runtime.lastError
 870 |             );
 871 |             return;
 872 |           }
 873 | 
 874 |           if (tabs && tabs.length > 0 && tabs[0].url) {
 875 |             console.log(
 876 |               "Chrome Extension: Got URL via fallback method:",
 877 |               tabs[0].url
 878 |             );
 879 | 
 880 |             // Send the URL to the server
 881 |             chrome.runtime.sendMessage({
 882 |               type: "UPDATE_SERVER_URL",
 883 |               tabId: chrome.devtools.inspectedWindow.tabId,
 884 |               url: tabs[0].url,
 885 |               source: "fallback_method",
 886 |             });
 887 |           } else {
 888 |             console.warn(
 889 |               "Chrome Extension: Could not retrieve URL through fallback method"
 890 |             );
 891 |           }
 892 |         });
 893 |       }
 894 |     };
 895 | 
 896 |     ws.onerror = (error) => {
 897 |       console.error(`Chrome Extension: WebSocket error for ${wsUrl}:`, error);
 898 |     };
 899 | 
 900 |     ws.onclose = (event) => {
 901 |       console.log(`Chrome Extension: WebSocket closed for ${wsUrl}:`, event);
 902 | 
 903 |       // Stop heartbeat
 904 |       if (heartbeatInterval) {
 905 |         clearInterval(heartbeatInterval);
 906 |         heartbeatInterval = null;
 907 |       }
 908 | 
 909 |       // Don't reconnect if this was an intentional closure
 910 |       if (intentionalClosure) {
 911 |         console.log(
 912 |           "Chrome Extension: Intentional WebSocket closure, not reconnecting"
 913 |         );
 914 |         return;
 915 |       }
 916 | 
 917 |       // Only attempt to reconnect if the closure wasn't intentional
 918 |       // Code 1000 (Normal Closure) and 1001 (Going Away) are normal closures
 919 |       // Code 1005 often happens with clean closures in Chrome
 920 |       const isAbnormalClosure = !(event.code === 1000 || event.code === 1001);
 921 | 
 922 |       // Check if this was an abnormal closure or if we need to reconnect after validation
 923 |       if (isAbnormalClosure || reconnectAfterValidation) {
 924 |         console.log(
 925 |           `Chrome Extension: Will attempt to reconnect WebSocket (closure code: ${event.code})`
 926 |         );
 927 | 
 928 |         // Try to reconnect after delay
 929 |         wsReconnectTimeout = setTimeout(() => {
 930 |           console.log(
 931 |             `Chrome Extension: Attempting to reconnect WebSocket to ${wsUrl}`
 932 |           );
 933 |           setupWebSocket();
 934 |         }, WS_RECONNECT_DELAY);
 935 |       } else {
 936 |         console.log(
 937 |           `Chrome Extension: Normal WebSocket closure, not reconnecting automatically`
 938 |         );
 939 |       }
 940 |     };
 941 | 
 942 |     ws.onmessage = async (event) => {
 943 |       try {
 944 |         const message = JSON.parse(event.data);
 945 | 
 946 |         // Don't log heartbeat responses to reduce noise
 947 |         if (message.type !== "heartbeat-response") {
 948 |           console.log("Chrome Extension: Received WebSocket message:", message);
 949 | 
 950 |           if (message.type === "server-shutdown") {
 951 |             console.log("Chrome Extension: Received server shutdown signal");
 952 |             // Clear any reconnection attempts
 953 |             if (wsReconnectTimeout) {
 954 |               clearTimeout(wsReconnectTimeout);
 955 |               wsReconnectTimeout = null;
 956 |             }
 957 |             // Close the connection gracefully
 958 |             ws.close(1000, "Server shutting down");
 959 |             return;
 960 |           }
 961 |         }
 962 | 
 963 |         if (message.type === "heartbeat-response") {
 964 |           // Just a heartbeat response, no action needed
 965 |           // Uncomment the next line for debug purposes only
 966 |           // console.log("Chrome Extension: Received heartbeat response");
 967 |         } else if (message.type === "take-screenshot") {
 968 |           console.log("Chrome Extension: Taking screenshot...");
 969 |           // Capture screenshot of the current tab
 970 |           chrome.tabs.captureVisibleTab(null, { format: "png" }, (dataUrl) => {
 971 |             if (chrome.runtime.lastError) {
 972 |               console.error(
 973 |                 "Chrome Extension: Screenshot capture failed:",
 974 |                 chrome.runtime.lastError
 975 |               );
 976 |               ws.send(
 977 |                 JSON.stringify({
 978 |                   type: "screenshot-error",
 979 |                   error: chrome.runtime.lastError.message,
 980 |                   requestId: message.requestId,
 981 |                 })
 982 |               );
 983 |               return;
 984 |             }
 985 | 
 986 |             console.log("Chrome Extension: Screenshot captured successfully");
 987 |             // Just send the screenshot data, let the server handle paths
 988 |             const response = {
 989 |               type: "screenshot-data",
 990 |               data: dataUrl,
 991 |               requestId: message.requestId,
 992 |               // Only include path if it's configured in settings
 993 |               ...(settings.screenshotPath && { path: settings.screenshotPath }),
 994 |               // Include auto-paste setting
 995 |               autoPaste: settings.allowAutoPaste,
 996 |             };
 997 | 
 998 |             console.log("Chrome Extension: Sending screenshot data response", {
 999 |               ...response,
1000 |               data: "[base64 data]",
1001 |             });
1002 | 
1003 |             ws.send(JSON.stringify(response));
1004 |           });
1005 |         } else if (message.type === "get-current-url") {
1006 |           console.log("Chrome Extension: Received request for current URL");
1007 | 
1008 |           // Get the current URL from the background script instead of inspectedWindow.eval
1009 |           let retryCount = 0;
1010 |           const maxRetries = 2;
1011 | 
1012 |           const requestCurrentUrl = () => {
1013 |             chrome.runtime.sendMessage(
1014 |               {
1015 |                 type: "GET_CURRENT_URL",
1016 |                 tabId: chrome.devtools.inspectedWindow.tabId,
1017 |               },
1018 |               (response) => {
1019 |                 if (chrome.runtime.lastError) {
1020 |                   console.error(
1021 |                     "Chrome Extension: Error getting URL from background:",
1022 |                     chrome.runtime.lastError
1023 |                   );
1024 | 
1025 |                   // Retry logic
1026 |                   if (retryCount < maxRetries) {
1027 |                     retryCount++;
1028 |                     console.log(
1029 |                       `Retrying URL request (${retryCount}/${maxRetries})...`
1030 |                     );
1031 |                     setTimeout(requestCurrentUrl, 500); // Wait 500ms before retrying
1032 |                     return;
1033 |                   }
1034 | 
1035 |                   ws.send(
1036 |                     JSON.stringify({
1037 |                       type: "current-url-response",
1038 |                       url: null,
1039 |                       tabId: chrome.devtools.inspectedWindow.tabId,
1040 |                       error:
1041 |                         "Failed to get URL from background: " +
1042 |                         chrome.runtime.lastError.message,
1043 |                       requestId: message.requestId,
1044 |                     })
1045 |                   );
1046 |                   return;
1047 |                 }
1048 | 
1049 |                 if (response && response.success && response.url) {
1050 |                   console.log(
1051 |                     "Chrome Extension: Got URL from background:",
1052 |                     response.url
1053 |                   );
1054 |                   ws.send(
1055 |                     JSON.stringify({
1056 |                       type: "current-url-response",
1057 |                       url: response.url,
1058 |                       tabId: chrome.devtools.inspectedWindow.tabId,
1059 |                       requestId: message.requestId,
1060 |                     })
1061 |                   );
1062 |                 } else {
1063 |                   console.error(
1064 |                     "Chrome Extension: Invalid URL response from background:",
1065 |                     response
1066 |                   );
1067 | 
1068 |                   // Last resort - try to get URL directly from the tab
1069 |                   chrome.tabs.query(
1070 |                     { active: true, currentWindow: true },
1071 |                     (tabs) => {
1072 |                       const url = tabs && tabs[0] && tabs[0].url;
1073 |                       console.log(
1074 |                         "Chrome Extension: Got URL directly from tab:",
1075 |                         url
1076 |                       );
1077 | 
1078 |                       ws.send(
1079 |                         JSON.stringify({
1080 |                           type: "current-url-response",
1081 |                           url: url || null,
1082 |                           tabId: chrome.devtools.inspectedWindow.tabId,
1083 |                           error:
1084 |                             response?.error ||
1085 |                             "Failed to get URL from background",
1086 |                           requestId: message.requestId,
1087 |                         })
1088 |                       );
1089 |                     }
1090 |                   );
1091 |                 }
1092 |               }
1093 |             );
1094 |           };
1095 | 
1096 |           requestCurrentUrl();
1097 |         }
1098 |       } catch (error) {
1099 |         console.error(
1100 |           "Chrome Extension: Error processing WebSocket message:",
1101 |           error
1102 |         );
1103 |       }
1104 |     };
1105 |   } catch (error) {
1106 |     console.error("Error creating WebSocket:", error);
1107 |     // Try again after delay
1108 |     wsReconnectTimeout = setTimeout(setupWebSocket, WS_RECONNECT_DELAY);
1109 |   }
1110 | }
1111 | 
1112 | // Initialize WebSocket connection when DevTools opens
1113 | setupWebSocket();
1114 | 
1115 | // Clean up WebSocket when DevTools closes
1116 | window.addEventListener("unload", () => {
1117 |   if (ws) {
1118 |     ws.close();
1119 |   }
1120 |   if (wsReconnectTimeout) {
1121 |     clearTimeout(wsReconnectTimeout);
1122 |   }
1123 | });
1124 | 
```

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

```typescript
   1 | #!/usr/bin/env node
   2 | 
   3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
   4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
   5 | import path from "path";
   6 | import fs from "fs";
   7 | 
   8 | // Create the MCP server
   9 | const server = new McpServer({
  10 |   name: "Browser Tools MCP",
  11 |   version: "1.2.0",
  12 | });
  13 | 
  14 | // Track the discovered server connection
  15 | let discoveredHost = "127.0.0.1";
  16 | let discoveredPort = 3025;
  17 | let serverDiscovered = false;
  18 | 
  19 | // Function to get the default port from environment variable or default
  20 | function getDefaultServerPort(): number {
  21 |   // Check environment variable first
  22 |   if (process.env.BROWSER_TOOLS_PORT) {
  23 |     const envPort = parseInt(process.env.BROWSER_TOOLS_PORT, 10);
  24 |     if (!isNaN(envPort) && envPort > 0) {
  25 |       return envPort;
  26 |     }
  27 |   }
  28 | 
  29 |   // Try to read from .port file
  30 |   try {
  31 |     const portFilePath = path.join(__dirname, ".port");
  32 |     if (fs.existsSync(portFilePath)) {
  33 |       const port = parseInt(fs.readFileSync(portFilePath, "utf8").trim(), 10);
  34 |       if (!isNaN(port) && port > 0) {
  35 |         return port;
  36 |       }
  37 |     }
  38 |   } catch (err) {
  39 |     console.error("Error reading port file:", err);
  40 |   }
  41 | 
  42 |   // Default port if no configuration found
  43 |   return 3025;
  44 | }
  45 | 
  46 | // Function to get default server host from environment variable or default
  47 | function getDefaultServerHost(): string {
  48 |   // Check environment variable first
  49 |   if (process.env.BROWSER_TOOLS_HOST) {
  50 |     return process.env.BROWSER_TOOLS_HOST;
  51 |   }
  52 | 
  53 |   // Default to localhost
  54 |   return "127.0.0.1";
  55 | }
  56 | 
  57 | // Server discovery function - similar to what you have in the Chrome extension
  58 | async function discoverServer(): Promise<boolean> {
  59 |   console.log("Starting server discovery process");
  60 | 
  61 |   // Common hosts to try
  62 |   const hosts = [getDefaultServerHost(), "127.0.0.1", "localhost"];
  63 | 
  64 |   // Ports to try (start with default, then try others)
  65 |   const defaultPort = getDefaultServerPort();
  66 |   const ports = [defaultPort];
  67 | 
  68 |   // Add additional ports (fallback range)
  69 |   for (let p = 3025; p <= 3035; p++) {
  70 |     if (p !== defaultPort) {
  71 |       ports.push(p);
  72 |     }
  73 |   }
  74 | 
  75 |   console.log(`Will try hosts: ${hosts.join(", ")}`);
  76 |   console.log(`Will try ports: ${ports.join(", ")}`);
  77 | 
  78 |   // Try to find the server
  79 |   for (const host of hosts) {
  80 |     for (const port of ports) {
  81 |       try {
  82 |         console.log(`Checking ${host}:${port}...`);
  83 | 
  84 |         // Use the identity endpoint for validation
  85 |         const response = await fetch(`http://${host}:${port}/.identity`, {
  86 |           signal: AbortSignal.timeout(1000), // 1 second timeout
  87 |         });
  88 | 
  89 |         if (response.ok) {
  90 |           const identity = await response.json();
  91 | 
  92 |           // Verify this is actually our server by checking the signature
  93 |           if (identity.signature === "mcp-browser-connector-24x7") {
  94 |             console.log(`Successfully found server at ${host}:${port}`);
  95 | 
  96 |             // Save the discovered connection
  97 |             discoveredHost = host;
  98 |             discoveredPort = port;
  99 |             serverDiscovered = true;
 100 | 
 101 |             return true;
 102 |           }
 103 |         }
 104 |       } catch (error: any) {
 105 |         // Ignore connection errors during discovery
 106 |         console.error(`Error checking ${host}:${port}: ${error.message}`);
 107 |       }
 108 |     }
 109 |   }
 110 | 
 111 |   console.error("No server found during discovery");
 112 |   return false;
 113 | }
 114 | 
 115 | // Wrapper function to ensure server connection before making requests
 116 | async function withServerConnection<T>(
 117 |   apiCall: () => Promise<T>
 118 | ): Promise<T | any> {
 119 |   // Attempt to discover server if not already discovered
 120 |   if (!serverDiscovered) {
 121 |     const discovered = await discoverServer();
 122 |     if (!discovered) {
 123 |       return {
 124 |         content: [
 125 |           {
 126 |             type: "text",
 127 |             text: "Failed to discover browser connector server. Please ensure it's running.",
 128 |           },
 129 |         ],
 130 |         isError: true,
 131 |       };
 132 |     }
 133 |   }
 134 | 
 135 |   // Now make the actual API call with discovered host/port
 136 |   try {
 137 |     return await apiCall();
 138 |   } catch (error: any) {
 139 |     // If the request fails, try rediscovering the server once
 140 |     console.error(
 141 |       `API call failed: ${error.message}. Attempting rediscovery...`
 142 |     );
 143 |     serverDiscovered = false;
 144 | 
 145 |     if (await discoverServer()) {
 146 |       console.error("Rediscovery successful. Retrying API call...");
 147 |       try {
 148 |         // Retry the API call with the newly discovered connection
 149 |         return await apiCall();
 150 |       } catch (retryError: any) {
 151 |         console.error(`Retry failed: ${retryError.message}`);
 152 |         return {
 153 |           content: [
 154 |             {
 155 |               type: "text",
 156 |               text: `Error after reconnection attempt: ${retryError.message}`,
 157 |             },
 158 |           ],
 159 |           isError: true,
 160 |         };
 161 |       }
 162 |     } else {
 163 |       console.error("Rediscovery failed. Could not reconnect to server.");
 164 |       return {
 165 |         content: [
 166 |           {
 167 |             type: "text",
 168 |             text: `Failed to reconnect to server: ${error.message}`,
 169 |           },
 170 |         ],
 171 |         isError: true,
 172 |       };
 173 |     }
 174 |   }
 175 | }
 176 | 
 177 | // We'll define our tools that retrieve data from the browser connector
 178 | server.tool("getConsoleLogs", "Check our browser logs", async () => {
 179 |   return await withServerConnection(async () => {
 180 |     const response = await fetch(
 181 |       `http://${discoveredHost}:${discoveredPort}/console-logs`
 182 |     );
 183 |     const json = await response.json();
 184 |     return {
 185 |       content: [
 186 |         {
 187 |           type: "text",
 188 |           text: JSON.stringify(json, null, 2),
 189 |         },
 190 |       ],
 191 |     };
 192 |   });
 193 | });
 194 | 
 195 | server.tool(
 196 |   "getConsoleErrors",
 197 |   "Check our browsers console errors",
 198 |   async () => {
 199 |     return await withServerConnection(async () => {
 200 |       const response = await fetch(
 201 |         `http://${discoveredHost}:${discoveredPort}/console-errors`
 202 |       );
 203 |       const json = await response.json();
 204 |       return {
 205 |         content: [
 206 |           {
 207 |             type: "text",
 208 |             text: JSON.stringify(json, null, 2),
 209 |           },
 210 |         ],
 211 |       };
 212 |     });
 213 |   }
 214 | );
 215 | 
 216 | server.tool("getNetworkErrors", "Check our network ERROR logs", async () => {
 217 |   return await withServerConnection(async () => {
 218 |     const response = await fetch(
 219 |       `http://${discoveredHost}:${discoveredPort}/network-errors`
 220 |     );
 221 |     const json = await response.json();
 222 |     return {
 223 |       content: [
 224 |         {
 225 |           type: "text",
 226 |           text: JSON.stringify(json, null, 2),
 227 |         },
 228 |       ],
 229 |       isError: true,
 230 |     };
 231 |   });
 232 | });
 233 | 
 234 | server.tool("getNetworkLogs", "Check ALL our network logs", async () => {
 235 |   return await withServerConnection(async () => {
 236 |     const response = await fetch(
 237 |       `http://${discoveredHost}:${discoveredPort}/network-success`
 238 |     );
 239 |     const json = await response.json();
 240 |     return {
 241 |       content: [
 242 |         {
 243 |           type: "text",
 244 |           text: JSON.stringify(json, null, 2),
 245 |         },
 246 |       ],
 247 |     };
 248 |   });
 249 | });
 250 | 
 251 | server.tool(
 252 |   "takeScreenshot",
 253 |   "Take a screenshot of the current browser tab",
 254 |   async () => {
 255 |     return await withServerConnection(async () => {
 256 |       try {
 257 |         const response = await fetch(
 258 |           `http://${discoveredHost}:${discoveredPort}/capture-screenshot`,
 259 |           {
 260 |             method: "POST",
 261 |           }
 262 |         );
 263 | 
 264 |         const result = await response.json();
 265 | 
 266 |         if (response.ok) {
 267 |           return {
 268 |             content: [
 269 |               {
 270 |                 type: "text",
 271 |                 text: "Successfully saved screenshot",
 272 |               },
 273 |             ],
 274 |           };
 275 |         } else {
 276 |           return {
 277 |             content: [
 278 |               {
 279 |                 type: "text",
 280 |                 text: `Error taking screenshot: ${result.error}`,
 281 |               },
 282 |             ],
 283 |           };
 284 |         }
 285 |       } catch (error: any) {
 286 |         const errorMessage =
 287 |           error instanceof Error ? error.message : String(error);
 288 |         return {
 289 |           content: [
 290 |             {
 291 |               type: "text",
 292 |               text: `Failed to take screenshot: ${errorMessage}`,
 293 |             },
 294 |           ],
 295 |         };
 296 |       }
 297 |     });
 298 |   }
 299 | );
 300 | 
 301 | server.tool(
 302 |   "getSelectedElement",
 303 |   "Get the selected element from the browser",
 304 |   async () => {
 305 |     return await withServerConnection(async () => {
 306 |       const response = await fetch(
 307 |         `http://${discoveredHost}:${discoveredPort}/selected-element`
 308 |       );
 309 |       const json = await response.json();
 310 |       return {
 311 |         content: [
 312 |           {
 313 |             type: "text",
 314 |             text: JSON.stringify(json, null, 2),
 315 |           },
 316 |         ],
 317 |       };
 318 |     });
 319 |   }
 320 | );
 321 | 
 322 | server.tool("wipeLogs", "Wipe all browser logs from memory", async () => {
 323 |   return await withServerConnection(async () => {
 324 |     const response = await fetch(
 325 |       `http://${discoveredHost}:${discoveredPort}/wipelogs`,
 326 |       {
 327 |         method: "POST",
 328 |       }
 329 |     );
 330 |     const json = await response.json();
 331 |     return {
 332 |       content: [
 333 |         {
 334 |           type: "text",
 335 |           text: json.message,
 336 |         },
 337 |       ],
 338 |     };
 339 |   });
 340 | });
 341 | 
 342 | // Define audit categories as enum to match the server's AuditCategory enum
 343 | enum AuditCategory {
 344 |   ACCESSIBILITY = "accessibility",
 345 |   PERFORMANCE = "performance",
 346 |   SEO = "seo",
 347 |   BEST_PRACTICES = "best-practices",
 348 |   PWA = "pwa",
 349 | }
 350 | 
 351 | // Add tool for accessibility audits, launches a headless browser instance
 352 | server.tool(
 353 |   "runAccessibilityAudit",
 354 |   "Run an accessibility audit on the current page",
 355 |   {},
 356 |   async () => {
 357 |     return await withServerConnection(async () => {
 358 |       try {
 359 |         // Simplified approach - let the browser connector handle the current tab and URL
 360 |         console.log(
 361 |           `Sending POST request to http://${discoveredHost}:${discoveredPort}/accessibility-audit`
 362 |         );
 363 |         const response = await fetch(
 364 |           `http://${discoveredHost}:${discoveredPort}/accessibility-audit`,
 365 |           {
 366 |             method: "POST",
 367 |             headers: {
 368 |               "Content-Type": "application/json",
 369 |               Accept: "application/json",
 370 |             },
 371 |             body: JSON.stringify({
 372 |               category: AuditCategory.ACCESSIBILITY,
 373 |               source: "mcp_tool",
 374 |               timestamp: Date.now(),
 375 |             }),
 376 |           }
 377 |         );
 378 | 
 379 |         // Log the response status
 380 |         console.log(`Accessibility audit response status: ${response.status}`);
 381 | 
 382 |         if (!response.ok) {
 383 |           const errorText = await response.text();
 384 |           console.error(`Accessibility audit error: ${errorText}`);
 385 |           throw new Error(`Server returned ${response.status}: ${errorText}`);
 386 |         }
 387 | 
 388 |         const json = await response.json();
 389 | 
 390 |         // flatten it by merging metadata with the report contents
 391 |         if (json.report) {
 392 |           const { metadata, report } = json;
 393 |           const flattened = {
 394 |             ...metadata,
 395 |             ...report,
 396 |           };
 397 | 
 398 |           return {
 399 |             content: [
 400 |               {
 401 |                 type: "text",
 402 |                 text: JSON.stringify(flattened, null, 2),
 403 |               },
 404 |             ],
 405 |           };
 406 |         } else {
 407 |           // Return as-is if it's not in the new format
 408 |           return {
 409 |             content: [
 410 |               {
 411 |                 type: "text",
 412 |                 text: JSON.stringify(json, null, 2),
 413 |               },
 414 |             ],
 415 |           };
 416 |         }
 417 |       } catch (error) {
 418 |         const errorMessage =
 419 |           error instanceof Error ? error.message : String(error);
 420 |         console.error("Error in accessibility audit:", errorMessage);
 421 |         return {
 422 |           content: [
 423 |             {
 424 |               type: "text",
 425 |               text: `Failed to run accessibility audit: ${errorMessage}`,
 426 |             },
 427 |           ],
 428 |         };
 429 |       }
 430 |     });
 431 |   }
 432 | );
 433 | 
 434 | // Add tool for performance audits, launches a headless browser instance
 435 | server.tool(
 436 |   "runPerformanceAudit",
 437 |   "Run a performance audit on the current page",
 438 |   {},
 439 |   async () => {
 440 |     return await withServerConnection(async () => {
 441 |       try {
 442 |         // Simplified approach - let the browser connector handle the current tab and URL
 443 |         console.log(
 444 |           `Sending POST request to http://${discoveredHost}:${discoveredPort}/performance-audit`
 445 |         );
 446 |         const response = await fetch(
 447 |           `http://${discoveredHost}:${discoveredPort}/performance-audit`,
 448 |           {
 449 |             method: "POST",
 450 |             headers: {
 451 |               "Content-Type": "application/json",
 452 |               Accept: "application/json",
 453 |             },
 454 |             body: JSON.stringify({
 455 |               category: AuditCategory.PERFORMANCE,
 456 |               source: "mcp_tool",
 457 |               timestamp: Date.now(),
 458 |             }),
 459 |           }
 460 |         );
 461 | 
 462 |         // Log the response status
 463 |         console.log(`Performance audit response status: ${response.status}`);
 464 | 
 465 |         if (!response.ok) {
 466 |           const errorText = await response.text();
 467 |           console.error(`Performance audit error: ${errorText}`);
 468 |           throw new Error(`Server returned ${response.status}: ${errorText}`);
 469 |         }
 470 | 
 471 |         const json = await response.json();
 472 | 
 473 |         // flatten it by merging metadata with the report contents
 474 |         if (json.report) {
 475 |           const { metadata, report } = json;
 476 |           const flattened = {
 477 |             ...metadata,
 478 |             ...report,
 479 |           };
 480 | 
 481 |           return {
 482 |             content: [
 483 |               {
 484 |                 type: "text",
 485 |                 text: JSON.stringify(flattened, null, 2),
 486 |               },
 487 |             ],
 488 |           };
 489 |         } else {
 490 |           // Return as-is if it's not in the new format
 491 |           return {
 492 |             content: [
 493 |               {
 494 |                 type: "text",
 495 |                 text: JSON.stringify(json, null, 2),
 496 |               },
 497 |             ],
 498 |           };
 499 |         }
 500 |       } catch (error) {
 501 |         const errorMessage =
 502 |           error instanceof Error ? error.message : String(error);
 503 |         console.error("Error in performance audit:", errorMessage);
 504 |         return {
 505 |           content: [
 506 |             {
 507 |               type: "text",
 508 |               text: `Failed to run performance audit: ${errorMessage}`,
 509 |             },
 510 |           ],
 511 |         };
 512 |       }
 513 |     });
 514 |   }
 515 | );
 516 | 
 517 | // Add tool for SEO audits, launches a headless browser instance
 518 | server.tool(
 519 |   "runSEOAudit",
 520 |   "Run an SEO audit on the current page",
 521 |   {},
 522 |   async () => {
 523 |     return await withServerConnection(async () => {
 524 |       try {
 525 |         console.log(
 526 |           `Sending POST request to http://${discoveredHost}:${discoveredPort}/seo-audit`
 527 |         );
 528 |         const response = await fetch(
 529 |           `http://${discoveredHost}:${discoveredPort}/seo-audit`,
 530 |           {
 531 |             method: "POST",
 532 |             headers: {
 533 |               "Content-Type": "application/json",
 534 |               Accept: "application/json",
 535 |             },
 536 |             body: JSON.stringify({
 537 |               category: AuditCategory.SEO,
 538 |               source: "mcp_tool",
 539 |               timestamp: Date.now(),
 540 |             }),
 541 |           }
 542 |         );
 543 | 
 544 |         // Log the response status
 545 |         console.log(`SEO audit response status: ${response.status}`);
 546 | 
 547 |         if (!response.ok) {
 548 |           const errorText = await response.text();
 549 |           console.error(`SEO audit error: ${errorText}`);
 550 |           throw new Error(`Server returned ${response.status}: ${errorText}`);
 551 |         }
 552 | 
 553 |         const json = await response.json();
 554 | 
 555 |         return {
 556 |           content: [
 557 |             {
 558 |               type: "text",
 559 |               text: JSON.stringify(json, null, 2),
 560 |             },
 561 |           ],
 562 |         };
 563 |       } catch (error) {
 564 |         const errorMessage =
 565 |           error instanceof Error ? error.message : String(error);
 566 |         console.error("Error in SEO audit:", errorMessage);
 567 |         return {
 568 |           content: [
 569 |             {
 570 |               type: "text",
 571 |               text: `Failed to run SEO audit: ${errorMessage}`,
 572 |             },
 573 |           ],
 574 |         };
 575 |       }
 576 |     });
 577 |   }
 578 | );
 579 | 
 580 | server.tool("runNextJSAudit", {}, async () => ({
 581 |   content: [
 582 |     {
 583 |       type: "text",
 584 |       text: `
 585 |       You are an expert in SEO and web development with NextJS. Given the following procedures for analyzing my codebase, please perform a comprehensive - page by page analysis of our NextJS application to identify any issues or areas of improvement for SEO.
 586 | 
 587 |       After each iteration of changes, reinvoke this tool to re-fetch our SEO audit procedures and then scan our codebase again to identify additional areas of improvement. 
 588 | 
 589 |       When no more areas of improvement are found, return "No more areas of improvement found, your NextJS application is optimized for SEO!".
 590 | 
 591 |       Start by analyzing each of the following aspects of our codebase:
 592 |       1. Meta tags - provides information about your website to search engines and social media platforms.
 593 | 
 594 |         Pages should provide the following standard meta tags:
 595 | 
 596 |         title
 597 |         description
 598 |         keywords
 599 |         robots
 600 |         viewport
 601 |         charSet
 602 |         Open Graph meta tags:
 603 | 
 604 |         og:site_name
 605 |         og:locale
 606 |         og:title
 607 |         og:description
 608 |         og:type
 609 |         og:url
 610 |         og:image
 611 |         og:image:alt
 612 |         og:image:type
 613 |         og:image:width
 614 |         og:image:height
 615 |         Article meta tags (actually it's also OpenGraph):
 616 | 
 617 |         article:published_time
 618 |         article:modified_time
 619 |         article:author
 620 |         Twitter meta tags:
 621 | 
 622 |         twitter:card
 623 |         twitter:site
 624 |         twitter:creator
 625 |         twitter:title
 626 |         twitter:description
 627 |         twitter:image
 628 | 
 629 |         For applications using the pages router, set up metatags like this in pages/[slug].tsx:
 630 |           import Head from "next/head";
 631 | 
 632 |           export default function Page() {
 633 |             return (
 634 |               <Head>
 635 |                 <title>
 636 |                   Next.js SEO: The Complete Checklist to Boost Your Site Ranking
 637 |                 </title>
 638 |                 <meta
 639 |                   name="description"
 640 |                   content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
 641 |                 />
 642 |                 <meta
 643 |                   name="keywords"
 644 |                   content="nextjs seo complete checklist, nextjs seo tutorial"
 645 |                 />
 646 |                 <meta name="robots" content="index, follow" />
 647 |                 <meta name="googlebot" content="index, follow" />
 648 |                 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 649 |                 <meta charSet="utf-8" />
 650 |                 <meta property="og:site_name" content="Blog | Minh Vu" />
 651 |                 <meta property="og:locale" content="en_US" />
 652 |                 <meta
 653 |                   property="og:title"
 654 |                   content="Next.js SEO: The Complete Checklist to Boost Your Site Ranking"
 655 |                 />
 656 |                 <meta
 657 |                   property="og:description"
 658 |                   content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
 659 |                 />
 660 |                 <meta property="og:type" content="website" />
 661 |                 <meta property="og:url" content="https://dminhvu.com/nextjs-seo" />
 662 |                 <meta
 663 |                   property="og:image"
 664 |                   content="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png"
 665 |                 />
 666 |                 <meta property="og:image:alt" content="Next.js SEO" />
 667 |                 <meta property="og:image:type" content="image/png" />
 668 |                 <meta property="og:image:width" content="1200" />
 669 |                 <meta property="og:image:height" content="630" />
 670 |                 <meta
 671 |                   property="article:published_time"
 672 |                   content="2024-01-11T11:35:00+07:00"
 673 |                 />
 674 |                 <meta
 675 |                   property="article:modified_time"
 676 |                   content="2024-01-11T11:35:00+07:00"
 677 |                 />
 678 |                 <meta
 679 |                   property="article:author"
 680 |                   content="https://www.linkedin.com/in/dminhvu02"
 681 |                 />
 682 |                 <meta name="twitter:card" content="summary_large_image" />
 683 |                 <meta name="twitter:site" content="@dminhvu02" />
 684 |                 <meta name="twitter:creator" content="@dminhvu02" />
 685 |                 <meta
 686 |                   name="twitter:title"
 687 |                   content="Next.js SEO: The Complete Checklist to Boost Your Site Ranking"
 688 |                 />
 689 |                 <meta
 690 |                   name="twitter:description"
 691 |                   content="Learn how to optimize your Next.js website for SEO by following this complete checklist."
 692 |                 />
 693 |                 <meta
 694 |                   name="twitter:image"
 695 |                   content="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-png"
 696 |                 />
 697 |               </Head>
 698 |             );
 699 |           }
 700 | 
 701 |         For applications using the app router, set up metatags like this in layout.tsx:
 702 |           import type { Viewport, Metadata } from "next";
 703 | 
 704 |           export const viewport: Viewport = {
 705 |             width: "device-width",
 706 |             initialScale: 1,
 707 |             themeColor: "#ffffff"
 708 |           };
 709 |           
 710 |           export const metadata: Metadata = {
 711 |             metadataBase: new URL("https://dminhvu.com"),
 712 |             openGraph: {
 713 |               siteName: "Blog | Minh Vu",
 714 |               type: "website",
 715 |               locale: "en_US"
 716 |             },
 717 |             robots: {
 718 |               index: true,
 719 |               follow: true,
 720 |               "max-image-preview": "large",
 721 |               "max-snippet": -1,
 722 |               "max-video-preview": -1,
 723 |               googleBot: "index, follow"
 724 |             },
 725 |             alternates: {
 726 |               types: {
 727 |                 "application/rss+xml": "https://dminhvu.com/rss.xml"
 728 |               }
 729 |             },
 730 |             applicationName: "Blog | Minh Vu",
 731 |             appleWebApp: {
 732 |               title: "Blog | Minh Vu",
 733 |               statusBarStyle: "default",
 734 |               capable: true
 735 |             },
 736 |             verification: {
 737 |               google: "YOUR_DATA",
 738 |               yandex: ["YOUR_DATA"],
 739 |               other: {
 740 |                 "msvalidate.01": ["YOUR_DATA"],
 741 |                 "facebook-domain-verification": ["YOUR_DATA"]
 742 |               }
 743 |             },
 744 |             icons: {
 745 |               icon: [
 746 |                 {
 747 |                   url: "/favicon.ico",
 748 |                   type: "image/x-icon"
 749 |                 },
 750 |                 {
 751 |                   url: "/favicon-16x16.png",
 752 |                   sizes: "16x16",
 753 |                   type: "image/png"
 754 |                 }
 755 |                 // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
 756 |               ],
 757 |               shortcut: [
 758 |                 {
 759 |                   url: "/favicon.ico",
 760 |                   type: "image/x-icon"
 761 |                 }
 762 |               ],
 763 |               apple: [
 764 |                 {
 765 |                   url: "/apple-icon-57x57.png",
 766 |                   sizes: "57x57",
 767 |                   type: "image/png"
 768 |                 },
 769 |                 {
 770 |                   url: "/apple-icon-60x60.png",
 771 |                   sizes: "60x60",
 772 |                   type: "image/png"
 773 |                 }
 774 |                 // add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png
 775 |               ]
 776 |             }
 777 |           };
 778 |         And like this for any page.tsx file:
 779 |           import { Metadata } from "next";
 780 | 
 781 |           export const metadata: Metadata = {
 782 |             title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
 783 |             description:
 784 |               "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
 785 |             keywords: [
 786 |               "elastic",
 787 |               "python",
 788 |               "javascript",
 789 |               "react",
 790 |               "machine learning",
 791 |               "data science"
 792 |             ],
 793 |             openGraph: {
 794 |               url: "https://dminhvu.com",
 795 |               type: "website",
 796 |               title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
 797 |               description:
 798 |                 "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
 799 |               images: [
 800 |                 {
 801 |                   url: "https://dminhvu.com/images/home/thumbnail.png",
 802 |                   width: 1200,
 803 |                   height: 630,
 804 |                   alt: "dminhvu"
 805 |                 }
 806 |               ]
 807 |             },
 808 |             twitter: {
 809 |               card: "summary_large_image",
 810 |               title: "Elastic Stack, Next.js, Python, JavaScript Tutorials | dminhvu",
 811 |               description:
 812 |                 "dminhvu.com - Programming blog for everyone to learn Elastic Stack, Next.js, Python, JavaScript, React, Machine Learning, Data Science, and more.",
 813 |               creator: "@dminhvu02",
 814 |               site: "@dminhvu02",
 815 |               images: [
 816 |                 {
 817 |                   url: "https://dminhvu.com/images/home/thumbnail.png",
 818 |                   width: 1200,
 819 |                   height: 630,
 820 |                   alt: "dminhvu"
 821 |                 }
 822 |               ]
 823 |             },
 824 |             alternates: {
 825 |               canonical: "https://dminhvu.com"
 826 |             }
 827 |           };
 828 | 
 829 |           Note that the charSet and viewport are automatically added by Next.js App Router, so you don't need to define them.
 830 | 
 831 |         For applications using the app router, dynamic metadata can be defined by using the generateMetadata function, this is useful when you have dynamic pages like [slug]/page.tsx, or [id]/page.tsx:
 832 | 
 833 |         import type { Metadata, ResolvingMetadata } from "next";
 834 | 
 835 |         type Params = {
 836 |           slug: string;
 837 |         };
 838 |         
 839 |         type Props = {
 840 |           params: Params;
 841 |           searchParams: { [key: string]: string | string[] | undefined };
 842 |         };
 843 |         
 844 |         export async function generateMetadata(
 845 |           { params, searchParams }: Props,
 846 |           parent: ResolvingMetadata
 847 |         ): Promise<Metadata> {
 848 |           const { slug } = params;
 849 |         
 850 |           const post: Post = await fetch("YOUR_ENDPOINT", {
 851 |             method: "GET",
 852 |             next: {
 853 |               revalidate: 60 * 60 * 24
 854 |             }
 855 |           }).then((res) => res.json());
 856 |         
 857 |           return {
 858 |             title: "{post.title} | dminhvu",
 859 |             authors: [
 860 |               {
 861 |                 name: post.author || "Minh Vu"
 862 |               }
 863 |             ],
 864 |             description: post.description,
 865 |             keywords: post.keywords,
 866 |             openGraph: {
 867 |               title: "{post.title} | dminhvu",
 868 |               description: post.description,
 869 |               type: "article",
 870 |               url: "https://dminhvu.com/{post.slug}",
 871 |               publishedTime: post.created_at,
 872 |               modifiedTime: post.modified_at,
 873 |               authors: ["https://dminhvu.com/about"],
 874 |               tags: post.categories,
 875 |               images: [
 876 |                 {
 877 |                   url: "https://ik.imagekit.io/dminhvu/assets/{post.slug}/thumbnail.png?tr=f-png",
 878 |                   width: 1024,
 879 |                   height: 576,
 880 |                   alt: post.title,
 881 |                   type: "image/png"
 882 |                 }
 883 |               ]
 884 |             },
 885 |             twitter: {
 886 |               card: "summary_large_image",
 887 |               site: "@dminhvu02",
 888 |               creator: "@dminhvu02",
 889 |               title: "{post.title} | dminhvu",
 890 |               description: post.description,
 891 |               images: [
 892 |                 {
 893 |                   url: "https://ik.imagekit.io/dminhvu/assets/{post.slug}/thumbnail.png?tr=f-png",
 894 |                   width: 1024,
 895 |                   height: 576,
 896 |                   alt: post.title
 897 |                 }
 898 |               ]
 899 |             },
 900 |             alternates: {
 901 |               canonical: "https://dminhvu.com/{post.slug}"
 902 |             }
 903 |           };
 904 |         }
 905 | 
 906 |         
 907 |       2. JSON-LD Schema
 908 | 
 909 |       JSON-LD is a format for structured data that can be used by search engines to understand your content. For example, you can use it to describe a person, an event, an organization, a movie, a book, a recipe, and many other types of entities.
 910 | 
 911 |       Our current recommendation for JSON-LD is to render structured data as a <script> tag in your layout.js or page.js components. For example:
 912 |       export default async function Page({ params }) {
 913 |         const { id } = await params
 914 |         const product = await getProduct(id)
 915 |       
 916 |         const jsonLd = {
 917 |           '@context': 'https://schema.org',
 918 |           '@type': 'Product',
 919 |           name: product.name,
 920 |           image: product.image,
 921 |           description: product.description,
 922 |         }
 923 |       
 924 |         return (
 925 |           <section>
 926 |             {/* Add JSON-LD to your page */}
 927 |             <script
 928 |               type="application/ld+json"
 929 |               dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
 930 |             />
 931 |             {/* ... */}
 932 |           </section>
 933 |         )
 934 |       }
 935 |       
 936 |       You can type your JSON-LD with TypeScript using community packages like schema-dts:
 937 | 
 938 | 
 939 |       import { Product, WithContext } from 'schema-dts'
 940 |       
 941 |       const jsonLd: WithContext<Product> = {
 942 |         '@context': 'https://schema.org',
 943 |         '@type': 'Product',
 944 |         name: 'Next.js Sticker',
 945 |         image: 'https://nextjs.org/imgs/sticker.png',
 946 |         description: 'Dynamic at the speed of static.',
 947 |       }
 948 |       3. Sitemap
 949 |       Your website should provide a sitemap so that search engines can easily crawl and index your pages.
 950 | 
 951 |         Generate Sitemap for Next.js Pages Router
 952 |         For Next.js Pages Router, you can use next-sitemap to generate a sitemap for your Next.js website after building.
 953 | 
 954 |         For example, running the following command will install next-sitemap and generate a sitemap for this blog:
 955 | 
 956 | 
 957 |         npm install next-sitemap
 958 |         npx next-sitemap
 959 |         A sitemap will be generated at public/sitemap.xml:
 960 | 
 961 |         public/sitemap.xml
 962 | 
 963 |         <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
 964 |         <url>
 965 |           <loc>https://dminhvu.com</loc>
 966 |             <lastmod>2024-01-11T02:03:09.613Z</lastmod>
 967 |             <changefreq>daily</changefreq>
 968 |           <priority>0.7</priority>
 969 |         </url>
 970 |         <!-- other pages -->
 971 |         </urlset>
 972 |         Please visit the next-sitemap page for more information.
 973 | 
 974 |         Generate Sitemap for Next.js App Router
 975 |         For Next.js App Router, you can define the sitemap.ts file at app/sitemap.ts:
 976 | 
 977 |         app/sitemap.ts
 978 | 
 979 |         import {
 980 |           getAllCategories,
 981 |           getAllPostSlugsWithModifyTime
 982 |         } from "@/utils/getData";
 983 |         import { MetadataRoute } from "next";
 984 |         
 985 |         export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
 986 |           const defaultPages = [
 987 |             {
 988 |               url: "https://dminhvu.com",
 989 |               lastModified: new Date(),
 990 |               changeFrequency: "daily",
 991 |               priority: 1
 992 |             },
 993 |             {
 994 |               url: "https://dminhvu.com/about",
 995 |               lastModified: new Date(),
 996 |               changeFrequency: "monthly",
 997 |               priority: 0.9
 998 |             },
 999 |             {
1000 |               url: "https://dminhvu.com/contact",
1001 |               lastModified: new Date(),
1002 |               changeFrequency: "monthly",
1003 |               priority: 0.9
1004 |             }
1005 |             // other pages
1006 |           ];
1007 |         
1008 |           const postSlugs = await getAllPostSlugsWithModifyTime();
1009 |           const categorySlugs = await getAllCategories();
1010 |         
1011 |           const sitemap = [
1012 |             ...defaultPages,
1013 |             ...postSlugs.map((e: any) => ({
1014 |               url: "https://dminhvu.com/{e.slug}",
1015 |               lastModified: e.modified_at,
1016 |               changeFrequency: "daily",
1017 |               priority: 0.8
1018 |             })),
1019 |             ...categorySlugs.map((e: any) => ({
1020 |               url: "https://dminhvu.com/category/{e}",
1021 |               lastModified: new Date(),
1022 |               changeFrequency: "daily",
1023 |               priority: 0.7
1024 |             }))
1025 |           ];
1026 |         
1027 |           return sitemap;
1028 |         }
1029 |         With this sitemap.ts file created, you can access the sitemap at https://dminhvu.com/sitemap.xml.
1030 | 
1031 | 
1032 |         <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1033 |           <url>
1034 |             <loc>https://dminhvu.com</loc>
1035 |             <lastmod>2024-01-11T02:03:09.613Z</lastmod>
1036 |             <changefreq>daily</changefreq>
1037 |             <priority>0.7</priority>
1038 |           </url>
1039 |           <!-- other pages -->
1040 |         </urlset>
1041 |       4. robots.txt
1042 |       A robots.txt file should be added to tell search engines which pages to crawl and which pages to ignore.
1043 | 
1044 |         robots.txt for Next.js Pages Router
1045 |         For Next.js Pages Router, you can create a robots.txt file at public/robots.txt:
1046 | 
1047 |         public/robots.txt
1048 | 
1049 |         User-agent: *
1050 |         Disallow:
1051 |         Sitemap: https://dminhvu.com/sitemap.xml
1052 |         You can prevent the search engine from crawling a page (usually search result pages, noindex pages, etc.) by adding the following line:
1053 | 
1054 |         public/robots.txt
1055 | 
1056 |         User-agent: *
1057 |         Disallow: /search?q=
1058 |         Disallow: /admin
1059 |         robots.txt for Next.js App Router
1060 |         For Next.js App Router, you don't need to manually define a robots.txt file. Instead, you can define the robots.ts file at app/robots.ts:
1061 | 
1062 |         app/robots.ts
1063 | 
1064 |         import { MetadataRoute } from "next";
1065 |         
1066 |         export default function robots(): MetadataRoute.Robots {
1067 |           return {
1068 |             rules: {
1069 |               userAgent: "*",
1070 |               allow: ["/"],
1071 |               disallow: ["/search?q=", "/admin/"]
1072 |             },
1073 |             sitemap: ["https://dminhvu.com/sitemap.xml"]
1074 |           };
1075 |         }
1076 |         With this robots.ts file created, you can access the robots.txt file at https://dminhvu.com/robots.txt.
1077 | 
1078 | 
1079 |         User-agent: *
1080 |         Allow: /
1081 |         Disallow: /search?q=
1082 |         Disallow: /admin
1083 |         
1084 |         Sitemap: https://dminhvu.com/sitemap.xml
1085 |       5. Link tags
1086 |       Link Tags for Next.js Pages Router
1087 |       For example, the current page has the following link tags if I use the Pages Router:
1088 | 
1089 |       pages/_app.tsx
1090 | 
1091 |       import Head from "next/head";
1092 |       
1093 |       export default function Page() {
1094 |         return (
1095 |           <Head>
1096 |             {/* other parts */}
1097 |             <link
1098 |               rel="alternate"
1099 |               type="application/rss+xml"
1100 |               href="https://dminhvu.com/rss.xml"
1101 |             />
1102 |             <link rel="icon" href="/favicon.ico" type="image/x-icon" />
1103 |             <link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png" />
1104 |             <link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png" />
1105 |             {/* add apple-touch-icon-72x72.png, apple-touch-icon-76x76.png, apple-touch-icon-114x114.png, apple-touch-icon-120x120.png, apple-touch-icon-144x144.png, apple-touch-icon-152x152.png, apple-touch-icon-180x180.png */}
1106 |             <link
1107 |               rel="icon"
1108 |               type="image/png"
1109 |               href="/favicon-16x16.png"
1110 |               sizes="16x16"
1111 |             />
1112 |             {/* add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png */}
1113 |           </Head>
1114 |         );
1115 |       }
1116 |       pages/[slug].tsx
1117 | 
1118 |       import Head from "next/head";
1119 |       
1120 |       export default function Page() {
1121 |         return (
1122 |           <Head>
1123 |             {/* other parts */}
1124 |             <link rel="canonical" href="https://dminhvu.com/nextjs-seo" />
1125 |           </Head>
1126 |         );
1127 |       }
1128 |       Link Tags for Next.js App Router
1129 |       For Next.js App Router, the link tags can be defined using the export const metadata or generateMetadata similar to the meta tags section.
1130 | 
1131 |       The code below is exactly the same as the meta tags for Next.js App Router section above.
1132 | 
1133 |       app/layout.tsx
1134 | 
1135 |       export const metadata: Metadata = {
1136 |         // other parts
1137 |         alternates: {
1138 |           types: {
1139 |             "application/rss+xml": "https://dminhvu.com/rss.xml"
1140 |           }
1141 |         },
1142 |         icons: {
1143 |           icon: [
1144 |             {
1145 |               url: "/favicon.ico",
1146 |               type: "image/x-icon"
1147 |             },
1148 |             {
1149 |               url: "/favicon-16x16.png",
1150 |               sizes: "16x16",
1151 |               type: "image/png"
1152 |             }
1153 |             // add favicon-32x32.png, favicon-96x96.png, android-chrome-192x192.png
1154 |           ],
1155 |           shortcut: [
1156 |             {
1157 |               url: "/favicon.ico",
1158 |               type: "image/x-icon"
1159 |             }
1160 |           ],
1161 |           apple: [
1162 |             {
1163 |               url: "/apple-icon-57x57.png",
1164 |               sizes: "57x57",
1165 |               type: "image/png"
1166 |             },
1167 |             {
1168 |               url: "/apple-icon-60x60.png",
1169 |               sizes: "60x60",
1170 |               type: "image/png"
1171 |             }
1172 |             // add apple-icon-72x72.png, apple-icon-76x76.png, apple-icon-114x114.png, apple-icon-120x120.png, apple-icon-144x144.png, apple-icon-152x152.png, apple-icon-180x180.png
1173 |           ]
1174 |         }
1175 |       };
1176 |       app/page.tsx
1177 | 
1178 |       export const metadata: Metadata = {
1179 |         // other parts
1180 |         alternates: {
1181 |           canonical: "https://dminhvu.com"
1182 |         }
1183 |       };
1184 |       6. Script optimization
1185 |       Script Optimization for General Scripts
1186 |       Next.js provides a built-in component called <Script> to add external scripts to your website.
1187 | 
1188 |       For example, you can add Google Analytics to your website by adding the following script tag:
1189 | 
1190 |       pages/_app.tsx
1191 | 
1192 |       import Head from "next/head";
1193 |       import Script from "next/script";
1194 |       
1195 |       export default function Page() {
1196 |         return (
1197 |           <Head>
1198 |             {/* other parts */}
1199 |             {process.env.NODE_ENV === "production" && (
1200 |               <>
1201 |                 <Script async strategy="afterInteractive" id="analytics">
1202 |                   {'
1203 |                     window.dataLayer = window.dataLayer || [];
1204 |                     function gtag(){dataLayer.push(arguments);}
1205 |                     gtag('js', new Date());
1206 |                     gtag('config', 'G-XXXXXXXXXX');
1207 |                   '}
1208 |                 </Script>
1209 |               </>
1210 |             )}
1211 |           </Head>
1212 |         );
1213 |       }
1214 |       Script Optimization for Common Third-Party Integrations
1215 |       Next.js App Router introduces a new library called @next/third-parties for:
1216 | 
1217 |       Google Tag Manager
1218 |       Google Analytics
1219 |       Google Maps Embed
1220 |       YouTube Embed
1221 |       To use the @next/third-parties library, you need to install it:
1222 | 
1223 | 
1224 |       npm install @next/third-parties
1225 |       Then, you can add the following code to your app/layout.tsx:
1226 | 
1227 |       app/layout.tsx
1228 | 
1229 |       import { GoogleTagManager } from "@next/third-parties/google";
1230 |       import { GoogleAnalytics } from "@next/third-parties/google";
1231 |       import Head from "next/head";
1232 |       
1233 |       export default function Page() {
1234 |         return (
1235 |           <html lang="en" className="scroll-smooth" suppressHydrationWarning>
1236 |             {process.env.NODE_ENV === "production" && (
1237 |               <>
1238 |                 <GoogleAnalytics gaId="G-XXXXXXXXXX" />
1239 |                 {/* other scripts */}
1240 |               </>
1241 |             )}
1242 |             {/* other parts */}
1243 |           </html>
1244 |         );
1245 |       }
1246 |       Please note that you don't need to include both GoogleTagManager and GoogleAnalytics if you only use one of them.
1247 |       7. Image optimization
1248 |       Image Optimization
1249 |       This part can be applied to both Pages Router and App Router.
1250 | 
1251 |       Image optimization is also an important part of SEO as it helps your website load faster.
1252 | 
1253 |       Faster image rendering speed will contribute to the Google PageSpeed score, which can improve user experience and SEO.
1254 | 
1255 |       You can use next/image to optimize images in your Next.js website.
1256 | 
1257 |       For example, the following code will optimize this post thumbnail:
1258 | 
1259 | 
1260 |       import Image from "next/image";
1261 |       
1262 |       export default function Page() {
1263 |         return (
1264 |           <Image
1265 |             src="https://ik.imagekit.io/dminhvu/assets/nextjs-seo/thumbnail.png?tr=f-webp"
1266 |             alt="Next.js SEO"
1267 |             width={1200}
1268 |             height={630}
1269 |           />
1270 |         );
1271 |       }
1272 |       Remember to use a CDN to serve your media (images, videos, etc.) to improve the loading speed.
1273 | 
1274 |       For the image format, use WebP if possible because it has a smaller size than PNG and JPEG.
1275 | 
1276 |       Given the provided procedures, begin by analyzing all of our Next.js pages.
1277 |       Check to see what metadata already exists, look for any robot.txt files, and take a closer look at some of the other aspects of our project to determine areas of improvement.
1278 |       Once you've performed this comprehensive analysis, return back a report on what we can do to improve our application.
1279 |       Do not actually make the code changes yet, just return a comprehensive plan that you will ask for approval for.
1280 |       If feedback is provided, adjust the plan accordingly and ask for approval again.
1281 |       If the user approves of the plan, go ahead and proceed to implement all the necessary code changes to completely optimize our application.
1282 |     `,
1283 |     },
1284 |   ],
1285 | }));
1286 | 
1287 | server.tool(
1288 |   "runDebuggerMode",
1289 |   "Run debugger mode to debug an issue in our application",
1290 |   async () => ({
1291 |     content: [
1292 |       {
1293 |         type: "text",
1294 |         text: `
1295 |       Please follow this exact sequence to debug an issue in our application:
1296 |   
1297 |   1. Reflect on 5-7 different possible sources of the problem
1298 |   2. Distill those down to 1-2 most likely sources
1299 |   3. Add additional logs to validate your assumptions and track the transformation of data structures throughout the application control flow before we move onto implementing the actual code fix
1300 |   4. Use the "getConsoleLogs", "getConsoleErrors", "getNetworkLogs" & "getNetworkErrors" tools to obtain any newly added web browser logs
1301 |   5. Obtain the server logs as well if accessible - otherwise, ask me to copy/paste them into the chat
1302 |   6. Deeply reflect on what could be wrong + produce a comprehensive analysis of the issue
1303 |   7. Suggest additional logs if the issue persists or if the source is not yet clear
1304 |   8. Once a fix is implemented, ask for approval to remove the previously added logs
1305 | 
1306 |   Note: DO NOT run any of our audits (runAccessibilityAudit, runPerformanceAudit, runBestPracticesAudit, runSEOAudit, runNextJSAudit) when in debugging mode unless explicitly asked to do so or unless you switch to audit mode.
1307 | `,
1308 |       },
1309 |     ],
1310 |   })
1311 | );
1312 | 
1313 | server.tool(
1314 |   "runAuditMode",
1315 |   "Run audit mode to optimize our application for SEO, accessibility and performance",
1316 |   async () => ({
1317 |     content: [
1318 |       {
1319 |         type: "text",
1320 |         text: `
1321 |       I want you to enter "Audit Mode". Use the following MCP tools one after the other in this exact sequence:
1322 |       
1323 |       1. runAccessibilityAudit
1324 |       2. runPerformanceAudit
1325 |       3. runBestPracticesAudit
1326 |       4. runSEOAudit
1327 |       5. runNextJSAudit (only if our application is ACTUALLY using NextJS)
1328 | 
1329 |       After running all of these tools, return back a comprehensive analysis of the audit results.
1330 | 
1331 |       Do NOT use runNextJSAudit tool unless you see that our application is ACTUALLY using NextJS.
1332 | 
1333 |       DO NOT use the takeScreenshot tool EVER during audit mode. ONLY use it if I specifically ask you to take a screenshot of something.
1334 | 
1335 |       DO NOT check console or network logs to get started - your main priority is to run the audits in the sequence defined above.
1336 |       
1337 |       After returning an in-depth analysis, scan through my code and identify various files/parts of my codebase that we want to modify/improve based on the results of our audits.
1338 | 
1339 |       After identifying what changes may be needed, do NOT make the actual changes. Instead, return back a comprehensive, step-by-step plan to address all of these changes and ask for approval to execute this plan. If feedback is received, make a new plan and ask for approval again. If approved, execute the ENTIRE plan and after all phases/steps are complete, re-run the auditing tools in the same 4 step sequence again before returning back another analysis for additional changes potentially needed.
1340 | 
1341 |       Keep repeating / iterating through this process with the four tools until our application is as optimized as possible for SEO, accessibility and performance.
1342 | 
1343 | `,
1344 |       },
1345 |     ],
1346 |   })
1347 | );
1348 | 
1349 | // Add tool for Best Practices audits, launches a headless browser instance
1350 | server.tool(
1351 |   "runBestPracticesAudit",
1352 |   "Run a best practices audit on the current page",
1353 |   {},
1354 |   async () => {
1355 |     return await withServerConnection(async () => {
1356 |       try {
1357 |         console.log(
1358 |           `Sending POST request to http://${discoveredHost}:${discoveredPort}/best-practices-audit`
1359 |         );
1360 |         const response = await fetch(
1361 |           `http://${discoveredHost}:${discoveredPort}/best-practices-audit`,
1362 |           {
1363 |             method: "POST",
1364 |             headers: {
1365 |               "Content-Type": "application/json",
1366 |               Accept: "application/json",
1367 |             },
1368 |             body: JSON.stringify({
1369 |               source: "mcp_tool",
1370 |               timestamp: Date.now(),
1371 |             }),
1372 |           }
1373 |         );
1374 | 
1375 |         // Check for errors
1376 |         if (!response.ok) {
1377 |           const errorText = await response.text();
1378 |           throw new Error(`Server returned ${response.status}: ${errorText}`);
1379 |         }
1380 | 
1381 |         const json = await response.json();
1382 | 
1383 |         // flatten it by merging metadata with the report contents
1384 |         if (json.report) {
1385 |           const { metadata, report } = json;
1386 |           const flattened = {
1387 |             ...metadata,
1388 |             ...report,
1389 |           };
1390 | 
1391 |           return {
1392 |             content: [
1393 |               {
1394 |                 type: "text",
1395 |                 text: JSON.stringify(flattened, null, 2),
1396 |               },
1397 |             ],
1398 |           };
1399 |         } else {
1400 |           // Return as-is if it's not in the new format
1401 |           return {
1402 |             content: [
1403 |               {
1404 |                 type: "text",
1405 |                 text: JSON.stringify(json, null, 2),
1406 |               },
1407 |             ],
1408 |           };
1409 |         }
1410 |       } catch (error) {
1411 |         const errorMessage =
1412 |           error instanceof Error ? error.message : String(error);
1413 |         console.error("Error in Best Practices audit:", errorMessage);
1414 |         return {
1415 |           content: [
1416 |             {
1417 |               type: "text",
1418 |               text: `Failed to run Best Practices audit: ${errorMessage}`,
1419 |             },
1420 |           ],
1421 |         };
1422 |       }
1423 |     });
1424 |   }
1425 | );
1426 | 
1427 | // Start receiving messages on stdio
1428 | (async () => {
1429 |   try {
1430 |     // Attempt initial server discovery
1431 |     console.error("Attempting initial server discovery on startup...");
1432 |     await discoverServer();
1433 |     if (serverDiscovered) {
1434 |       console.error(
1435 |         `Successfully discovered server at ${discoveredHost}:${discoveredPort}`
1436 |       );
1437 |     } else {
1438 |       console.error(
1439 |         "Initial server discovery failed. Will try again when tools are used."
1440 |       );
1441 |     }
1442 | 
1443 |     const transport = new StdioServerTransport();
1444 | 
1445 |     // Ensure stdout is only used for JSON messages
1446 |     const originalStdoutWrite = process.stdout.write.bind(process.stdout);
1447 |     process.stdout.write = (chunk: any, encoding?: any, callback?: any) => {
1448 |       // Only allow JSON messages to pass through
1449 |       if (typeof chunk === "string" && !chunk.startsWith("{")) {
1450 |         return true; // Silently skip non-JSON messages
1451 |       }
1452 |       return originalStdoutWrite(chunk, encoding, callback);
1453 |     };
1454 | 
1455 |     await server.connect(transport);
1456 |   } catch (error) {
1457 |     console.error("Failed to initialize MCP server:", error);
1458 |     process.exit(1);
1459 |   }
1460 | })();
1461 | 
```
Page 2/3FirstPrevNextLast