chrometools-mcp 1.9.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,13 +8,13 @@ MCP server for Chrome automation using Puppeteer with persistent browser session
8
8
  - [Usage](#usage)
9
9
  - [AI Optimization Features](#ai-optimization-features) ⭐ **NEW**
10
10
  - [Scenario Recorder](#scenario-recorder) ⭐ **NEW** - Visual UI-based recording with smart optimization
11
- - [Available Tools](#available-tools) - **39+ Tools Total**
11
+ - [Available Tools](#available-tools) - **40+ Tools Total**
12
12
  - [AI-Powered Tools](#ai-powered-tools) ⭐ **NEW** - smartFindElement, analyzePage, getAllInteractiveElements, findElementsByText
13
13
  - [Core Tools](#1-core-tools) - ping, openBrowser
14
14
  - [Interaction Tools](#2-interaction-tools) - click, type, scrollTo
15
15
  - [Inspection Tools](#3-inspection-tools) - getElement, getComputedCss, getBoxModel, screenshot
16
16
  - [Advanced Tools](#4-advanced-tools) - executeScript, getConsoleLogs, listNetworkRequests, getNetworkRequest, filterNetworkRequests, hover, setStyles, setViewport, getViewport, navigateTo
17
- - [Recorder Tools](#6-recorder-tools) ⭐ **NEW** - enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode
17
+ - [Recorder Tools](#6-recorder-tools) ⭐ **NEW** - enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode, appendScenarioToFile, generatePageObject
18
18
  - [Typical Workflow Example](#typical-workflow-example)
19
19
  - [Tool Usage Tips](#tool-usage-tips)
20
20
  - [Configuration](#configuration)
@@ -506,56 +506,74 @@ Extract detailed design specifications from Figma including text content, colors
506
506
 
507
507
  ### 6. Recorder Tools ⭐ NEW
508
508
 
509
- **Directory Management**: All recorder tools support an optional `directory` parameter to specify where scenarios are stored.
509
+ **URL-Based Storage (v2.1+)**: Scenarios are automatically organized by website domain in `~/.config/chrometools-mcp/projects/{domain}/scenarios/`.
510
510
 
511
- **Default Location**: `~/.config/chrometools-mcp` (consistent, predictable location in user's home folder)
511
+ **Automatic Domain Detection**: Project ID is extracted from the URL where recording starts:
512
+ - `https://www.google.com` → `google`
513
+ - `https://dev.example.com:8080` → `example-8080`
514
+ - `http://localhost:3000` → `localhost-3000`
515
+ - `file:///test.html` → `local`
512
516
 
513
- You can override the default by:
514
- - Passing explicit `directory` parameter to any recorder tool
515
- - Setting environment variable: `CLAUDE_PROJECT_DIR` or `PROJECT_DIR`
517
+ **Domain Organization Rules**:
518
+ 1. Main domain only (subdomains stripped): `mail.google.com` `google`
519
+ 2. Ports included for ALL domains: `example.com:8080` `example-8080`
520
+ 3. Protocol ignored: `http` and `https` both → same project
516
521
 
517
- Once a directory is set (explicitly or via environment), it's remembered for the entire MCP server session.
522
+ **Global Scenario Access**: All tools (`listScenarios`, `searchScenarios`) return scenarios from **all projects**. Agent can filter by:
523
+ - `projectId`: Domain-based identifier (e.g., "google", "localhost-3000")
524
+ - `entryUrl`: URL where recording started
525
+ - `exitUrl`: URL where recording ended
518
526
 
519
527
  **Example**:
520
528
  ```javascript
521
- // Use default location (recommended)
522
- enableRecorder() // Saves to ~/.config/chrometools-mcp
523
-
524
- // Or specify explicitly
525
- enableRecorder({ directory: "/path/to/project" })
526
-
527
- // Later calls reuse the same directory automatically
528
- executeScenario({ name: "test" }) // Uses remembered directory
529
+ // Record scenario on google.com
530
+ enableRecorder() // Saves to ~/.config/chrometools-mcp/projects/google/scenarios/
531
+
532
+ // List ALL scenarios from all websites
533
+ listScenarios()
534
+ // Returns: [
535
+ // { name: "search", projectId: "google", entryUrl: "https://google.com" },
536
+ // { name: "login", projectId: "localhost-3000", entryUrl: "http://localhost:3000" }
537
+ // ]
538
+
539
+ // Agent filters by projectId or URL
540
+ scenarios.filter(s => s.projectId === "google")
541
+ scenarios.filter(s => s.entryUrl.includes("localhost"))
542
+
543
+ // Execute scenario (searches all projects automatically)
544
+ executeScenario({ name: "login" }) // Finds scenario in any project
529
545
  ```
530
546
 
531
547
  ---
532
548
 
533
549
  #### enableRecorder
534
- Inject visual recorder UI widget into the current page.
535
- - **Parameters**:
536
- - `directory` (optional): Directory to save scenarios (defaults to `~/.config/chrometools-mcp`)
550
+ Inject visual recorder UI widget into the current page. Scenarios are automatically saved to `~/.config/chrometools-mcp/projects/{domain}/scenarios/` based on the website URL.
551
+ - **Parameters**: None
537
552
  - **Use case**: Start recording user interactions visually
538
- - **Returns**: Success status
553
+ - **Returns**: Success status with storage location
539
554
  - **Features**:
540
555
  - Floating widget with compact mode (minimize to 50x50px)
541
556
  - Visual recording indicator (red pulsing border)
542
557
  - Start/Pause/Stop/Stop & Save/Clear controls
543
558
  - Real-time action list display
544
559
  - Metadata fields (name, description, tags)
560
+ - Automatic domain-based project detection from URL
545
561
 
546
562
  #### executeScenario
547
- Execute a previously recorded scenario by name.
563
+ Execute a previously recorded scenario by name. Searches all projects automatically via global index.
548
564
  - **Parameters**:
549
565
  - `name` (required): Scenario name
566
+ - `projectId` (optional): Project ID (domain) to disambiguate when multiple scenarios have the same name. Examples: `"google"`, `"localhost-3000"`
550
567
  - `parameters` (optional): Runtime parameters (e.g., { email: "user@test.com" })
551
568
  - `executeDependencies` (optional): Execute dependencies before running scenario (default: true)
552
- - `directory` (optional): Directory where scenarios are stored (defaults to `~/.config/chrometools-mcp`)
553
- - **Use case**: Run automated test scenarios
569
+ - **Use case**: Run automated test scenarios across projects
554
570
  - **Returns**: Execution result with success/failure status
555
571
  - **Features**:
556
572
  - Automatic dependency resolution (enabled by default)
573
+ - Cross-project dependency support
557
574
  - Secret parameter injection
558
575
  - Fallback selector retry logic
576
+ - Name collision detection with helpful error messages
559
577
  - **Example**:
560
578
  ```javascript
561
579
  // Execute with dependencies (default)
@@ -563,73 +581,134 @@ Execute a previously recorded scenario by name.
563
581
 
564
582
  // Execute without dependencies
565
583
  executeScenario({ name: "create_post", executeDependencies: false })
584
+
585
+ // Disambiguate when multiple scenarios have same name
586
+ executeScenario({ name: "login", projectId: "google" })
587
+ executeScenario({ name: "login", projectId: "localhost-3000" })
588
+ ```
589
+
590
+ - **Name Collision Handling**:
591
+ If multiple scenarios with the same name exist across different projects, you'll get an error:
592
+ ```json
593
+ {
594
+ "success": false,
595
+ "error": "Multiple scenarios named 'login' found. Please specify projectId.",
596
+ "availableProjectIds": ["google", "localhost-3000"],
597
+ "hint": "Use: executeScenario({ name: \"login\", projectId: \"one-of-the-above\" })"
598
+ }
566
599
  ```
567
600
 
568
601
  #### listScenarios
569
- Get all available scenarios with metadata.
570
- - **Parameters**:
571
- - `directory` (optional): Directory where scenarios are stored (defaults to `~/.config/chrometools-mcp`)
572
- - **Use case**: Browse recorded scenarios
573
- - **Returns**: Array of scenarios with names, descriptions, tags, timestamps
602
+ Get all available scenarios with metadata from **all websites**. Agent can filter by `projectId`, `entryUrl`, or `exitUrl`.
603
+ - **Parameters**: None
604
+ - **Use case**: Browse recorded scenarios across all websites
605
+ - **Returns**: Array of scenarios with names, descriptions, tags, timestamps, `projectId`, `entryUrl`, `exitUrl`
606
+ - **Example**:
607
+ ```javascript
608
+ // List all scenarios from all websites
609
+ const scenarios = await listScenarios()
610
+
611
+ // Agent filters by projectId
612
+ const googleScenarios = scenarios.filter(s => s.projectId === "google")
613
+
614
+ // Agent filters by URL
615
+ const localhostScenarios = scenarios.filter(s => s.entryUrl.includes("localhost"))
616
+ ```
574
617
 
575
618
  #### searchScenarios
576
- Search scenarios by text or tags.
619
+ Search scenarios by text or tags across **all websites**. Agent can further filter results by `projectId` or URLs.
577
620
  - **Parameters**:
578
621
  - `text` (optional): Search in name/description
579
622
  - `tags` (optional): Array of tags to filter
580
- - `directory` (optional): Directory where scenarios are stored (defaults to `~/.config/chrometools-mcp`)
581
- - **Use case**: Find specific scenarios
582
- - **Returns**: Matching scenarios
623
+ - **Use case**: Find specific scenarios across all websites
624
+ - **Returns**: Matching scenarios with `projectId`, `entryUrl`, `exitUrl` metadata
625
+ - **Example**:
626
+ ```javascript
627
+ // Search across all websites
628
+ const results = await searchScenarios({ text: "login" })
629
+
630
+ // Search by tags
631
+ const authScenarios = await searchScenarios({ tags: ["auth"] })
632
+
633
+ // Agent filters results by domain
634
+ const googleLogins = results.filter(s => s.projectId === "google")
635
+ ```
583
636
 
584
637
  #### getScenarioInfo
585
- Get detailed information about a scenario.
638
+ Get detailed information about a scenario. Searches all projects automatically.
586
639
  - **Parameters**:
587
640
  - `name` (required): Scenario name
588
641
  - `includeSecrets` (optional): Include secret values (default: false)
589
- - `directory` (optional): Directory where scenarios are stored (defaults to `~/.config/chrometools-mcp`)
590
642
  - **Use case**: Inspect scenario actions and dependencies
591
- - **Returns**: Full scenario details (actions, metadata, dependencies)
643
+ - **Returns**: Full scenario details (actions, metadata, dependencies, project info)
592
644
 
593
645
  #### deleteScenario
594
- Delete a scenario and its associated secrets.
646
+ Delete a scenario and its associated secrets. Searches all projects to find the scenario.
595
647
  - **Parameters**:
596
648
  - `name` (required): Scenario name
597
- - `directory` (optional): Directory where scenarios are stored (defaults to `~/.config/chrometools-mcp`)
598
649
  - **Use case**: Clean up unused scenarios
599
650
  - **Returns**: Success confirmation
600
651
 
601
652
  #### exportScenarioAsCode ⭐ **NEW**
602
- Export recorded scenario as executable test code for various frameworks. Automatically cleans unstable selectors (CSS Modules, styled-components, Emotion).
653
+ Export recorded scenario as executable test code for creating a **NEW** test file. Automatically cleans unstable selectors (CSS Modules, styled-components, Emotion). Optionally generates Page Object class. Returns JSON with code and suggested filename - Claude Code will create the file. To add tests to **EXISTING** files, use `appendScenarioToFile` instead.
603
654
 
604
655
  - **Parameters**:
605
656
  - `scenarioName` (required): Name of scenario to export
606
657
  - `language` (required): Target framework - `"playwright-typescript"`, `"playwright-python"`, `"selenium-python"`, `"selenium-java"`
607
658
  - `cleanSelectors` (optional): Remove unstable CSS classes (default: true)
608
659
  - `includeComments` (optional): Include descriptive comments (default: true)
609
- - `directory` (optional): Directory where scenarios are stored (defaults to `~/.config/chrometools-mcp`)
660
+ - `generatePageObject` (optional): Also generate Page Object class for the page (default: false)
661
+ - `pageObjectClassName` (optional): Custom Page Object class name (auto-generated if not provided)
610
662
 
611
- - **Use case**: Convert recorded scenarios into maintainable test code
663
+ - **Use case**: Create new test files from recorded scenarios with optional Page Objects
612
664
 
613
- - **Returns**: Generated test code as string
665
+ - **Returns**: JSON with:
666
+ - `action`: `"create_new_file"`
667
+ - `suggestedFileName`: Suggested test filename
668
+ - `testCode`: Full test code with imports
669
+ - `instruction`: Instructions for Claude Code
670
+ - `pageObject` (if `generatePageObject=true`): Page Object code and metadata
614
671
 
615
- - **Example**:
672
+ - **Example 1 - Test only**:
616
673
  ```javascript
617
- // Export scenario as Playwright TypeScript
674
+ // Export scenario as new Playwright TypeScript file
618
675
  exportScenarioAsCode({
619
676
  scenarioName: "checkout_flow",
677
+ language: "playwright-typescript"
678
+ })
679
+
680
+ // Returns JSON:
681
+ {
682
+ "action": "create_new_file",
683
+ "suggestedFileName": "checkout_flow.spec.ts",
684
+ "testCode": "import { test, expect } from '@playwright/test';\n\ntest('checkout_flow', async ({ page }) => {\n await page.goto('https://example.com');\n await page.locator('button[data-testid=\"add-to-cart\"]').click();\n await expect(page).toHaveURL(/checkout/);\n});",
685
+ "instruction": "Create a new test file 'checkout_flow.spec.ts' with the testCode."
686
+ }
687
+ ```
688
+
689
+ - **Example 2 - Test + Page Object**:
690
+ ```javascript
691
+ // Export with Page Object class
692
+ exportScenarioAsCode({
693
+ scenarioName: "login_test",
620
694
  language: "playwright-typescript",
621
- cleanSelectors: true,
622
- includeComments: true
695
+ generatePageObject: true,
696
+ pageObjectClassName: "LoginPage"
623
697
  })
624
698
 
625
- // Returns clean test code:
626
- // import { test, expect } from '@playwright/test';
627
- //
628
- // test('checkout_flow', async ({ page }) => {
629
- // await page.goto('https://example.com');
630
- // await page.locator('button[data-testid="add-to-cart"]').click();
631
- // await expect(page).toHaveURL(/checkout/);
632
- // });
699
+ // Returns JSON with both files:
700
+ {
701
+ "action": "create_new_file",
702
+ "suggestedFileName": "login_test.spec.ts",
703
+ "testCode": "import { test } from '@playwright/test';\nimport { LoginPage } from './LoginPage';\n\ntest('login_test', async ({ page }) => {\n const loginPage = new LoginPage(page);\n await loginPage.goto();\n await loginPage.fillEmailInput('user@test.com');\n await loginPage.clickLoginButton();\n});",
704
+ "pageObject": {
705
+ "code": "import { Page, Locator } from '@playwright/test';\n\nexport class LoginPage { ... }",
706
+ "className": "LoginPage",
707
+ "suggestedFileName": "LoginPage.ts",
708
+ "elementCount": 12
709
+ },
710
+ "instruction": "Create a new test file 'login_test.spec.ts' with the testCode. Also create a Page Object file 'LoginPage.ts' with the pageObject.code."
711
+ }
633
712
  ```
634
713
 
635
714
  - **Selector Cleaning**: Automatically removes unstable patterns:
@@ -639,6 +718,153 @@ Export recorded scenario as executable test code for various frameworks. Automat
639
718
  - Hash suffixes: `component_a1b2c3d` → removed
640
719
  - Prefers stable selectors: `data-testid`, `role`, `aria-label`, semantic attributes
641
720
 
721
+ #### appendScenarioToFile ⭐ **NEW v2.3.0**
722
+ Append recorded scenario as test code to an **EXISTING** test file. Automatically cleans unstable selectors (CSS Modules, styled-components, Emotion). Optionally generates Page Object class. Returns JSON with test code (without imports) - Claude Code will read the file, append the test, and write back. To create **NEW** test files, use `exportScenarioAsCode` instead.
723
+
724
+ - **Parameters**:
725
+ - `scenarioName` (required): Name of scenario to export
726
+ - `language` (required): Target framework - `"playwright-typescript"`, `"playwright-python"`, `"selenium-python"`, `"selenium-java"`
727
+ - `targetFile` (required): Path to existing test file to append to
728
+ - `testName` (optional): Override test name (default: from scenario name)
729
+ - `insertPosition` (optional): Where to insert: `'end'` (default), `'before'`, `'after'`
730
+ - `referenceTestName` (optional): Reference test name for 'before'/'after' insertion
731
+ - `cleanSelectors` (optional): Remove unstable CSS classes (default: true)
732
+ - `includeComments` (optional): Include descriptive comments (default: true)
733
+ - `generatePageObject` (optional): Also generate Page Object class for the page (default: false)
734
+ - `pageObjectClassName` (optional): Custom Page Object class name (auto-generated if not provided)
735
+
736
+ - **Use case**: Add tests to existing test files without overwriting current tests
737
+
738
+ - **Architecture**: MCP server generates only test code (without imports). Claude Code reads the target file, appends the test at the specified position, and writes the file back. This separation ensures MCP doesn't need file system access to test files.
739
+
740
+ - **Returns**: JSON with:
741
+ - `action`: `"append_test"`
742
+ - `targetFile`: Path to file to update
743
+ - `testCode`: Test code only (without imports/headers)
744
+ - `testName`: Name of test to append
745
+ - `insertPosition`: Where to insert test
746
+ - `referenceTestName`: Reference test for 'before'/'after' positioning
747
+ - `instruction`: Instructions for Claude Code to read/append/write
748
+ - `pageObject` (if `generatePageObject=true`): Page Object code and metadata
749
+
750
+ - **Example 1 - Append to end**:
751
+ ```javascript
752
+ // Append test to end of existing file
753
+ appendScenarioToFile({
754
+ scenarioName: "new_feature_test",
755
+ language: "playwright-typescript",
756
+ targetFile: "./tests/features.spec.ts"
757
+ })
758
+
759
+ // Returns JSON:
760
+ {
761
+ "action": "append_test",
762
+ "targetFile": "./tests/features.spec.ts",
763
+ "testCode": "test('new_feature_test', async ({ page }) => {\n // Test implementation\n await page.click('#submit');\n await expect(page.locator('.result')).toBeVisible();\n});",
764
+ "testName": "new_feature_test",
765
+ "insertPosition": "end",
766
+ "referenceTestName": null,
767
+ "instruction": "Read file './tests/features.spec.ts', append the testCode at position 'end', then write the file back."
768
+ }
769
+ ```
770
+
771
+ - **Example 2 - Insert before specific test**:
772
+ ```javascript
773
+ // Insert test before specific test
774
+ appendScenarioToFile({
775
+ scenarioName: "setup_test",
776
+ language: "selenium-python",
777
+ targetFile: "./tests/test_suite.py",
778
+ insertPosition: "before",
779
+ referenceTestName: "test_main",
780
+ testName: "test_setup_data"
781
+ })
782
+ ```
783
+
784
+ - **Example 3 - Append with Page Object**:
785
+ ```javascript
786
+ // Append test and generate Page Object
787
+ appendScenarioToFile({
788
+ scenarioName: "login_test",
789
+ language: "playwright-typescript",
790
+ targetFile: "./tests/auth.spec.ts",
791
+ generatePageObject: true,
792
+ pageObjectClassName: "LoginPage"
793
+ })
794
+
795
+ // Returns JSON with both test code and Page Object:
796
+ {
797
+ "action": "append_test",
798
+ "targetFile": "./tests/auth.spec.ts",
799
+ "testCode": "test('login_test', async ({ page }) => {\n await page.fill('#username', 'user');\n await page.fill('#password', 'pass');\n await page.click('button[type=\"submit\"]');\n});",
800
+ "testName": "login_test",
801
+ "insertPosition": "end",
802
+ "referenceTestName": null,
803
+ "pageObject": {
804
+ "code": "export class LoginPage { ... }",
805
+ "className": "LoginPage",
806
+ "suggestedFileName": "LoginPage.ts",
807
+ "elementCount": 8
808
+ },
809
+ "instruction": "Read file './tests/auth.spec.ts', append the testCode at position 'end', then write the file back. Also create a Page Object file 'LoginPage.ts' with the provided pageObject.code."
810
+ }
811
+ ```
812
+
813
+ #### generatePageObject ⭐ **NEW**
814
+ Generate Page Object Model (POM) class from current page structure. Analyzes page, extracts interactive elements, and generates framework-specific code with smart naming and helper methods.
815
+
816
+ - **Parameters**:
817
+ - `className` (optional): Page Object class name (auto-generated from page title/URL if not provided)
818
+ - `framework` (optional): Target framework - `"playwright-typescript"` (default), `"playwright-python"`, `"selenium-python"`, `"selenium-java"`
819
+ - `includeComments` (optional): Include descriptive comments (default: true)
820
+ - `groupElements` (optional): Group elements by page sections (default: true)
821
+
822
+ - **Features**:
823
+ - **Smart Selector Generation**: Prioritizes id > name > data-testid > unique class > CSS path
824
+ - **Intelligent Naming**: Auto-generates element names from labels, placeholders, text, attributes
825
+ - **Section Grouping**: Groups elements by semantic sections (header, nav, form, footer, main, etc.)
826
+ - **Helper Methods**: Auto-generates fill() and click() methods for common actions
827
+ - **Multi-Framework**: Supports Playwright (TS/Python) and Selenium (Python/Java)
828
+
829
+ - **Use cases**:
830
+ - Generate POM classes for test automation
831
+ - Create maintainable test structure from existing pages
832
+ - Bootstrap test framework setup quickly
833
+ - Extract page structure for documentation
834
+
835
+ - **Returns**: Page Object code with metadata (className, url, title, elementCount, framework)
836
+
837
+ - **Example**:
838
+ ```javascript
839
+ // 1. Navigate to page
840
+ openBrowser({ url: "https://example.com/login" })
841
+
842
+ // 2. Generate Page Object
843
+ generatePageObject({
844
+ className: "LoginPage",
845
+ framework: "playwright-typescript",
846
+ includeComments: true,
847
+ groupElements: true
848
+ })
849
+
850
+ // Returns:
851
+ {
852
+ "success": true,
853
+ "className": "LoginPage",
854
+ "url": "https://example.com/login",
855
+ "title": "Login - Example Site",
856
+ "elementCount": 12,
857
+ "framework": "playwright-typescript",
858
+ "code": "import { Page, Locator } from '@playwright/test';\n\nexport class LoginPage {\n readonly page: Page;\n \n /** Email input field */\n readonly emailInput: Locator;\n /** Password input field */\n readonly passwordInput: Locator;\n /** Login button */\n readonly loginButton: Locator;\n \n constructor(page: Page) {\n this.page = page;\n this.emailInput = page.locator('#email');\n this.passwordInput = page.locator('#password');\n this.loginButton = page.locator('button[type=\"submit\"]');\n }\n \n async goto() {\n await this.page.goto('https://example.com/login');\n }\n \n async fillEmailInput(text: string) {\n await this.emailInput.fill(text);\n }\n \n async fillPasswordInput(text: string) {\n await this.passwordInput.fill(text);\n }\n \n async clickLoginButton() {\n await this.loginButton.click();\n }\n}"
859
+ }
860
+ ```
861
+
862
+ - **Supported Frameworks**:
863
+ - `playwright-typescript`: Playwright with TypeScript (locators, async/await, Page Object pattern)
864
+ - `playwright-python`: Playwright with Python (sync API, snake_case naming)
865
+ - `selenium-python`: Selenium with Python (WebDriver, explicit waits, By locators)
866
+ - `selenium-java`: Selenium with Java (WebDriver, Page Factory compatible)
867
+
642
868
  ---
643
869
 
644
870
  ## Typical Workflow Example
@@ -0,0 +1,206 @@
1
+ /**
2
+ * browser/browser-manager.js
3
+ *
4
+ * Browser lifecycle management
5
+ */
6
+
7
+ import puppeteer from 'puppeteer';
8
+ import { spawn } from 'child_process';
9
+ import http from 'http';
10
+ import { getChromePath, getTempDir, isWSL, CHROME_DEBUG_PORT } from '../utils/platform-utils.js';
11
+
12
+ // Global browser instance (persists between requests)
13
+ let browserPromise = null;
14
+ let chromeProcess = null;
15
+
16
+ /**
17
+ * Debug log helper (only logs to stderr when DEBUG=1)
18
+ * @param {...any} args - Arguments to log
19
+ */
20
+ function debugLog(...args) {
21
+ if (process.env.DEBUG === '1') {
22
+ console.error('[browser-manager]', ...args);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Get WebSocket endpoint from Chrome remote debugging
28
+ * @param {number} port - Chrome debugging port
29
+ * @param {number} maxRetries - Maximum retry attempts
30
+ * @returns {Promise<string>} - WebSocket debugger URL
31
+ */
32
+ async function getChromeWebSocketEndpoint(port = CHROME_DEBUG_PORT, maxRetries = 10) {
33
+ for (let i = 0; i < maxRetries; i++) {
34
+ try {
35
+ const response = await new Promise((resolve, reject) => {
36
+ const req = http.get(`http://localhost:${port}/json/version`, (res) => {
37
+ let data = '';
38
+ res.on('data', chunk => data += chunk);
39
+ res.on('end', () => resolve(data));
40
+ });
41
+ req.on('error', reject);
42
+ req.setTimeout(1000);
43
+ });
44
+
45
+ const info = JSON.parse(response);
46
+ if (info.webSocketDebuggerUrl) {
47
+ return info.webSocketDebuggerUrl;
48
+ }
49
+ } catch (err) {
50
+ // Chrome might not be ready yet, wait and retry
51
+ await new Promise(resolve => setTimeout(resolve, 500));
52
+ }
53
+ }
54
+ throw new Error('Could not get Chrome WebSocket endpoint after multiple retries');
55
+ }
56
+
57
+ /**
58
+ * Initialize browser (singleton)
59
+ * @returns {Promise<Browser>} - Puppeteer browser instance
60
+ */
61
+ export async function getBrowser() {
62
+ // Check if we have a cached browser and if it's still connected
63
+ if (browserPromise) {
64
+ try {
65
+ const cachedBrowser = await browserPromise;
66
+ if (cachedBrowser && cachedBrowser.isConnected()) {
67
+ return cachedBrowser;
68
+ }
69
+ // Browser disconnected, reset the promise
70
+ debugLog("Browser disconnected, will reconnect...");
71
+ browserPromise = null;
72
+ } catch (error) {
73
+ debugLog("Error checking cached browser:", error.message);
74
+ browserPromise = null;
75
+ }
76
+ }
77
+
78
+ if (!browserPromise) {
79
+ browserPromise = (async () => {
80
+ try {
81
+ let browser;
82
+ let endpoint;
83
+
84
+ // Try to connect to existing Chrome with remote debugging
85
+ try {
86
+ endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 2);
87
+ browser = await puppeteer.connect({
88
+ browserWSEndpoint: endpoint,
89
+ defaultViewport: null,
90
+ });
91
+ debugLog("Connected to existing Chrome instance");
92
+ debugLog("WebSocket endpoint:", endpoint);
93
+
94
+ // Set up disconnect handler to reset browserPromise
95
+ browser.on('disconnected', () => {
96
+ debugLog("Browser disconnected");
97
+ browserPromise = null;
98
+ });
99
+
100
+ return browser;
101
+ } catch (connectError) {
102
+ debugLog("No existing Chrome found, launching new instance...");
103
+ }
104
+
105
+ // Launch new Chrome with remote debugging enabled
106
+ const chromePath = getChromePath();
107
+ const userDataDir = `${getTempDir()}/chrome-mcp-profile`;
108
+
109
+ debugLog("Chrome path:", chromePath);
110
+ debugLog("User data dir:", userDataDir);
111
+
112
+ chromeProcess = spawn(chromePath, [
113
+ `--remote-debugging-port=${CHROME_DEBUG_PORT}`,
114
+ '--no-first-run',
115
+ '--no-default-browser-check',
116
+ `--user-data-dir=${userDataDir}`,
117
+ ], {
118
+ detached: true,
119
+ stdio: 'ignore',
120
+ });
121
+
122
+ chromeProcess.unref(); // Allow Node to exit even if Chrome is running
123
+
124
+ debugLog("Chrome launched with remote debugging on port", CHROME_DEBUG_PORT);
125
+
126
+ // Wait for Chrome to start and get the endpoint
127
+ endpoint = await getChromeWebSocketEndpoint(CHROME_DEBUG_PORT, 20);
128
+
129
+ // Connect to the Chrome instance
130
+ browser = await puppeteer.connect({
131
+ browserWSEndpoint: endpoint,
132
+ defaultViewport: null,
133
+ });
134
+
135
+ debugLog("Connected to Chrome instance");
136
+ debugLog("WebSocket endpoint:", endpoint);
137
+
138
+ // Set up disconnect handler to reset browserPromise
139
+ browser.on('disconnected', () => {
140
+ debugLog("Browser disconnected");
141
+ browserPromise = null;
142
+ });
143
+
144
+ return browser;
145
+ } catch (error) {
146
+ // Check if it's a display-related error in WSL
147
+ if (isWSL && (
148
+ error.message.includes('DISPLAY') ||
149
+ error.message.includes('connect ECONNREFUSED') ||
150
+ error.message.includes('cannot open display')
151
+ )) {
152
+ const helpMessage = `
153
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
154
+ ❌ WSL X Server Error Detected
155
+
156
+ You are running in WSL environment with headless:false mode.
157
+ This requires an X server to display the browser GUI.
158
+
159
+ 🔧 Solution:
160
+ 1. Start X server on Windows (e.g., VcXsrv, X410)
161
+ 2. Set DISPLAY in your MCP config:
162
+
163
+ {
164
+ "mcpServers": {
165
+ "chrometools": {
166
+ "env": {
167
+ "DISPLAY": "172.25.96.1:0"
168
+ }
169
+ }
170
+ }
171
+ }
172
+
173
+ 📚 For detailed setup instructions, see:
174
+ WSL_SETUP.md in chrometools-mcp package
175
+
176
+ 💡 Alternative: Run in headless mode (modify index.js)
177
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
178
+ `;
179
+ console.error(helpMessage);
180
+ throw new Error(`WSL X Server not available. ${error.message}\n\nSee above for setup instructions.`);
181
+ }
182
+
183
+ // Re-throw other errors as-is
184
+ throw error;
185
+ }
186
+ })();
187
+ }
188
+
189
+ return await browserPromise;
190
+ }
191
+
192
+ /**
193
+ * Close browser and cleanup
194
+ * @returns {Promise<void>}
195
+ */
196
+ export async function closeBrowser() {
197
+ if (browserPromise) {
198
+ try {
199
+ const browser = await browserPromise;
200
+ await browser.close();
201
+ } catch (error) {
202
+ debugLog("Error closing browser:", error.message);
203
+ }
204
+ browserPromise = null;
205
+ }
206
+ }