chrometools-mcp 1.8.2 → 2.2.0

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
@@ -14,7 +14,7 @@ MCP server for Chrome automation using Puppeteer with persistent browser session
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, generatePageObject
18
18
  - [Typical Workflow Example](#typical-workflow-example)
19
19
  - [Tool Usage Tips](#tool-usage-tips)
20
20
  - [Configuration](#configuration)
@@ -506,54 +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. If not provided, the directory is auto-detected using this cascade:
510
- 1. `CLAUDE_PROJECT_DIR` environment variable (set by Claude Code)
511
- 2. `PROJECT_DIR` environment variable (custom)
512
- 3. Git repository root (detected via `git rev-parse --show-toplevel`)
513
- 4. Current working directory (fallback)
509
+ **URL-Based Storage (v2.1+)**: Scenarios are automatically organized by website domain in `~/.config/chrometools-mcp/projects/{domain}/scenarios/`.
514
510
 
515
- Once a directory is set (explicitly or auto-detected), it's remembered for the entire MCP server session.
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`
516
516
 
517
- **Example**:
518
- ```javascript
519
- // Let it auto-detect (recommended)
520
- enableRecorder()
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
521
521
 
522
- // Or specify explicitly
523
- enableRecorder({ directory: "/path/to/project" })
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
524
526
 
525
- // Later calls reuse the same directory automatically
526
- executeScenario({ name: "test" }) // Uses remembered directory
527
+ **Example**:
528
+ ```javascript
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
527
545
  ```
528
546
 
529
547
  ---
530
548
 
531
549
  #### enableRecorder
532
- Inject visual recorder UI widget into the current page.
533
- - **Parameters**:
534
- - `directory` (optional): Directory to save scenarios (auto-detected if not provided)
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
535
552
  - **Use case**: Start recording user interactions visually
536
- - **Returns**: Success status
553
+ - **Returns**: Success status with storage location
537
554
  - **Features**:
538
555
  - Floating widget with compact mode (minimize to 50x50px)
539
556
  - Visual recording indicator (red pulsing border)
540
557
  - Start/Pause/Stop/Stop & Save/Clear controls
541
558
  - Real-time action list display
542
559
  - Metadata fields (name, description, tags)
560
+ - Automatic domain-based project detection from URL
543
561
 
544
562
  #### executeScenario
545
- Execute a previously recorded scenario by name.
563
+ Execute a previously recorded scenario by name. Searches all projects automatically via global index.
546
564
  - **Parameters**:
547
565
  - `name` (required): Scenario name
566
+ - `projectId` (optional): Project ID (domain) to disambiguate when multiple scenarios have the same name. Examples: `"google"`, `"localhost-3000"`
548
567
  - `parameters` (optional): Runtime parameters (e.g., { email: "user@test.com" })
549
568
  - `executeDependencies` (optional): Execute dependencies before running scenario (default: true)
550
- - `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
551
- - **Use case**: Run automated test scenarios
569
+ - **Use case**: Run automated test scenarios across projects
552
570
  - **Returns**: Execution result with success/failure status
553
571
  - **Features**:
554
572
  - Automatic dependency resolution (enabled by default)
573
+ - Cross-project dependency support
555
574
  - Secret parameter injection
556
575
  - Fallback selector retry logic
576
+ - Name collision detection with helpful error messages
557
577
  - **Example**:
558
578
  ```javascript
559
579
  // Execute with dependencies (default)
@@ -561,56 +581,96 @@ Execute a previously recorded scenario by name.
561
581
 
562
582
  // Execute without dependencies
563
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
+ }
564
599
  ```
565
600
 
566
601
  #### listScenarios
567
- Get all available scenarios with metadata.
568
- - **Parameters**:
569
- - `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
570
- - **Use case**: Browse recorded scenarios
571
- - **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
+ ```
572
617
 
573
618
  #### searchScenarios
574
- 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.
575
620
  - **Parameters**:
576
621
  - `text` (optional): Search in name/description
577
622
  - `tags` (optional): Array of tags to filter
578
- - `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
579
- - **Use case**: Find specific scenarios
580
- - **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
+ ```
581
636
 
582
637
  #### getScenarioInfo
583
- Get detailed information about a scenario.
638
+ Get detailed information about a scenario. Searches all projects automatically.
584
639
  - **Parameters**:
585
640
  - `name` (required): Scenario name
586
641
  - `includeSecrets` (optional): Include secret values (default: false)
587
- - `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
588
642
  - **Use case**: Inspect scenario actions and dependencies
589
- - **Returns**: Full scenario details (actions, metadata, dependencies)
643
+ - **Returns**: Full scenario details (actions, metadata, dependencies, project info)
590
644
 
591
645
  #### deleteScenario
592
- Delete a scenario and its associated secrets.
646
+ Delete a scenario and its associated secrets. Searches all projects to find the scenario.
593
647
  - **Parameters**:
594
648
  - `name` (required): Scenario name
595
- - `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
596
649
  - **Use case**: Clean up unused scenarios
597
650
  - **Returns**: Success confirmation
598
651
 
599
652
  #### exportScenarioAsCode ⭐ **NEW**
600
- 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 various frameworks. Automatically cleans unstable selectors (CSS Modules, styled-components, Emotion). Optionally generates Page Object class for the page. Can append tests to existing files. Searches all projects to find the scenario.
601
654
 
602
655
  - **Parameters**:
603
656
  - `scenarioName` (required): Name of scenario to export
604
657
  - `language` (required): Target framework - `"playwright-typescript"`, `"playwright-python"`, `"selenium-python"`, `"selenium-java"`
605
658
  - `cleanSelectors` (optional): Remove unstable CSS classes (default: true)
606
659
  - `includeComments` (optional): Include descriptive comments (default: true)
607
- - `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
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)
662
+ - `appendToFile` (optional): Path to existing test file to append to (enables **append mode**) ⭐ **NEW**
663
+ - `testName` (optional): Override test name (default: from scenario name) ⭐ **NEW**
664
+ - `insertPosition` (optional): Where to insert: `'end'` (default), `'before'`, `'after'` ⭐ **NEW**
665
+ - `referenceTestName` (optional): Reference test name for before/after insertion ⭐ **NEW**
608
666
 
609
- - **Use case**: Convert recorded scenarios into maintainable test code
667
+ - **Use case**: Convert recorded scenarios into maintainable test code with optional Page Objects, or append to existing test suites
610
668
 
611
- - **Returns**: Generated test code as string
669
+ - **Returns**:
670
+ - **Without `appendToFile`**: Test code as string (or JSON with Page Object)
671
+ - **With `appendToFile`**: JSON with `{success, mode: "append", file, testName, message}`
612
672
 
613
- - **Example**:
673
+ - **Example 1 - Test only** (default behavior):
614
674
  ```javascript
615
675
  // Export scenario as Playwright TypeScript
616
676
  exportScenarioAsCode({
@@ -630,6 +690,69 @@ Export recorded scenario as executable test code for various frameworks. Automat
630
690
  // });
631
691
  ```
632
692
 
693
+ - **Example 2 - Test + Page Object** ⭐ **NEW**:
694
+ ```javascript
695
+ // Export with Page Object class
696
+ exportScenarioAsCode({
697
+ scenarioName: "login_test",
698
+ language: "playwright-typescript",
699
+ generatePageObject: true,
700
+ pageObjectClassName: "LoginPage"
701
+ })
702
+
703
+ // Returns JSON with both files:
704
+ {
705
+ "success": true,
706
+ "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});",
707
+ "pageObjectCode": "import { Page, Locator } from '@playwright/test';\n\nexport class LoginPage {\n readonly page: Page;\n readonly emailInput: Locator;\n readonly loginButton: Locator;\n \n constructor(page: Page) {\n this.page = page;\n this.emailInput = page.locator('#email');\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 clickLoginButton() {\n await this.loginButton.click();\n }\n}",
708
+ "pageObjectClassName": "LoginPage",
709
+ "framework": "playwright-typescript",
710
+ "elementCount": 12
711
+ }
712
+ ```
713
+
714
+ - **Example 3 - Append Mode** ⭐ **NEW**:
715
+ ```javascript
716
+ // Append test to end of existing file
717
+ exportScenarioAsCode({
718
+ scenarioName: "new_feature_test",
719
+ language: "playwright-typescript",
720
+ appendToFile: "./tests/features.spec.ts"
721
+ })
722
+
723
+ // Returns:
724
+ {
725
+ "success": true,
726
+ "mode": "append",
727
+ "file": "./tests/features.spec.ts",
728
+ "testName": "new_feature_test",
729
+ "insertPosition": "end",
730
+ "message": "Test 'new_feature_test' successfully appended to ./tests/features.spec.ts"
731
+ }
732
+ ```
733
+
734
+ - **Example 4 - Append with Position Control** ⭐ **NEW**:
735
+ ```javascript
736
+ // Insert test before specific test
737
+ exportScenarioAsCode({
738
+ scenarioName: "setup_test",
739
+ language: "selenium-python",
740
+ appendToFile: "./tests/test_suite.py",
741
+ insertPosition: "before",
742
+ referenceTestName: "main_test",
743
+ testName: "test_setup_data" // Override name
744
+ })
745
+
746
+ // Or insert after specific test
747
+ exportScenarioAsCode({
748
+ scenarioName: "cleanup",
749
+ language: "playwright-python",
750
+ appendToFile: "./tests/test_flow.py",
751
+ insertPosition: "after",
752
+ referenceTestName: "test_main_flow"
753
+ })
754
+ ```
755
+
633
756
  - **Selector Cleaning**: Automatically removes unstable patterns:
634
757
  - CSS Modules: `Button_primary__2x3yZ` → removed
635
758
  - Styled-components: `sc-AbCdEf-0` → removed
@@ -637,6 +760,61 @@ Export recorded scenario as executable test code for various frameworks. Automat
637
760
  - Hash suffixes: `component_a1b2c3d` → removed
638
761
  - Prefers stable selectors: `data-testid`, `role`, `aria-label`, semantic attributes
639
762
 
763
+ #### generatePageObject ⭐ **NEW**
764
+ 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.
765
+
766
+ - **Parameters**:
767
+ - `className` (optional): Page Object class name (auto-generated from page title/URL if not provided)
768
+ - `framework` (optional): Target framework - `"playwright-typescript"` (default), `"playwright-python"`, `"selenium-python"`, `"selenium-java"`
769
+ - `includeComments` (optional): Include descriptive comments (default: true)
770
+ - `groupElements` (optional): Group elements by page sections (default: true)
771
+
772
+ - **Features**:
773
+ - **Smart Selector Generation**: Prioritizes id > name > data-testid > unique class > CSS path
774
+ - **Intelligent Naming**: Auto-generates element names from labels, placeholders, text, attributes
775
+ - **Section Grouping**: Groups elements by semantic sections (header, nav, form, footer, main, etc.)
776
+ - **Helper Methods**: Auto-generates fill() and click() methods for common actions
777
+ - **Multi-Framework**: Supports Playwright (TS/Python) and Selenium (Python/Java)
778
+
779
+ - **Use cases**:
780
+ - Generate POM classes for test automation
781
+ - Create maintainable test structure from existing pages
782
+ - Bootstrap test framework setup quickly
783
+ - Extract page structure for documentation
784
+
785
+ - **Returns**: Page Object code with metadata (className, url, title, elementCount, framework)
786
+
787
+ - **Example**:
788
+ ```javascript
789
+ // 1. Navigate to page
790
+ openBrowser({ url: "https://example.com/login" })
791
+
792
+ // 2. Generate Page Object
793
+ generatePageObject({
794
+ className: "LoginPage",
795
+ framework: "playwright-typescript",
796
+ includeComments: true,
797
+ groupElements: true
798
+ })
799
+
800
+ // Returns:
801
+ {
802
+ "success": true,
803
+ "className": "LoginPage",
804
+ "url": "https://example.com/login",
805
+ "title": "Login - Example Site",
806
+ "elementCount": 12,
807
+ "framework": "playwright-typescript",
808
+ "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}"
809
+ }
810
+ ```
811
+
812
+ - **Supported Frameworks**:
813
+ - `playwright-typescript`: Playwright with TypeScript (locators, async/await, Page Object pattern)
814
+ - `playwright-python`: Playwright with Python (sync API, snake_case naming)
815
+ - `selenium-python`: Selenium with Python (WebDriver, explicit waits, By locators)
816
+ - `selenium-java`: Selenium with Java (WebDriver, Page Factory compatible)
817
+
640
818
  ---
641
819
 
642
820
  ## 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
+ }