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/CHANGELOG.md +757 -494
- package/README.md +219 -41
- package/browser/browser-manager.js +206 -0
- package/browser/page-manager.js +298 -0
- package/index.js +525 -1892
- package/package.json +55 -55
- package/recorder/page-object-generator.js +720 -0
- package/recorder/recorder-script.js +118 -12
- package/recorder/scenario-executor.js +970 -946
- package/recorder/scenario-storage.js +253 -29
- package/server/tool-definitions.js +620 -0
- package/server/tool-schemas.js +295 -0
- package/utils/code-generators/code-generator-base.js +61 -0
- package/utils/code-generators/file-appender.js +202 -0
- package/utils/code-generators/playwright-python.js +84 -0
- package/utils/code-generators/playwright-typescript.js +95 -0
- package/utils/code-generators/selenium-java.js +123 -0
- package/utils/code-generators/selenium-python.js +82 -0
- package/utils/css-utils.js +151 -0
- package/utils/image-processing.js +236 -0
- package/utils/platform-utils.js +62 -0
- package/utils/url-to-project.js +141 -0
- package/index.js.backup +0 -3674
- package/utils/project-detector.js +0 -87
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
|
-
**
|
|
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
|
-
|
|
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
|
-
**
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
570
|
-
- **
|
|
571
|
-
- **
|
|
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
|
-
|
|
579
|
-
- **
|
|
580
|
-
- **
|
|
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
|
-
- `
|
|
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**:
|
|
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
|
+
}
|