#
tokens: 38886/50000 4/29 files (page 2/3)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 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

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

```markdown
   1 | ## Resources
   2 | 
   3 | Expose data and content from your servers to LLMs
   4 | 
   5 | Resources are a core primitive in the Model Context Protocol (MCP) that allow servers to expose data and content that can be read by clients and used as context for LLM interactions.
   6 | 
   7 | Resources are designed to be application-controlled, meaning that the client application can decide how and when they should be used. Different MCP clients may handle resources differently. For example:
   8 | 
   9 | Claude Desktop currently requires users to explicitly select resources before they can be used
  10 | Other clients might automatically select resources based on heuristics
  11 | Some implementations may even allow the AI model itself to determine which resources to use
  12 | Server authors should be prepared to handle any of these interaction patterns when implementing resource support. In order to expose data to models automatically, server authors should use a model-controlled primitive such as Tools.
  13 | 
  14 | ​
  15 | Overview
  16 | Resources represent any kind of data that an MCP server wants to make available to clients. This can include:
  17 | 
  18 | File contents
  19 | Database records
  20 | API responses
  21 | Live system data
  22 | Screenshots and images
  23 | Log files
  24 | And more
  25 | Each resource is identified by a unique URI and can contain either text or binary data.
  26 | 
  27 | ​
  28 | Resource URIs
  29 | Resources are identified using URIs that follow this format:
  30 | 
  31 | [protocol]://[host]/[path]
  32 | For example:
  33 | 
  34 | file:///home/user/documents/report.pdf
  35 | postgres://database/customers/schema
  36 | screen://localhost/display1
  37 | The protocol and path structure is defined by the MCP server implementation. Servers can define their own custom URI schemes.
  38 | 
  39 | ​
  40 | Resource types
  41 | Resources can contain two types of content:
  42 | 
  43 | ​
  44 | Text resources
  45 | Text resources contain UTF-8 encoded text data. These are suitable for:
  46 | 
  47 | Source code
  48 | Configuration files
  49 | Log files
  50 | JSON/XML data
  51 | Plain text
  52 | ​
  53 | Binary resources
  54 | Binary resources contain raw binary data encoded in base64. These are suitable for:
  55 | 
  56 | Images
  57 | PDFs
  58 | Audio files
  59 | Video files
  60 | Other non-text formats
  61 | ​
  62 | Resource discovery
  63 | Clients can discover available resources through two main methods:
  64 | 
  65 | ​
  66 | Direct resources
  67 | Servers expose a list of concrete resources via the resources/list endpoint. Each resource includes:
  68 | 
  69 | {
  70 | uri: string; // Unique identifier for the resource
  71 | name: string; // Human-readable name
  72 | description?: string; // Optional description
  73 | mimeType?: string; // Optional MIME type
  74 | }
  75 | ​
  76 | Resource templates
  77 | For dynamic resources, servers can expose URI templates that clients can use to construct valid resource URIs:
  78 | 
  79 | {
  80 | uriTemplate: string; // URI template following RFC 6570
  81 | name: string; // Human-readable name for this type
  82 | description?: string; // Optional description
  83 | mimeType?: string; // Optional MIME type for all matching resources
  84 | }
  85 | ​
  86 | Reading resources
  87 | To read a resource, clients make a resources/read request with the resource URI.
  88 | 
  89 | The server responds with a list of resource contents:
  90 | 
  91 | {
  92 | contents: [
  93 | {
  94 | uri: string; // The URI of the resource
  95 | mimeType?: string; // Optional MIME type
  96 | 
  97 |       // One of:
  98 |       text?: string;      // For text resources
  99 |       blob?: string;      // For binary resources (base64 encoded)
 100 |     }
 101 | 
 102 | ]
 103 | }
 104 | Servers may return multiple resources in response to one resources/read request. This could be used, for example, to return a list of files inside a directory when the directory is read.
 105 | 
 106 | ​
 107 | Resource updates
 108 | MCP supports real-time updates for resources through two mechanisms:
 109 | 
 110 | ​
 111 | List changes
 112 | Servers can notify clients when their list of available resources changes via the notifications/resources/list_changed notification.
 113 | 
 114 | ​
 115 | Content changes
 116 | Clients can subscribe to updates for specific resources:
 117 | 
 118 | Client sends resources/subscribe with resource URI
 119 | Server sends notifications/resources/updated when the resource changes
 120 | Client can fetch latest content with resources/read
 121 | Client can unsubscribe with resources/unsubscribe
 122 | ​
 123 | Example implementation
 124 | Here’s a simple example of implementing resource support in an MCP server:
 125 | 
 126 | ## Prompts
 127 | 
 128 | Create reusable prompt templates and workflows
 129 | 
 130 | Prompts enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. They provide a powerful way to standardize and share common LLM interactions.
 131 | 
 132 | Prompts are designed to be user-controlled, meaning they are exposed from servers to clients with the intention of the user being able to explicitly select them for use.
 133 | 
 134 | ​
 135 | Overview
 136 | Prompts in MCP are predefined templates that can:
 137 | 
 138 | Accept dynamic arguments
 139 | Include context from resources
 140 | Chain multiple interactions
 141 | Guide specific workflows
 142 | Surface as UI elements (like slash commands)
 143 | ​
 144 | Prompt structure
 145 | Each prompt is defined with:
 146 | 
 147 | {
 148 | name: string; // Unique identifier for the prompt
 149 | description?: string; // Human-readable description
 150 | arguments?: [ // Optional list of arguments
 151 | {
 152 | name: string; // Argument identifier
 153 | description?: string; // Argument description
 154 | required?: boolean; // Whether argument is required
 155 | }
 156 | ]
 157 | }
 158 | ​
 159 | Discovering prompts
 160 | Clients can discover available prompts through the prompts/list endpoint:
 161 | 
 162 | // Request
 163 | {
 164 | method: "prompts/list"
 165 | }
 166 | 
 167 | // Response
 168 | {
 169 | prompts: [
 170 | {
 171 | name: "analyze-code",
 172 | description: "Analyze code for potential improvements",
 173 | arguments: [
 174 | {
 175 | name: "language",
 176 | description: "Programming language",
 177 | required: true
 178 | }
 179 | ]
 180 | }
 181 | ]
 182 | }
 183 | ​
 184 | Using prompts
 185 | To use a prompt, clients make a prompts/get request:
 186 | 
 187 | // Request
 188 | {
 189 | method: "prompts/get",
 190 | params: {
 191 | name: "analyze-code",
 192 | arguments: {
 193 | language: "python"
 194 | }
 195 | }
 196 | }
 197 | 
 198 | // Response
 199 | {
 200 | description: "Analyze Python code for potential improvements",
 201 | messages: [
 202 | {
 203 | role: "user",
 204 | content: {
 205 | type: "text",
 206 | text: "Please analyze the following Python code for potential improvements:\n\n`python\ndef calculate_sum(numbers):\n    total = 0\n    for num in numbers:\n        total = total + num\n    return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n`"
 207 | }
 208 | }
 209 | ]
 210 | }
 211 | ​
 212 | Dynamic prompts
 213 | Prompts can be dynamic and include:
 214 | 
 215 | ​
 216 | Embedded resource context
 217 | 
 218 | {
 219 | "name": "analyze-project",
 220 | "description": "Analyze project logs and code",
 221 | "arguments": [
 222 | {
 223 | "name": "timeframe",
 224 | "description": "Time period to analyze logs",
 225 | "required": true
 226 | },
 227 | {
 228 | "name": "fileUri",
 229 | "description": "URI of code file to review",
 230 | "required": true
 231 | }
 232 | ]
 233 | }
 234 | When handling the prompts/get request:
 235 | 
 236 | {
 237 | "messages": [
 238 | {
 239 | "role": "user",
 240 | "content": {
 241 | "type": "text",
 242 | "text": "Analyze these system logs and the code file for any issues:"
 243 | }
 244 | },
 245 | {
 246 | "role": "user",
 247 | "content": {
 248 | "type": "resource",
 249 | "resource": {
 250 | "uri": "logs://recent?timeframe=1h",
 251 | "text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded",
 252 | "mimeType": "text/plain"
 253 | }
 254 | }
 255 | },
 256 | {
 257 | "role": "user",
 258 | "content": {
 259 | "type": "resource",
 260 | "resource": {
 261 | "uri": "file:///path/to/code.py",
 262 | "text": "def connect_to_service(timeout=30):\n retries = 3\n for attempt in range(retries):\n try:\n return establish_connection(timeout)\n except TimeoutError:\n if attempt == retries - 1:\n raise\n time.sleep(5)\n\ndef establish_connection(timeout):\n # Connection implementation\n pass",
 263 | "mimeType": "text/x-python"
 264 | }
 265 | }
 266 | }
 267 | ]
 268 | }
 269 | ​
 270 | Multi-step workflows
 271 | 
 272 | const debugWorkflow = {
 273 | name: "debug-error",
 274 | async getMessages(error: string) {
 275 | return [
 276 | {
 277 | role: "user",
 278 | content: {
 279 | type: "text",
 280 | text: `Here's an error I'm seeing: ${error}`
 281 | }
 282 | },
 283 | {
 284 | role: "assistant",
 285 | content: {
 286 | type: "text",
 287 | text: "I'll help analyze this error. What have you tried so far?"
 288 | }
 289 | },
 290 | {
 291 | role: "user",
 292 | content: {
 293 | type: "text",
 294 | text: "I've tried restarting the service, but the error persists."
 295 | }
 296 | }
 297 | ];
 298 | }
 299 | };
 300 | ​
 301 | Example implementation
 302 | Here’s a complete example of implementing prompts in an MCP server:
 303 | 
 304 | TypeScript
 305 | Python
 306 | 
 307 | import { Server } from "@modelcontextprotocol/sdk/server";
 308 | import {
 309 | ListPromptsRequestSchema,
 310 | GetPromptRequestSchema
 311 | } from "@modelcontextprotocol/sdk/types";
 312 | 
 313 | const PROMPTS = {
 314 | "git-commit": {
 315 | name: "git-commit",
 316 | description: "Generate a Git commit message",
 317 | arguments: [
 318 | {
 319 | name: "changes",
 320 | description: "Git diff or description of changes",
 321 | required: true
 322 | }
 323 | ]
 324 | },
 325 | "explain-code": {
 326 | name: "explain-code",
 327 | description: "Explain how code works",
 328 | arguments: [
 329 | {
 330 | name: "code",
 331 | description: "Code to explain",
 332 | required: true
 333 | },
 334 | {
 335 | name: "language",
 336 | description: "Programming language",
 337 | required: false
 338 | }
 339 | ]
 340 | }
 341 | };
 342 | 
 343 | const server = new Server({
 344 | name: "example-prompts-server",
 345 | version: "1.0.0"
 346 | }, {
 347 | capabilities: {
 348 | prompts: {}
 349 | }
 350 | });
 351 | 
 352 | // List available prompts
 353 | server.setRequestHandler(ListPromptsRequestSchema, async () => {
 354 | return {
 355 | prompts: Object.values(PROMPTS)
 356 | };
 357 | });
 358 | 
 359 | // Get specific prompt
 360 | server.setRequestHandler(GetPromptRequestSchema, async (request) => {
 361 | const prompt = PROMPTS[request.params.name];
 362 | if (!prompt) {
 363 | throw new Error(`Prompt not found: ${request.params.name}`);
 364 | }
 365 | 
 366 | if (request.params.name === "git-commit") {
 367 | return {
 368 | messages: [
 369 | {
 370 | role: "user",
 371 | content: {
 372 | type: "text",
 373 | text: `Generate a concise but descriptive commit message for these changes:\n\n${request.params.arguments?.changes}`
 374 | }
 375 | }
 376 | ]
 377 | };
 378 | }
 379 | 
 380 | if (request.params.name === "explain-code") {
 381 | const language = request.params.arguments?.language || "Unknown";
 382 | return {
 383 | messages: [
 384 | {
 385 | role: "user",
 386 | content: {
 387 | type: "text",
 388 | text: `Explain how this ${language} code works:\n\n${request.params.arguments?.code}`
 389 | }
 390 | }
 391 | ]
 392 | };
 393 | }
 394 | 
 395 | throw new Error("Prompt implementation not found");
 396 | });
 397 | ​
 398 | Best practices
 399 | When implementing prompts:
 400 | 
 401 | Use clear, descriptive prompt names
 402 | Provide detailed descriptions for prompts and arguments
 403 | Validate all required arguments
 404 | Handle missing arguments gracefully
 405 | Consider versioning for prompt templates
 406 | Cache dynamic content when appropriate
 407 | Implement error handling
 408 | Document expected argument formats
 409 | Consider prompt composability
 410 | Test prompts with various inputs
 411 | ​
 412 | UI integration
 413 | Prompts can be surfaced in client UIs as:
 414 | 
 415 | Slash commands
 416 | Quick actions
 417 | Context menu items
 418 | Command palette entries
 419 | Guided workflows
 420 | Interactive forms
 421 | ​
 422 | Updates and changes
 423 | Servers can notify clients about prompt changes:
 424 | 
 425 | Server capability: prompts.listChanged
 426 | Notification: notifications/prompts/list_changed
 427 | Client re-fetches prompt list
 428 | ​
 429 | Security considerations
 430 | When implementing prompts:
 431 | 
 432 | Validate all arguments
 433 | Sanitize user input
 434 | Consider rate limiting
 435 | Implement access controls
 436 | Audit prompt usage
 437 | Handle sensitive data appropriately
 438 | Validate generated content
 439 | Implement timeouts
 440 | Consider prompt injection risks
 441 | Document security requirements
 442 | 
 443 | ## Tools
 444 | 
 445 | Tools
 446 | Enable LLMs to perform actions through your server
 447 | 
 448 | Tools are a powerful primitive in the Model Context Protocol (MCP) that enable servers to expose executable functionality to clients. Through tools, LLMs can interact with external systems, perform computations, and take actions in the real world.
 449 | 
 450 | Tools are designed to be model-controlled, meaning that tools are exposed from servers to clients with the intention of the AI model being able to automatically invoke them (with a human in the loop to grant approval).
 451 | 
 452 | ​
 453 | Overview
 454 | Tools in MCP allow servers to expose executable functions that can be invoked by clients and used by LLMs to perform actions. Key aspects of tools include:
 455 | 
 456 | Discovery: Clients can list available tools through the tools/list endpoint
 457 | Invocation: Tools are called using the tools/call endpoint, where servers perform the requested operation and return results
 458 | Flexibility: Tools can range from simple calculations to complex API interactions
 459 | Like resources, tools are identified by unique names and can include descriptions to guide their usage. However, unlike resources, tools represent dynamic operations that can modify state or interact with external systems.
 460 | 
 461 | ​
 462 | Tool definition structure
 463 | Each tool is defined with the following structure:
 464 | 
 465 | {
 466 | name: string; // Unique identifier for the tool
 467 | description?: string; // Human-readable description
 468 | inputSchema: { // JSON Schema for the tool's parameters
 469 | type: "object",
 470 | properties: { ... } // Tool-specific parameters
 471 | }
 472 | }
 473 | ​
 474 | Implementing tools
 475 | Here’s an example of implementing a basic tool in an MCP server:
 476 | 
 477 | TypeScript
 478 | Python
 479 | 
 480 | const server = new Server({
 481 | name: "example-server",
 482 | version: "1.0.0"
 483 | }, {
 484 | capabilities: {
 485 | tools: {}
 486 | }
 487 | });
 488 | 
 489 | // Define available tools
 490 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 491 | return {
 492 | tools: [{
 493 | name: "calculate_sum",
 494 | description: "Add two numbers together",
 495 | inputSchema: {
 496 | type: "object",
 497 | properties: {
 498 | a: { type: "number" },
 499 | b: { type: "number" }
 500 | },
 501 | required: ["a", "b"]
 502 | }
 503 | }]
 504 | };
 505 | });
 506 | 
 507 | // Handle tool execution
 508 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
 509 | if (request.params.name === "calculate_sum") {
 510 | const { a, b } = request.params.arguments;
 511 | return {
 512 | content: [
 513 | {
 514 | type: "text",
 515 | text: String(a + b)
 516 | }
 517 | ]
 518 | };
 519 | }
 520 | throw new Error("Tool not found");
 521 | });
 522 | ​
 523 | Example tool patterns
 524 | Here are some examples of types of tools that a server could provide:
 525 | 
 526 | ​
 527 | System operations
 528 | Tools that interact with the local system:
 529 | 
 530 | {
 531 | name: "execute_command",
 532 | description: "Run a shell command",
 533 | inputSchema: {
 534 | type: "object",
 535 | properties: {
 536 | command: { type: "string" },
 537 | args: { type: "array", items: { type: "string" } }
 538 | }
 539 | }
 540 | }
 541 | ​
 542 | API integrations
 543 | Tools that wrap external APIs:
 544 | 
 545 | {
 546 | name: "github_create_issue",
 547 | description: "Create a GitHub issue",
 548 | inputSchema: {
 549 | type: "object",
 550 | properties: {
 551 | title: { type: "string" },
 552 | body: { type: "string" },
 553 | labels: { type: "array", items: { type: "string" } }
 554 | }
 555 | }
 556 | }
 557 | ​
 558 | Data processing
 559 | Tools that transform or analyze data:
 560 | 
 561 | {
 562 | name: "analyze_csv",
 563 | description: "Analyze a CSV file",
 564 | inputSchema: {
 565 | type: "object",
 566 | properties: {
 567 | filepath: { type: "string" },
 568 | operations: {
 569 | type: "array",
 570 | items: {
 571 | enum: ["sum", "average", "count"]
 572 | }
 573 | }
 574 | }
 575 | }
 576 | }
 577 | ​
 578 | Best practices
 579 | When implementing tools:
 580 | 
 581 | Provide clear, descriptive names and descriptions
 582 | Use detailed JSON Schema definitions for parameters
 583 | Include examples in tool descriptions to demonstrate how the model should use them
 584 | Implement proper error handling and validation
 585 | Use progress reporting for long operations
 586 | Keep tool operations focused and atomic
 587 | Document expected return value structures
 588 | Implement proper timeouts
 589 | Consider rate limiting for resource-intensive operations
 590 | Log tool usage for debugging and monitoring
 591 | ​
 592 | Security considerations
 593 | When exposing tools:
 594 | 
 595 | ​
 596 | Input validation
 597 | Validate all parameters against the schema
 598 | Sanitize file paths and system commands
 599 | Validate URLs and external identifiers
 600 | Check parameter sizes and ranges
 601 | Prevent command injection
 602 | ​
 603 | Access control
 604 | Implement authentication where needed
 605 | Use appropriate authorization checks
 606 | Audit tool usage
 607 | Rate limit requests
 608 | Monitor for abuse
 609 | ​
 610 | Error handling
 611 | Don’t expose internal errors to clients
 612 | Log security-relevant errors
 613 | Handle timeouts appropriately
 614 | Clean up resources after errors
 615 | Validate return values
 616 | ​
 617 | Tool discovery and updates
 618 | MCP supports dynamic tool discovery:
 619 | 
 620 | Clients can list available tools at any time
 621 | Servers can notify clients when tools change using notifications/tools/list_changed
 622 | Tools can be added or removed during runtime
 623 | Tool definitions can be updated (though this should be done carefully)
 624 | ​
 625 | Error handling
 626 | Tool errors should be reported within the result object, not as MCP protocol-level errors. This allows the LLM to see and potentially handle the error. When a tool encounters an error:
 627 | 
 628 | Set isError to true in the result
 629 | Include error details in the content array
 630 | Here’s an example of proper error handling for tools:
 631 | 
 632 | TypeScript
 633 | Python
 634 | 
 635 | try {
 636 | // Tool operation
 637 | const result = performOperation();
 638 | return {
 639 | content: [
 640 | {
 641 | type: "text",
 642 | text: `Operation successful: ${result}`
 643 | }
 644 | ]
 645 | };
 646 | } catch (error) {
 647 | return {
 648 | isError: true,
 649 | content: [
 650 | {
 651 | type: "text",
 652 | text: `Error: ${error.message}`
 653 | }
 654 | ]
 655 | };
 656 | }
 657 | This approach allows the LLM to see that an error occurred and potentially take corrective action or request human intervention.
 658 | 
 659 | ​
 660 | Testing tools
 661 | A comprehensive testing strategy for MCP tools should cover:
 662 | 
 663 | Functional testing: Verify tools execute correctly with valid inputs and handle invalid inputs appropriately
 664 | Integration testing: Test tool interaction with external systems using both real and mocked dependencies
 665 | Security testing: Validate authentication, authorization, input sanitization, and rate limiting
 666 | Performance testing: Check behavior under load, timeout handling, and resource cleanup
 667 | Error handling: Ensure tools properly report errors through the MCP protocol and clean up resources
 668 | 
 669 | ## Sampling
 670 | 
 671 | Sampling
 672 | Let your servers request completions from LLMs
 673 | 
 674 | Sampling is a powerful MCP feature that allows servers to request LLM completions through the client, enabling sophisticated agentic behaviors while maintaining security and privacy.
 675 | 
 676 | This feature of MCP is not yet supported in the Claude Desktop client.
 677 | 
 678 | ​
 679 | How sampling works
 680 | The sampling flow follows these steps:
 681 | 
 682 | Server sends a sampling/createMessage request to the client
 683 | Client reviews the request and can modify it
 684 | Client samples from an LLM
 685 | Client reviews the completion
 686 | Client returns the result to the server
 687 | This human-in-the-loop design ensures users maintain control over what the LLM sees and generates.
 688 | 
 689 | ​
 690 | Message format
 691 | Sampling requests use a standardized message format:
 692 | 
 693 | {
 694 | messages: [
 695 | {
 696 | role: "user" | "assistant",
 697 | content: {
 698 | type: "text" | "image",
 699 | 
 700 |         // For text:
 701 |         text?: string,
 702 | 
 703 |         // For images:
 704 |         data?: string,             // base64 encoded
 705 |         mimeType?: string
 706 |       }
 707 |     }
 708 | 
 709 | ],
 710 | modelPreferences?: {
 711 | hints?: [{
 712 | name?: string // Suggested model name/family
 713 | }],
 714 | costPriority?: number, // 0-1, importance of minimizing cost
 715 | speedPriority?: number, // 0-1, importance of low latency
 716 | intelligencePriority?: number // 0-1, importance of capabilities
 717 | },
 718 | systemPrompt?: string,
 719 | includeContext?: "none" | "thisServer" | "allServers",
 720 | temperature?: number,
 721 | maxTokens: number,
 722 | stopSequences?: string[],
 723 | metadata?: Record<string, unknown>
 724 | }
 725 | ​
 726 | Request parameters
 727 | ​
 728 | Messages
 729 | The messages array contains the conversation history to send to the LLM. Each message has:
 730 | 
 731 | role: Either “user” or “assistant”
 732 | content: The message content, which can be:
 733 | Text content with a text field
 734 | Image content with data (base64) and mimeType fields
 735 | ​
 736 | Model preferences
 737 | The modelPreferences object allows servers to specify their model selection preferences:
 738 | 
 739 | hints: Array of model name suggestions that clients can use to select an appropriate model:
 740 | 
 741 | name: String that can match full or partial model names (e.g. “claude-3”, “sonnet”)
 742 | Clients may map hints to equivalent models from different providers
 743 | Multiple hints are evaluated in preference order
 744 | Priority values (0-1 normalized):
 745 | 
 746 | costPriority: Importance of minimizing costs
 747 | speedPriority: Importance of low latency response
 748 | intelligencePriority: Importance of advanced model capabilities
 749 | Clients make the final model selection based on these preferences and their available models.
 750 | 
 751 | ​
 752 | System prompt
 753 | An optional systemPrompt field allows servers to request a specific system prompt. The client may modify or ignore this.
 754 | 
 755 | ​
 756 | Context inclusion
 757 | The includeContext parameter specifies what MCP context to include:
 758 | 
 759 | "none": No additional context
 760 | "thisServer": Include context from the requesting server
 761 | "allServers": Include context from all connected MCP servers
 762 | The client controls what context is actually included.
 763 | 
 764 | ​
 765 | Sampling parameters
 766 | Fine-tune the LLM sampling with:
 767 | 
 768 | temperature: Controls randomness (0.0 to 1.0)
 769 | maxTokens: Maximum tokens to generate
 770 | stopSequences: Array of sequences that stop generation
 771 | metadata: Additional provider-specific parameters
 772 | ​
 773 | Response format
 774 | The client returns a completion result:
 775 | 
 776 | {
 777 | model: string, // Name of the model used
 778 | stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string,
 779 | role: "user" | "assistant",
 780 | content: {
 781 | type: "text" | "image",
 782 | text?: string,
 783 | data?: string,
 784 | mimeType?: string
 785 | }
 786 | }
 787 | ​
 788 | Example request
 789 | Here’s an example of requesting sampling from a client:
 790 | 
 791 | {
 792 | "method": "sampling/createMessage",
 793 | "params": {
 794 | "messages": [
 795 | {
 796 | "role": "user",
 797 | "content": {
 798 | "type": "text",
 799 | "text": "What files are in the current directory?"
 800 | }
 801 | }
 802 | ],
 803 | "systemPrompt": "You are a helpful file system assistant.",
 804 | "includeContext": "thisServer",
 805 | "maxTokens": 100
 806 | }
 807 | }
 808 | ​
 809 | Best practices
 810 | When implementing sampling:
 811 | 
 812 | Always provide clear, well-structured prompts
 813 | Handle both text and image content appropriately
 814 | Set reasonable token limits
 815 | Include relevant context through includeContext
 816 | Validate responses before using them
 817 | Handle errors gracefully
 818 | Consider rate limiting sampling requests
 819 | Document expected sampling behavior
 820 | Test with various model parameters
 821 | Monitor sampling costs
 822 | ​
 823 | Human in the loop controls
 824 | Sampling is designed with human oversight in mind:
 825 | 
 826 | ​
 827 | For prompts
 828 | Clients should show users the proposed prompt
 829 | Users should be able to modify or reject prompts
 830 | System prompts can be filtered or modified
 831 | Context inclusion is controlled by the client
 832 | ​
 833 | For completions
 834 | Clients should show users the completion
 835 | Users should be able to modify or reject completions
 836 | Clients can filter or modify completions
 837 | Users control which model is used
 838 | ​
 839 | Security considerations
 840 | When implementing sampling:
 841 | 
 842 | Validate all message content
 843 | Sanitize sensitive information
 844 | Implement appropriate rate limits
 845 | Monitor sampling usage
 846 | Encrypt data in transit
 847 | Handle user data privacy
 848 | Audit sampling requests
 849 | Control cost exposure
 850 | Implement timeouts
 851 | Handle model errors gracefully
 852 | ​
 853 | Common patterns
 854 | ​
 855 | Agentic workflows
 856 | Sampling enables agentic patterns like:
 857 | 
 858 | Reading and analyzing resources
 859 | Making decisions based on context
 860 | Generating structured data
 861 | Handling multi-step tasks
 862 | Providing interactive assistance
 863 | ​
 864 | Context management
 865 | Best practices for context:
 866 | 
 867 | Request minimal necessary context
 868 | Structure context clearly
 869 | Handle context size limits
 870 | Update context as needed
 871 | Clean up stale context
 872 | ​
 873 | Error handling
 874 | Robust error handling should:
 875 | 
 876 | Catch sampling failures
 877 | Handle timeout errors
 878 | Manage rate limits
 879 | Validate responses
 880 | Provide fallback behaviors
 881 | Log errors appropriately
 882 | ​
 883 | Limitations
 884 | Be aware of these limitations:
 885 | 
 886 | Sampling depends on client capabilities
 887 | Users control sampling behavior
 888 | Context size has limits
 889 | Rate limits may apply
 890 | Costs should be considered
 891 | Model availability varies
 892 | Response times vary
 893 | Not all content types supported
 894 | 
 895 | ## Roots
 896 | 
 897 | Roots
 898 | Understanding roots in MCP
 899 | 
 900 | Roots are a concept in MCP that define the boundaries where servers can operate. They provide a way for clients to inform servers about relevant resources and their locations.
 901 | 
 902 | ​
 903 | What are Roots?
 904 | A root is a URI that a client suggests a server should focus on. When a client connects to a server, it declares which roots the server should work with. While primarily used for filesystem paths, roots can be any valid URI including HTTP URLs.
 905 | 
 906 | For example, roots could be:
 907 | 
 908 | file:///home/user/projects/myapp
 909 | https://api.example.com/v1
 910 | ​
 911 | Why Use Roots?
 912 | Roots serve several important purposes:
 913 | 
 914 | Guidance: They inform servers about relevant resources and locations
 915 | Clarity: Roots make it clear which resources are part of your workspace
 916 | Organization: Multiple roots let you work with different resources simultaneously
 917 | ​
 918 | How Roots Work
 919 | When a client supports roots, it:
 920 | 
 921 | Declares the roots capability during connection
 922 | Provides a list of suggested roots to the server
 923 | Notifies the server when roots change (if supported)
 924 | While roots are informational and not strictly enforcing, servers should:
 925 | 
 926 | Respect the provided roots
 927 | Use root URIs to locate and access resources
 928 | Prioritize operations within root boundaries
 929 | ​
 930 | Common Use Cases
 931 | Roots are commonly used to define:
 932 | 
 933 | Project directories
 934 | Repository locations
 935 | API endpoints
 936 | Configuration locations
 937 | Resource boundaries
 938 | ​
 939 | Best Practices
 940 | When working with roots:
 941 | 
 942 | Only suggest necessary resources
 943 | Use clear, descriptive names for roots
 944 | Monitor root accessibility
 945 | Handle root changes gracefully
 946 | ​
 947 | Example
 948 | Here’s how a typical MCP client might expose roots:
 949 | 
 950 | {
 951 | "roots": [
 952 | {
 953 | "uri": "file:///home/user/projects/frontend",
 954 | "name": "Frontend Repository"
 955 | },
 956 | {
 957 | "uri": "https://api.example.com/v1",
 958 | "name": "API Endpoint"
 959 | }
 960 | ]
 961 | }
 962 | This configuration suggests the server focus on both a local repository and an API endpoint while keeping them logically separated.
 963 | 
 964 | ## Transports
 965 | 
 966 | Transports
 967 | Learn about MCP’s communication mechanisms
 968 | 
 969 | Transports in the Model Context Protocol (MCP) provide the foundation for communication between clients and servers. A transport handles the underlying mechanics of how messages are sent and received.
 970 | 
 971 | ​
 972 | Message Format
 973 | MCP uses JSON-RPC 2.0 as its wire format. The transport layer is responsible for converting MCP protocol messages into JSON-RPC format for transmission and converting received JSON-RPC messages back into MCP protocol messages.
 974 | 
 975 | There are three types of JSON-RPC messages used:
 976 | 
 977 | ​
 978 | Requests
 979 | 
 980 | {
 981 | jsonrpc: "2.0",
 982 | id: number | string,
 983 | method: string,
 984 | params?: object
 985 | }
 986 | ​
 987 | Responses
 988 | 
 989 | {
 990 | jsonrpc: "2.0",
 991 | id: number | string,
 992 | result?: object,
 993 | error?: {
 994 | code: number,
 995 | message: string,
 996 | data?: unknown
 997 | }
 998 | }
 999 | ​
1000 | Notifications
1001 | 
1002 | {
1003 | jsonrpc: "2.0",
1004 | method: string,
1005 | params?: object
1006 | }
1007 | ​
1008 | Built-in Transport Types
1009 | MCP includes two standard transport implementations:
1010 | 
1011 | ​
1012 | Standard Input/Output (stdio)
1013 | The stdio transport enables communication through standard input and output streams. This is particularly useful for local integrations and command-line tools.
1014 | 
1015 | Use stdio when:
1016 | 
1017 | Building command-line tools
1018 | Implementing local integrations
1019 | Needing simple process communication
1020 | Working with shell scripts
1021 | TypeScript (Server)
1022 | TypeScript (Client)
1023 | Python (Server)
1024 | Python (Client)
1025 | 
1026 | const server = new Server({
1027 | name: "example-server",
1028 | version: "1.0.0"
1029 | }, {
1030 | capabilities: {}
1031 | });
1032 | 
1033 | const transport = new StdioServerTransport();
1034 | await server.connect(transport);
1035 | ​
1036 | Server-Sent Events (SSE)
1037 | SSE transport enables server-to-client streaming with HTTP POST requests for client-to-server communication.
1038 | 
1039 | Use SSE when:
1040 | 
1041 | Only server-to-client streaming is needed
1042 | Working with restricted networks
1043 | Implementing simple updates
1044 | TypeScript (Server)
1045 | TypeScript (Client)
1046 | Python (Server)
1047 | Python (Client)
1048 | 
1049 | import express from "express";
1050 | 
1051 | const app = express();
1052 | 
1053 | const server = new Server({
1054 | name: "example-server",
1055 | version: "1.0.0"
1056 | }, {
1057 | capabilities: {}
1058 | });
1059 | 
1060 | let transport: SSEServerTransport | null = null;
1061 | 
1062 | app.get("/sse", (req, res) => {
1063 | transport = new SSEServerTransport("/messages", res);
1064 | server.connect(transport);
1065 | });
1066 | 
1067 | app.post("/messages", (req, res) => {
1068 | if (transport) {
1069 | transport.handlePostMessage(req, res);
1070 | }
1071 | });
1072 | 
1073 | app.listen(3000);
1074 | ​
1075 | Custom Transports
1076 | MCP makes it easy to implement custom transports for specific needs. Any transport implementation just needs to conform to the Transport interface:
1077 | 
1078 | You can implement custom transports for:
1079 | 
1080 | Custom network protocols
1081 | Specialized communication channels
1082 | Integration with existing systems
1083 | Performance optimization
1084 | TypeScript
1085 | Python
1086 | 
1087 | interface Transport {
1088 | // Start processing messages
1089 | start(): Promise<void>;
1090 | 
1091 | // Send a JSON-RPC message
1092 | send(message: JSONRPCMessage): Promise<void>;
1093 | 
1094 | // Close the connection
1095 | close(): Promise<void>;
1096 | 
1097 | // Callbacks
1098 | onclose?: () => void;
1099 | onerror?: (error: Error) => void;
1100 | onmessage?: (message: JSONRPCMessage) => void;
1101 | }
1102 | ​
1103 | Error Handling
1104 | Transport implementations should handle various error scenarios:
1105 | 
1106 | Connection errors
1107 | Message parsing errors
1108 | Protocol errors
1109 | Network timeouts
1110 | Resource cleanup
1111 | Example error handling:
1112 | 
1113 | TypeScript
1114 | Python
1115 | 
1116 | class ExampleTransport implements Transport {
1117 | async start() {
1118 | try {
1119 | // Connection logic
1120 | } catch (error) {
1121 | this.onerror?.(new Error(`Failed to connect: ${error}`));
1122 | throw error;
1123 | }
1124 | }
1125 | 
1126 | async send(message: JSONRPCMessage) {
1127 | try {
1128 | // Sending logic
1129 | } catch (error) {
1130 | this.onerror?.(new Error(`Failed to send message: ${error}`));
1131 | throw error;
1132 | }
1133 | }
1134 | }
1135 | ​
1136 | Best Practices
1137 | When implementing or using MCP transport:
1138 | 
1139 | Handle connection lifecycle properly
1140 | Implement proper error handling
1141 | Clean up resources on connection close
1142 | Use appropriate timeouts
1143 | Validate messages before sending
1144 | Log transport events for debugging
1145 | Implement reconnection logic when appropriate
1146 | Handle backpressure in message queues
1147 | Monitor connection health
1148 | Implement proper security measures
1149 | ​
1150 | Security Considerations
1151 | When implementing transport:
1152 | 
1153 | ​
1154 | Authentication and Authorization
1155 | Implement proper authentication mechanisms
1156 | Validate client credentials
1157 | Use secure token handling
1158 | Implement authorization checks
1159 | ​
1160 | Data Security
1161 | Use TLS for network transport
1162 | Encrypt sensitive data
1163 | Validate message integrity
1164 | Implement message size limits
1165 | Sanitize input data
1166 | ​
1167 | Network Security
1168 | Implement rate limiting
1169 | Use appropriate timeouts
1170 | Handle denial of service scenarios
1171 | Monitor for unusual patterns
1172 | Implement proper firewall rules
1173 | ​
1174 | Debugging Transport
1175 | Tips for debugging transport issues:
1176 | 
1177 | Enable debug logging
1178 | Monitor message flow
1179 | Check connection states
1180 | Validate message formats
1181 | Test error scenarios
1182 | Use network analysis tools
1183 | Implement health checks
1184 | Monitor resource usage
1185 | Test edge cases
1186 | Use proper error tracking
1187 | 
```

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

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

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

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

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

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