chrometools-mcp 1.7.1 → 1.8.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/CHANGELOG.md +92 -0
- package/README.md +122 -4
- package/element-finder-utils.js +27 -8
- package/index.js +194 -11
- package/package.json +1 -1
- package/recorder/recorder-script.js +1667 -1598
- package/recorder/scenario-executor.js +946 -942
- package/recorder/scenario-storage.js +85 -58
- package/utils/code-generators/code-generator-base.js +237 -0
- package/utils/code-generators/playwright-python.js +253 -0
- package/utils/code-generators/playwright-typescript.js +230 -0
- package/utils/code-generators/selenium-java.js +309 -0
- package/utils/code-generators/selenium-python.js +328 -0
- package/utils/project-detector.js +87 -0
- package/utils/selector-cleaner.js +352 -0
- package/RECORDER_SPEC.md +0 -911
- package/REFACTORING-SUMMARY.md +0 -132
- package/REFACTORING.md +0 -57
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.8.2] - 2025-12-19
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Code Generator Bugs** - Fixed multiple issues in test code generators
|
|
9
|
+
- **Python generators**: Fixed comment syntax - now use `#` instead of `//`
|
|
10
|
+
- **Python generators**: Moved `import re` to top of file instead of inline
|
|
11
|
+
- **Java generator**: Fixed variable name conflicts - now uses unique names (`typeElement`, `hoverElement`, etc.) instead of reusing `element`
|
|
12
|
+
- **Java generator**: Added missing imports (`JavascriptExecutor`, `Select`)
|
|
13
|
+
- **All generators**: Implemented language-specific comment generation via `generateComment()` method
|
|
14
|
+
- Location: `utils/code-generators/code-generator-base.js`, `playwright-python.js`, `selenium-python.js`, `selenium-java.js`
|
|
15
|
+
|
|
16
|
+
## [1.8.1] - 2025-12-19
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Smart Project Directory Detection** - Scenarios now automatically save to the correct project directory
|
|
20
|
+
- Auto-detection cascade: `CLAUDE_PROJECT_DIR` env var → `PROJECT_DIR` env var → Git root → current working directory
|
|
21
|
+
- Optional `directory` parameter on all recorder tools (`enableRecorder`, `executeScenario`, `listScenarios`, `searchScenarios`, `getScenarioInfo`, `deleteScenario`, `exportScenarioAsCode`)
|
|
22
|
+
- Session memory: once directory is set (explicitly or auto-detected), it's remembered for the entire MCP server session
|
|
23
|
+
- Solves issue where scenarios were saved to unpredictable locations based on where MCP process was launched
|
|
24
|
+
- Location: `utils/project-detector.js`, `index.js:97-115`
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- **All recorder storage functions now accept `baseDir` parameter** - Breaking change for direct API usage
|
|
28
|
+
- Updated: `saveScenario()`, `loadScenario()`, `listScenarios()`, `searchScenarios()`, `deleteScenario()`, `loadIndex()`, and all other storage functions
|
|
29
|
+
- MCP tool users: no breaking changes, just new optional `directory` parameter
|
|
30
|
+
- Location: `recorder/scenario-storage.js`, `recorder/scenario-executor.js`, `recorder/recorder-script.js`
|
|
31
|
+
|
|
32
|
+
## [1.8.0] - 2025-12-19
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
- **Test Code Generation** - New MCP tool `exportScenarioAsCode` for generating executable test code from recorded scenarios
|
|
36
|
+
- Supports 4 test frameworks: Playwright (TypeScript/Python), Selenium (Python/Java)
|
|
37
|
+
- Automatic selector cleaning - removes unstable CSS classes (CSS Modules, styled-components, Emotion, hashed classes)
|
|
38
|
+
- Generates clean, readable test code with comments
|
|
39
|
+
- Smart selector stability analysis with pattern-based detection
|
|
40
|
+
- Fallback selector selection - chooses most stable selector from fallbacks
|
|
41
|
+
- Location: `utils/code-generators/`, `utils/selector-cleaner.js`
|
|
42
|
+
|
|
43
|
+
**Unstable patterns detected:**
|
|
44
|
+
- CSS Modules: `Button_primary__2x3yZ`
|
|
45
|
+
- Styled-components: `sc-AbCdEf-0`
|
|
46
|
+
- Emotion: `css-1a2b3c4d`
|
|
47
|
+
- Hash suffixes: `component_a1b2c3d`
|
|
48
|
+
- Random hashes: `_1a2b3c4d`
|
|
49
|
+
|
|
50
|
+
**Usage:**
|
|
51
|
+
```javascript
|
|
52
|
+
exportScenarioAsCode('checkout', {
|
|
53
|
+
language: 'playwright-typescript',
|
|
54
|
+
cleanSelectors: true,
|
|
55
|
+
includeComments: true
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## [1.7.4] - 2025-12-19
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
- **executeScenario auto-opens browser** - No longer throws error when no page is open
|
|
63
|
+
- Automatically opens browser at scenario's `entryUrl` if no page is currently open
|
|
64
|
+
- Eliminates need to manually call `openBrowser` before executing scenarios
|
|
65
|
+
- Improves user experience by treating browser opening as a side effect
|
|
66
|
+
- Falls back gracefully: shows error only if scenario has no `entryUrl`
|
|
67
|
+
- Location: `index.js:3417-3469`
|
|
68
|
+
|
|
69
|
+
## [1.7.3] - 2025-12-19
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
- **Fixed CSS selector generation crash** - `analyzePage` now handles attribute values with special characters
|
|
73
|
+
- Added `isSafeSelectorValue()` function to validate attribute values before using in selectors
|
|
74
|
+
- Filters out attributes containing problematic characters: `["'\\[]{}()]`
|
|
75
|
+
- Added try-catch blocks to prevent selector syntax errors
|
|
76
|
+
- Example: `button[data-counter="["b"]"]` (invalid) is now skipped
|
|
77
|
+
- Location: `element-finder-utils.js:302-360`
|
|
78
|
+
|
|
79
|
+
### Improved
|
|
80
|
+
- **Recorder now skips hidden elements** - Prevents recording actions on invisible elements
|
|
81
|
+
- Added `isElementVisible()` function to check element visibility before recording
|
|
82
|
+
- Checks: offsetWidth/Height, display, visibility, opacity
|
|
83
|
+
- Applies to all event types: click, type, select, upload, hover, drag
|
|
84
|
+
- Prevents scenarios with duplicate/invisible element actions (e.g., Yandex search with hidden input)
|
|
85
|
+
- Console logs when skipping hidden elements for debugging
|
|
86
|
+
- Location: `recorder/recorder-script.js:1173-1188`
|
|
87
|
+
|
|
88
|
+
## [1.7.2] - 2025-12-16
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
- **Figma API Token Setup documentation** - Added comprehensive guide on how to obtain and configure Figma Personal Access Token
|
|
92
|
+
- Step-by-step instructions for getting token from Figma account settings
|
|
93
|
+
- Configuration examples for both Claude Desktop and Claude Code
|
|
94
|
+
- Environment variable setup (`FIGMA_TOKEN`)
|
|
95
|
+
- Note about alternative parameter-based token passing
|
|
96
|
+
|
|
5
97
|
## [1.7.1] - 2025-12-15
|
|
6
98
|
|
|
7
99
|
### Performance
|
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
|
|
17
|
+
- [Recorder Tools](#6-recorder-tools) ⭐ **NEW** - enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode
|
|
18
18
|
- [Typical Workflow Example](#typical-workflow-example)
|
|
19
19
|
- [Tool Usage Tips](#tool-usage-tips)
|
|
20
20
|
- [Configuration](#configuration)
|
|
@@ -506,9 +506,32 @@ 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)
|
|
514
|
+
|
|
515
|
+
Once a directory is set (explicitly or auto-detected), it's remembered for the entire MCP server session.
|
|
516
|
+
|
|
517
|
+
**Example**:
|
|
518
|
+
```javascript
|
|
519
|
+
// Let it auto-detect (recommended)
|
|
520
|
+
enableRecorder()
|
|
521
|
+
|
|
522
|
+
// Or specify explicitly
|
|
523
|
+
enableRecorder({ directory: "/path/to/project" })
|
|
524
|
+
|
|
525
|
+
// Later calls reuse the same directory automatically
|
|
526
|
+
executeScenario({ name: "test" }) // Uses remembered directory
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
509
531
|
#### enableRecorder
|
|
510
532
|
Inject visual recorder UI widget into the current page.
|
|
511
|
-
- **Parameters**:
|
|
533
|
+
- **Parameters**:
|
|
534
|
+
- `directory` (optional): Directory to save scenarios (auto-detected if not provided)
|
|
512
535
|
- **Use case**: Start recording user interactions visually
|
|
513
536
|
- **Returns**: Success status
|
|
514
537
|
- **Features**:
|
|
@@ -524,6 +547,7 @@ Execute a previously recorded scenario by name.
|
|
|
524
547
|
- `name` (required): Scenario name
|
|
525
548
|
- `parameters` (optional): Runtime parameters (e.g., { email: "user@test.com" })
|
|
526
549
|
- `executeDependencies` (optional): Execute dependencies before running scenario (default: true)
|
|
550
|
+
- `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
|
|
527
551
|
- **Use case**: Run automated test scenarios
|
|
528
552
|
- **Returns**: Execution result with success/failure status
|
|
529
553
|
- **Features**:
|
|
@@ -541,7 +565,8 @@ Execute a previously recorded scenario by name.
|
|
|
541
565
|
|
|
542
566
|
#### listScenarios
|
|
543
567
|
Get all available scenarios with metadata.
|
|
544
|
-
- **Parameters**:
|
|
568
|
+
- **Parameters**:
|
|
569
|
+
- `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
|
|
545
570
|
- **Use case**: Browse recorded scenarios
|
|
546
571
|
- **Returns**: Array of scenarios with names, descriptions, tags, timestamps
|
|
547
572
|
|
|
@@ -550,6 +575,7 @@ Search scenarios by text or tags.
|
|
|
550
575
|
- **Parameters**:
|
|
551
576
|
- `text` (optional): Search in name/description
|
|
552
577
|
- `tags` (optional): Array of tags to filter
|
|
578
|
+
- `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
|
|
553
579
|
- **Use case**: Find specific scenarios
|
|
554
580
|
- **Returns**: Matching scenarios
|
|
555
581
|
|
|
@@ -558,15 +584,59 @@ Get detailed information about a scenario.
|
|
|
558
584
|
- **Parameters**:
|
|
559
585
|
- `name` (required): Scenario name
|
|
560
586
|
- `includeSecrets` (optional): Include secret values (default: false)
|
|
587
|
+
- `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
|
|
561
588
|
- **Use case**: Inspect scenario actions and dependencies
|
|
562
589
|
- **Returns**: Full scenario details (actions, metadata, dependencies)
|
|
563
590
|
|
|
564
591
|
#### deleteScenario
|
|
565
592
|
Delete a scenario and its associated secrets.
|
|
566
|
-
- **Parameters**:
|
|
593
|
+
- **Parameters**:
|
|
594
|
+
- `name` (required): Scenario name
|
|
595
|
+
- `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
|
|
567
596
|
- **Use case**: Clean up unused scenarios
|
|
568
597
|
- **Returns**: Success confirmation
|
|
569
598
|
|
|
599
|
+
#### exportScenarioAsCode ⭐ **NEW**
|
|
600
|
+
Export recorded scenario as executable test code for various frameworks. Automatically cleans unstable selectors (CSS Modules, styled-components, Emotion).
|
|
601
|
+
|
|
602
|
+
- **Parameters**:
|
|
603
|
+
- `scenarioName` (required): Name of scenario to export
|
|
604
|
+
- `language` (required): Target framework - `"playwright-typescript"`, `"playwright-python"`, `"selenium-python"`, `"selenium-java"`
|
|
605
|
+
- `cleanSelectors` (optional): Remove unstable CSS classes (default: true)
|
|
606
|
+
- `includeComments` (optional): Include descriptive comments (default: true)
|
|
607
|
+
- `directory` (optional): Directory where scenarios are stored (auto-detected if not provided)
|
|
608
|
+
|
|
609
|
+
- **Use case**: Convert recorded scenarios into maintainable test code
|
|
610
|
+
|
|
611
|
+
- **Returns**: Generated test code as string
|
|
612
|
+
|
|
613
|
+
- **Example**:
|
|
614
|
+
```javascript
|
|
615
|
+
// Export scenario as Playwright TypeScript
|
|
616
|
+
exportScenarioAsCode({
|
|
617
|
+
scenarioName: "checkout_flow",
|
|
618
|
+
language: "playwright-typescript",
|
|
619
|
+
cleanSelectors: true,
|
|
620
|
+
includeComments: true
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
// Returns clean test code:
|
|
624
|
+
// import { test, expect } from '@playwright/test';
|
|
625
|
+
//
|
|
626
|
+
// test('checkout_flow', async ({ page }) => {
|
|
627
|
+
// await page.goto('https://example.com');
|
|
628
|
+
// await page.locator('button[data-testid="add-to-cart"]').click();
|
|
629
|
+
// await expect(page).toHaveURL(/checkout/);
|
|
630
|
+
// });
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
- **Selector Cleaning**: Automatically removes unstable patterns:
|
|
634
|
+
- CSS Modules: `Button_primary__2x3yZ` → removed
|
|
635
|
+
- Styled-components: `sc-AbCdEf-0` → removed
|
|
636
|
+
- Emotion: `css-1a2b3c4d` → removed
|
|
637
|
+
- Hash suffixes: `component_a1b2c3d` → removed
|
|
638
|
+
- Prefers stable selectors: `data-testid`, `role`, `aria-label`, semantic attributes
|
|
639
|
+
|
|
570
640
|
---
|
|
571
641
|
|
|
572
642
|
## Typical Workflow Example
|
|
@@ -667,6 +737,54 @@ If you don't need to see the browser window, you can use xvfb (virtual X server)
|
|
|
667
737
|
|
|
668
738
|
This runs Chrome in GUI mode but on a virtual display (window is not visible).
|
|
669
739
|
|
|
740
|
+
### Figma API Token Setup
|
|
741
|
+
|
|
742
|
+
To use Figma tools, you need to configure your Figma Personal Access Token.
|
|
743
|
+
|
|
744
|
+
**How to get your Figma token:**
|
|
745
|
+
1. Go to your Figma account settings: https://www.figma.com/settings
|
|
746
|
+
2. Scroll down to "Personal access tokens"
|
|
747
|
+
3. Click "Create a new personal access token"
|
|
748
|
+
4. Give it a name (e.g., "chrometools-mcp")
|
|
749
|
+
5. Copy the generated token
|
|
750
|
+
|
|
751
|
+
**Add token to MCP configuration:**
|
|
752
|
+
|
|
753
|
+
**Claude Desktop** (`~/.claude/mcp_config.json` or `~/AppData/Roaming/Claude/mcp_config.json` on Windows):
|
|
754
|
+
|
|
755
|
+
```json
|
|
756
|
+
{
|
|
757
|
+
"mcpServers": {
|
|
758
|
+
"chrometools": {
|
|
759
|
+
"command": "npx",
|
|
760
|
+
"args": ["-y", "chrometools-mcp"],
|
|
761
|
+
"env": {
|
|
762
|
+
"FIGMA_TOKEN": "your-figma-token-here"
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
**Claude Code** (`~/.claude.json`):
|
|
770
|
+
|
|
771
|
+
```json
|
|
772
|
+
{
|
|
773
|
+
"mcpServers": {
|
|
774
|
+
"chrometools": {
|
|
775
|
+
"type": "stdio",
|
|
776
|
+
"command": "npx",
|
|
777
|
+
"args": ["-y", "chrometools-mcp"],
|
|
778
|
+
"env": {
|
|
779
|
+
"FIGMA_TOKEN": "your-figma-token-here"
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**Note:** Alternatively, you can pass the token directly in each Figma tool call using the `figmaToken` parameter, but using the environment variable is more convenient.
|
|
787
|
+
|
|
670
788
|
---
|
|
671
789
|
|
|
672
790
|
## WSL Setup Guide
|
package/element-finder-utils.js
CHANGED
|
@@ -295,6 +295,16 @@ function scoreInputField(element, context, description) {
|
|
|
295
295
|
return score;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Check if attribute value is safe to use in CSS selector
|
|
300
|
+
* Returns false if value contains characters that could break selector syntax
|
|
301
|
+
*/
|
|
302
|
+
function isSafeSelectorValue(value) {
|
|
303
|
+
if (!value || typeof value !== 'string') return false;
|
|
304
|
+
// Check for problematic characters: quotes, brackets, backslashes
|
|
305
|
+
return !/["'\\[\]{}()]/.test(value);
|
|
306
|
+
}
|
|
307
|
+
|
|
298
308
|
/**
|
|
299
309
|
* Generate unique CSS selector for an element
|
|
300
310
|
*/
|
|
@@ -320,23 +330,32 @@ function getUniqueSelectorInPage(element) {
|
|
|
320
330
|
}
|
|
321
331
|
}
|
|
322
332
|
|
|
323
|
-
// Try name attribute
|
|
324
|
-
if (element.name) {
|
|
333
|
+
// Try name attribute (only if value is safe)
|
|
334
|
+
if (element.name && isSafeSelectorValue(element.name)) {
|
|
325
335
|
const selector = `${element.tagName.toLowerCase()}[name="${element.name}"]`;
|
|
326
|
-
|
|
327
|
-
|
|
336
|
+
try {
|
|
337
|
+
if (document.querySelectorAll(selector).length === 1) {
|
|
338
|
+
return selector;
|
|
339
|
+
}
|
|
340
|
+
} catch (e) {
|
|
341
|
+
// Invalid selector, skip
|
|
328
342
|
}
|
|
329
343
|
}
|
|
330
344
|
|
|
331
|
-
// Try data attributes
|
|
345
|
+
// Try data attributes (only if values are safe)
|
|
332
346
|
const dataAttrs = Array.from(element.attributes)
|
|
333
|
-
.filter(attr => attr.name.startsWith('data-'))
|
|
347
|
+
.filter(attr => attr.name.startsWith('data-') && isSafeSelectorValue(attr.value))
|
|
334
348
|
.slice(0, 2);
|
|
335
349
|
|
|
336
350
|
for (const attr of dataAttrs) {
|
|
337
351
|
const selector = `${element.tagName.toLowerCase()}[${attr.name}="${attr.value}"]`;
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
try {
|
|
353
|
+
if (document.querySelectorAll(selector).length === 1) {
|
|
354
|
+
return selector;
|
|
355
|
+
}
|
|
356
|
+
} catch (e) {
|
|
357
|
+
// Invalid selector, skip
|
|
358
|
+
continue;
|
|
340
359
|
}
|
|
341
360
|
}
|
|
342
361
|
|
package/index.js
CHANGED
|
@@ -47,6 +47,18 @@ import {
|
|
|
47
47
|
deleteScenario
|
|
48
48
|
} from './recorder/scenario-storage.js';
|
|
49
49
|
|
|
50
|
+
// Import Project Detection
|
|
51
|
+
import { detectProjectRoot } from './utils/project-detector.js';
|
|
52
|
+
|
|
53
|
+
// Import Code Generators
|
|
54
|
+
import { PlaywrightTypeScriptGenerator } from './utils/code-generators/playwright-typescript.js';
|
|
55
|
+
import { PlaywrightPythonGenerator } from './utils/code-generators/playwright-python.js';
|
|
56
|
+
import { SeleniumPythonGenerator } from './utils/code-generators/selenium-python.js';
|
|
57
|
+
import { SeleniumJavaGenerator } from './utils/code-generators/selenium-java.js';
|
|
58
|
+
|
|
59
|
+
// Global state for scenarios directory
|
|
60
|
+
let currentScenariosDirectory = null;
|
|
61
|
+
|
|
50
62
|
// Import Figma tools
|
|
51
63
|
import {
|
|
52
64
|
parseFigmaUrl,
|
|
@@ -76,6 +88,32 @@ const isWSL = (() => {
|
|
|
76
88
|
// Detect Windows environment (including WSL)
|
|
77
89
|
const isWindows = process.platform === 'win32' || isWSL;
|
|
78
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Get base directory for scenarios storage
|
|
93
|
+
* Uses cascade strategy: explicit param → remembered → auto-detect
|
|
94
|
+
* @param {string|null} explicitDir - Optional explicit directory path
|
|
95
|
+
* @returns {string} - Base directory path
|
|
96
|
+
*/
|
|
97
|
+
function getScenariosBaseDir(explicitDir = null) {
|
|
98
|
+
// 1. If explicitly provided - remember and return
|
|
99
|
+
if (explicitDir) {
|
|
100
|
+
currentScenariosDirectory = path.resolve(explicitDir);
|
|
101
|
+
console.log('[chrometools-mcp] Using explicit directory:', currentScenariosDirectory);
|
|
102
|
+
return currentScenariosDirectory;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. If already remembered - return
|
|
106
|
+
if (currentScenariosDirectory) {
|
|
107
|
+
console.log('[chrometools-mcp] Using remembered directory:', currentScenariosDirectory);
|
|
108
|
+
return currentScenariosDirectory;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 3. Auto-detect from environment
|
|
112
|
+
const detected = detectProjectRoot();
|
|
113
|
+
currentScenariosDirectory = detected;
|
|
114
|
+
return detected;
|
|
115
|
+
}
|
|
116
|
+
|
|
79
117
|
// Get Chrome executable path based on platform
|
|
80
118
|
function getChromePath() {
|
|
81
119
|
if (process.platform === 'win32') {
|
|
@@ -398,7 +436,8 @@ async function setupRecorderAutoReinjection(page) {
|
|
|
398
436
|
// Check if this page had recorder before
|
|
399
437
|
if (pagesWithRecorder.has(page)) {
|
|
400
438
|
try {
|
|
401
|
-
|
|
439
|
+
const baseDir = getScenariosBaseDir();
|
|
440
|
+
await injectRecorder(page, baseDir);
|
|
402
441
|
} catch (error) {
|
|
403
442
|
console.error('[chrometools-mcp] Failed to re-inject recorder:', error.message);
|
|
404
443
|
}
|
|
@@ -411,7 +450,8 @@ async function setupRecorderAutoReinjection(page) {
|
|
|
411
450
|
// Check if this page had recorder before
|
|
412
451
|
if (pagesWithRecorder.has(page)) {
|
|
413
452
|
try {
|
|
414
|
-
|
|
453
|
+
const baseDir = getScenariosBaseDir();
|
|
454
|
+
await injectRecorder(page, baseDir);
|
|
415
455
|
} catch (error) {
|
|
416
456
|
console.error('[chrometools-mcp] Failed to re-inject recorder after reload:', error.message);
|
|
417
457
|
}
|
|
@@ -1576,7 +1616,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1576
1616
|
description: "Inject recorder UI widget. Visual recording with start/stop/save controls.",
|
|
1577
1617
|
inputSchema: {
|
|
1578
1618
|
type: "object",
|
|
1579
|
-
properties: {
|
|
1619
|
+
properties: {
|
|
1620
|
+
directory: { type: "string", description: "Directory to save scenarios (optional, defaults to auto-detected project root)" },
|
|
1621
|
+
},
|
|
1580
1622
|
},
|
|
1581
1623
|
},
|
|
1582
1624
|
{
|
|
@@ -1588,6 +1630,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1588
1630
|
name: { type: "string", description: "Scenario name" },
|
|
1589
1631
|
parameters: { type: "object", description: "Execution parameters" },
|
|
1590
1632
|
executeDependencies: { type: "boolean", description: "Execute dependencies (default: true)" },
|
|
1633
|
+
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1591
1634
|
},
|
|
1592
1635
|
required: ["name"],
|
|
1593
1636
|
},
|
|
@@ -1597,7 +1640,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1597
1640
|
description: "List all scenarios with metadata.",
|
|
1598
1641
|
inputSchema: {
|
|
1599
1642
|
type: "object",
|
|
1600
|
-
properties: {
|
|
1643
|
+
properties: {
|
|
1644
|
+
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1645
|
+
},
|
|
1601
1646
|
},
|
|
1602
1647
|
},
|
|
1603
1648
|
{
|
|
@@ -1608,6 +1653,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1608
1653
|
properties: {
|
|
1609
1654
|
text: { type: "string", description: "Search text" },
|
|
1610
1655
|
tags: { type: "array", items: { type: "string" }, description: "Filter tags" },
|
|
1656
|
+
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1611
1657
|
},
|
|
1612
1658
|
},
|
|
1613
1659
|
},
|
|
@@ -1619,6 +1665,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1619
1665
|
properties: {
|
|
1620
1666
|
name: { type: "string", description: "Scenario name" },
|
|
1621
1667
|
includeSecrets: { type: "boolean", description: "Include secrets (default: false)" },
|
|
1668
|
+
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1622
1669
|
},
|
|
1623
1670
|
required: ["name"],
|
|
1624
1671
|
},
|
|
@@ -1630,10 +1677,42 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
1630
1677
|
type: "object",
|
|
1631
1678
|
properties: {
|
|
1632
1679
|
name: { type: "string", description: "Scenario name" },
|
|
1680
|
+
directory: { type: "string", description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)" },
|
|
1633
1681
|
},
|
|
1634
1682
|
required: ["name"],
|
|
1635
1683
|
},
|
|
1636
1684
|
},
|
|
1685
|
+
{
|
|
1686
|
+
name: "exportScenarioAsCode",
|
|
1687
|
+
description: "Export recorded scenario as executable test code for various frameworks. Automatically cleans unstable selectors (CSS modules, styled-components).",
|
|
1688
|
+
inputSchema: {
|
|
1689
|
+
type: "object",
|
|
1690
|
+
properties: {
|
|
1691
|
+
scenarioName: {
|
|
1692
|
+
type: "string",
|
|
1693
|
+
description: "Name of scenario to export"
|
|
1694
|
+
},
|
|
1695
|
+
language: {
|
|
1696
|
+
type: "string",
|
|
1697
|
+
enum: ["playwright-typescript", "playwright-python", "selenium-python", "selenium-java"],
|
|
1698
|
+
description: "Target test framework and language"
|
|
1699
|
+
},
|
|
1700
|
+
cleanSelectors: {
|
|
1701
|
+
type: "boolean",
|
|
1702
|
+
description: "Remove unstable CSS classes (default: true)"
|
|
1703
|
+
},
|
|
1704
|
+
includeComments: {
|
|
1705
|
+
type: "boolean",
|
|
1706
|
+
description: "Include descriptive comments (default: true)"
|
|
1707
|
+
},
|
|
1708
|
+
directory: {
|
|
1709
|
+
type: "string",
|
|
1710
|
+
description: "Directory where scenarios are stored (optional, defaults to auto-detected project root)"
|
|
1711
|
+
},
|
|
1712
|
+
},
|
|
1713
|
+
required: ["scenarioName", "language"],
|
|
1714
|
+
},
|
|
1715
|
+
},
|
|
1637
1716
|
],
|
|
1638
1717
|
};
|
|
1639
1718
|
});
|
|
@@ -3392,8 +3471,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3392
3471
|
}
|
|
3393
3472
|
|
|
3394
3473
|
if (name === "enableRecorder") {
|
|
3474
|
+
const baseDir = getScenariosBaseDir(args.directory);
|
|
3395
3475
|
const page = await getLastOpenPage();
|
|
3396
|
-
const result = await injectRecorder(page);
|
|
3476
|
+
const result = await injectRecorder(page, baseDir);
|
|
3397
3477
|
|
|
3398
3478
|
// Track this page as having recorder enabled
|
|
3399
3479
|
if (result.success) {
|
|
@@ -3415,8 +3495,48 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3415
3495
|
}
|
|
3416
3496
|
|
|
3417
3497
|
if (name === "executeScenario") {
|
|
3418
|
-
|
|
3419
|
-
const
|
|
3498
|
+
// Get base directory for scenarios
|
|
3499
|
+
const baseDir = getScenariosBaseDir(args.directory);
|
|
3500
|
+
|
|
3501
|
+
// Try to get existing page, or auto-open browser using scenario's entryUrl
|
|
3502
|
+
let page;
|
|
3503
|
+
try {
|
|
3504
|
+
page = await getLastOpenPage();
|
|
3505
|
+
} catch (error) {
|
|
3506
|
+
// No page is open - load scenario and open browser at entryUrl
|
|
3507
|
+
const scenario = await loadScenario(args.name, false, baseDir);
|
|
3508
|
+
if (!scenario) {
|
|
3509
|
+
return {
|
|
3510
|
+
content: [{
|
|
3511
|
+
type: 'text',
|
|
3512
|
+
text: JSON.stringify({
|
|
3513
|
+
success: false,
|
|
3514
|
+
error: `Scenario "${args.name}" not found`
|
|
3515
|
+
}, null, 2)
|
|
3516
|
+
}]
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
|
|
3520
|
+
const entryUrl = scenario.metadata?.entryUrl;
|
|
3521
|
+
if (!entryUrl) {
|
|
3522
|
+
return {
|
|
3523
|
+
content: [{
|
|
3524
|
+
type: 'text',
|
|
3525
|
+
text: JSON.stringify({
|
|
3526
|
+
success: false,
|
|
3527
|
+
error: `Scenario "${args.name}" has no entryUrl. Cannot auto-open browser.`
|
|
3528
|
+
}, null, 2)
|
|
3529
|
+
}]
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
// Auto-open browser at scenario's entry URL
|
|
3534
|
+
page = await getOrCreatePage(entryUrl);
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
const options = {
|
|
3538
|
+
baseDir: baseDir
|
|
3539
|
+
};
|
|
3420
3540
|
|
|
3421
3541
|
// Pass executeDependencies option if provided
|
|
3422
3542
|
if (args.executeDependencies !== undefined) {
|
|
@@ -3434,7 +3554,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3434
3554
|
}
|
|
3435
3555
|
|
|
3436
3556
|
if (name === "listScenarios") {
|
|
3437
|
-
const
|
|
3557
|
+
const baseDir = getScenariosBaseDir(args.directory);
|
|
3558
|
+
const scenarios = await listScenarios(baseDir);
|
|
3438
3559
|
|
|
3439
3560
|
return {
|
|
3440
3561
|
content: [{
|
|
@@ -3445,7 +3566,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3445
3566
|
}
|
|
3446
3567
|
|
|
3447
3568
|
if (name === "searchScenarios") {
|
|
3448
|
-
const
|
|
3569
|
+
const baseDir = getScenariosBaseDir(args.directory);
|
|
3570
|
+
const results = await searchScenarios(args, baseDir);
|
|
3449
3571
|
|
|
3450
3572
|
return {
|
|
3451
3573
|
content: [{
|
|
@@ -3456,7 +3578,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3456
3578
|
}
|
|
3457
3579
|
|
|
3458
3580
|
if (name === "getScenarioInfo") {
|
|
3459
|
-
const
|
|
3581
|
+
const baseDir = getScenariosBaseDir(args.directory);
|
|
3582
|
+
const scenario = await loadScenario(args.name, args.includeSecrets || false, baseDir);
|
|
3460
3583
|
|
|
3461
3584
|
return {
|
|
3462
3585
|
content: [{
|
|
@@ -3467,7 +3590,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3467
3590
|
}
|
|
3468
3591
|
|
|
3469
3592
|
if (name === "deleteScenario") {
|
|
3470
|
-
const
|
|
3593
|
+
const baseDir = getScenariosBaseDir(args.directory);
|
|
3594
|
+
const result = await deleteScenario(args.name, baseDir);
|
|
3471
3595
|
|
|
3472
3596
|
return {
|
|
3473
3597
|
content: [{
|
|
@@ -3477,6 +3601,65 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
3477
3601
|
};
|
|
3478
3602
|
}
|
|
3479
3603
|
|
|
3604
|
+
if (name === "exportScenarioAsCode") {
|
|
3605
|
+
const baseDir = getScenariosBaseDir(args.directory);
|
|
3606
|
+
const scenario = await loadScenario(args.scenarioName, false, baseDir);
|
|
3607
|
+
|
|
3608
|
+
if (!scenario) {
|
|
3609
|
+
return {
|
|
3610
|
+
content: [{
|
|
3611
|
+
type: 'text',
|
|
3612
|
+
text: JSON.stringify({
|
|
3613
|
+
error: `Scenario "${args.scenarioName}" not found`
|
|
3614
|
+
}, null, 2)
|
|
3615
|
+
}],
|
|
3616
|
+
isError: true
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
// Select generator based on language
|
|
3621
|
+
let generator;
|
|
3622
|
+
const options = {
|
|
3623
|
+
cleanSelectors: args.cleanSelectors !== false, // default true
|
|
3624
|
+
includeComments: args.includeComments !== false, // default true
|
|
3625
|
+
};
|
|
3626
|
+
|
|
3627
|
+
switch (args.language) {
|
|
3628
|
+
case 'playwright-typescript':
|
|
3629
|
+
generator = new PlaywrightTypeScriptGenerator(options);
|
|
3630
|
+
break;
|
|
3631
|
+
case 'playwright-python':
|
|
3632
|
+
generator = new PlaywrightPythonGenerator(options);
|
|
3633
|
+
break;
|
|
3634
|
+
case 'selenium-python':
|
|
3635
|
+
generator = new SeleniumPythonGenerator(options);
|
|
3636
|
+
break;
|
|
3637
|
+
case 'selenium-java':
|
|
3638
|
+
generator = new SeleniumJavaGenerator(options);
|
|
3639
|
+
break;
|
|
3640
|
+
default:
|
|
3641
|
+
return {
|
|
3642
|
+
content: [{
|
|
3643
|
+
type: 'text',
|
|
3644
|
+
text: JSON.stringify({
|
|
3645
|
+
error: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
|
|
3646
|
+
}, null, 2)
|
|
3647
|
+
}],
|
|
3648
|
+
isError: true
|
|
3649
|
+
};
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
// Generate code
|
|
3653
|
+
const code = generator.generate(scenario, options);
|
|
3654
|
+
|
|
3655
|
+
return {
|
|
3656
|
+
content: [{
|
|
3657
|
+
type: 'text',
|
|
3658
|
+
text: code
|
|
3659
|
+
}]
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3480
3663
|
return {
|
|
3481
3664
|
content: [
|
|
3482
3665
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrometools-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, visual testing, Figma comparison, and design validation. Works seamlessly in WSL, Linux, and macOS.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|