#
tokens: 30686/50000 2/29 files (page 3/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 3 of 3. Use http://codebase.md/sugatraj/cursor-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
├── debugcommands.mdc
├── docs
│   ├── mcp-docs.md
│   └── mcp.md
├── LICENSE
├── package.json
├── README.md
├── SETUP_INSTRUCTIONS.md
├── start-servers.bat
└── start-servers.sh
```

# Files

--------------------------------------------------------------------------------
/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 | 
```

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

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