chrometools-mcp 2.2.0 → 2.4.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 CHANGED
@@ -2,6 +2,111 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [2.4.0] - 2025-12-29
6
+
7
+ ### Added
8
+ - **Tool Filtering by Groups** - New `ENABLED_TOOLS` environment variable for selective group enabling
9
+ - Configure via `env.ENABLED_TOOLS` in MCP client config (comma-separated list of group names)
10
+ - Available groups: `core`, `interaction`, `inspection`, `debug`, `advanced`, `recorder`, `figma`
11
+ - Group structure optimized:
12
+ - `debug` - NEW group for debugging tools (console logs, network monitoring)
13
+ - `advanced` - Combined with AI tools (now includes smartFindElement, analyzePage, etc.)
14
+ - If not set, all tools are enabled (default behavior)
15
+ - If set, only tools from specified groups are available to AI
16
+ - Primary benefit: **Token optimization** - all 43 tools consume ~28k tokens (~14% of context). Enable only needed groups to reduce token usage and costs
17
+ - Additional use cases: security/compliance restrictions, workflow simplification, improved AI focus
18
+ - Examples:
19
+ - Basic automation: `ENABLED_TOOLS=core,interaction,inspection`
20
+ - Advanced with AI: `ENABLED_TOOLS=core,interaction,advanced`
21
+ - With debugging: `ENABLED_TOOLS=core,interaction,inspection,debug`
22
+ - Figma validation: `ENABLED_TOOLS=core,figma`
23
+ - Location:
24
+ - `server/tool-groups.js` - group definitions (7 groups, 43 tools total)
25
+ - `index.js:36` - import groups module
26
+ - `index.js:77-92` - parsing and filtering logic
27
+ - `index.js:195-203` - apply filter to ListTools handler
28
+ - Documentation: README.md section "Tool Filtering with ENABLED_TOOLS"
29
+
30
+ ## [2.3.2] - 2025-12-25
31
+
32
+ ### Changed
33
+ - **appendScenarioToFile** - Simplified architecture: MCP server no longer reads test files
34
+ - Removed `FileAppender.validateFile()` and `FileAppender.readFile()` calls
35
+ - Removed `generator.appendTest()` call - no longer merges content server-side
36
+ - Returns only test code (without imports) via `testCode` field
37
+ - Changed `action: "write_file"` → `action: "append_test"` (more accurate description)
38
+ - Claude Code now responsible for reading file, appending test, and writing back
39
+ - Clearer separation of concerns: MCP generates code, Claude Code handles file I/O
40
+ - Location: `index.js:2126-2215`
41
+
42
+ ## [2.3.1] - 2024-12-25
43
+
44
+ ### Fixed
45
+ - **executeScenario** - Fixed scenario execution when current page URL doesn't match scenario's entryUrl
46
+ - Added automatic navigation to entryUrl before executing scenario
47
+ - Normalizes URLs for comparison (ignores trailing slashes, query params like `nr`, `redirect_ts`)
48
+ - Prevents timeout errors when scenario expects different page than currently open
49
+ - Example: If scenario recorded on `ya.ru` but current page is `ya.ru/search/`, automatically navigates to `ya.ru`
50
+ - Logs navigation events to console for debugging
51
+ - Location: `index.js:1951-1987`
52
+
53
+ - **appendScenarioToFile** - Unified response format with exportScenarioAsCode for better Claude Code compatibility
54
+ - Changed `action: "append_to_file"` → `action: "write_file"` (clearer action)
55
+ - Changed `updatedContent` → `testCode` (same field name as exportScenarioAsCode)
56
+ - Added `content` field (duplicate of testCode for compatibility)
57
+ - Simplified instruction: single-step "Write the testCode..." instead of two-step "Read... then write..."
58
+ - Improved error message when file not found: now suggests using exportScenarioAsCode instead
59
+ - Location: `index.js:2191-2229`
60
+
61
+ ## [2.3.0] - 2024-12-25
62
+
63
+ ### Breaking Changes
64
+ - **exportScenarioAsCode** - Removed append-to-file functionality to eliminate confusion
65
+ - **REMOVED** parameters: `appendToFile`, `testName`, `insertPosition`, `referenceTestName`
66
+ - Now exclusively creates NEW test files (returns JSON with `action: "create_new_file"`)
67
+ - Returns JSON format with `action`, `suggestedFileName`, `testCode`, `instruction` fields
68
+ - Claude Code writes files based on returned JSON (MCP server no longer writes files directly)
69
+ - Migration: Use new `appendScenarioToFile` tool instead of `appendToFile` parameter
70
+
71
+ ### Added
72
+ - **appendScenarioToFile** - NEW tool for appending tests to existing files
73
+ - Parameters: `scenarioName`, `language`, `targetFile` (required)
74
+ - Optional: `testName`, `insertPosition`, `referenceTestName`, `cleanSelectors`, `includeComments`, `generatePageObject`, `pageObjectClassName`
75
+ - Returns JSON with `action: "append_to_file"`, `targetFile`, `updatedContent`, `instruction`
76
+ - Safely appends tests without overwriting existing tests
77
+ - Claude Code writes updated file content based on returned JSON
78
+ - Location: `index.js:2041-2186`, `server/tool-definitions.js:577-628`
79
+
80
+ ### Changed
81
+ - **exportScenarioAsCode** - Changed return format from plain text to structured JSON
82
+ - Returns: `{action, suggestedFileName, testCode, instruction, pageObject?}`
83
+ - Suggests filename based on scenario name and language
84
+ - Includes clear instructions for Claude Code to create files
85
+ - Location: `index.js:2188-2357`, `server/tool-definitions.js:542-576`
86
+ - **File writing responsibility** - Moved from MCP server to Claude Code
87
+ - MCP tools now return JSON with file content + instructions
88
+ - Claude Code uses Write tool to create/update files
89
+ - Eliminates risk of MCP server directly overwriting files
90
+
91
+ ### Documentation
92
+ - **README.md** - Split exportScenarioAsCode documentation into two sections
93
+ - exportScenarioAsCode: For creating new test files
94
+ - appendScenarioToFile: For appending to existing files
95
+ - Updated tool count: 39+ → 40+ tools
96
+ - Added clear examples for both tools with JSON response formats
97
+ - Location: `README.md:11,17,652-806`
98
+
99
+ ## [2.2.1] - 2024-12-24
100
+
101
+ ### Improved
102
+ - **exportScenarioAsCode** - Clarified tool description to prevent accidental test file overwrites
103
+ - Added explicit warning: default mode (without `appendToFile`) returns code for NEW file
104
+ - Emphasized that `appendToFile` parameter is **REQUIRED** to safely add tests to existing files
105
+ - Updated `appendToFile` parameter description to highlight safety aspect
106
+ - Updated `insertPosition` and `referenceTestName` descriptions to clarify they only work with `appendToFile`
107
+ - Added warning section in README.md documentation
108
+ - Location: `server/tool-definitions.js:544,575-589`, `README.md:667-669`
109
+
5
110
  ## [2.2.0] - 2025-12-21
6
111
 
7
112
  ### Added
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, generatePageObject
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)
@@ -650,7 +650,7 @@ Delete a scenario and its associated secrets. Searches all projects to find the
650
650
  - **Returns**: Success confirmation
651
651
 
652
652
  #### exportScenarioAsCode ⭐ **NEW**
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.
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.
654
654
 
655
655
  - **Parameters**:
656
656
  - `scenarioName` (required): Name of scenario to export
@@ -659,38 +659,34 @@ Export recorded scenario as executable test code for various frameworks. Automat
659
659
  - `includeComments` (optional): Include descriptive comments (default: true)
660
660
  - `generatePageObject` (optional): Also generate Page Object class for the page (default: false)
661
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**
666
662
 
667
- - **Use case**: Convert recorded scenarios into maintainable test code with optional Page Objects, or append to existing test suites
663
+ - **Use case**: Create new test files from recorded scenarios with optional Page Objects
668
664
 
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}`
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
672
671
 
673
- - **Example 1 - Test only** (default behavior):
672
+ - **Example 1 - Test only**:
674
673
  ```javascript
675
- // Export scenario as Playwright TypeScript
674
+ // Export scenario as new Playwright TypeScript file
676
675
  exportScenarioAsCode({
677
676
  scenarioName: "checkout_flow",
678
- language: "playwright-typescript",
679
- cleanSelectors: true,
680
- includeComments: true
677
+ language: "playwright-typescript"
681
678
  })
682
679
 
683
- // Returns clean test code:
684
- // import { test, expect } from '@playwright/test';
685
- //
686
- // test('checkout_flow', async ({ page }) => {
687
- // await page.goto('https://example.com');
688
- // await page.locator('button[data-testid="add-to-cart"]').click();
689
- // await expect(page).toHaveURL(/checkout/);
690
- // });
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
+ }
691
687
  ```
692
688
 
693
- - **Example 2 - Test + Page Object** ⭐ **NEW**:
689
+ - **Example 2 - Test + Page Object**:
694
690
  ```javascript
695
691
  // Export with Page Object class
696
692
  exportScenarioAsCode({
@@ -702,63 +698,117 @@ Export recorded scenario as executable test code for various frameworks. Automat
702
698
 
703
699
  // Returns JSON with both files:
704
700
  {
705
- "success": true,
701
+ "action": "create_new_file",
702
+ "suggestedFileName": "login_test.spec.ts",
706
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});",
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
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
711
  }
712
712
  ```
713
713
 
714
- - **Example 3 - Append Mode** ⭐ **NEW**:
714
+ - **Selector Cleaning**: Automatically removes unstable patterns:
715
+ - CSS Modules: `Button_primary__2x3yZ` → removed
716
+ - Styled-components: `sc-AbCdEf-0` → removed
717
+ - Emotion: `css-1a2b3c4d` → removed
718
+ - Hash suffixes: `component_a1b2c3d` → removed
719
+ - Prefers stable selectors: `data-testid`, `role`, `aria-label`, semantic attributes
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**:
715
751
  ```javascript
716
752
  // Append test to end of existing file
717
- exportScenarioAsCode({
753
+ appendScenarioToFile({
718
754
  scenarioName: "new_feature_test",
719
755
  language: "playwright-typescript",
720
- appendToFile: "./tests/features.spec.ts"
756
+ targetFile: "./tests/features.spec.ts"
721
757
  })
722
758
 
723
- // Returns:
759
+ // Returns JSON:
724
760
  {
725
- "success": true,
726
- "mode": "append",
727
- "file": "./tests/features.spec.ts",
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});",
728
764
  "testName": "new_feature_test",
729
765
  "insertPosition": "end",
730
- "message": "Test 'new_feature_test' successfully appended to ./tests/features.spec.ts"
766
+ "referenceTestName": null,
767
+ "instruction": "Read file './tests/features.spec.ts', append the testCode at position 'end', then write the file back."
731
768
  }
732
769
  ```
733
770
 
734
- - **Example 4 - Append with Position Control** ⭐ **NEW**:
771
+ - **Example 2 - Insert before specific test**:
735
772
  ```javascript
736
773
  // Insert test before specific test
737
- exportScenarioAsCode({
774
+ appendScenarioToFile({
738
775
  scenarioName: "setup_test",
739
776
  language: "selenium-python",
740
- appendToFile: "./tests/test_suite.py",
777
+ targetFile: "./tests/test_suite.py",
741
778
  insertPosition: "before",
742
- referenceTestName: "main_test",
743
- testName: "test_setup_data" // Override name
779
+ referenceTestName: "test_main",
780
+ testName: "test_setup_data"
744
781
  })
782
+ ```
745
783
 
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"
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"
753
793
  })
754
- ```
755
794
 
756
- - **Selector Cleaning**: Automatically removes unstable patterns:
757
- - CSS Modules: `Button_primary__2x3yZ` → removed
758
- - Styled-components: `sc-AbCdEf-0` → removed
759
- - Emotion: `css-1a2b3c4d` → removed
760
- - Hash suffixes: `component_a1b2c3d` removed
761
- - Prefers stable selectors: `data-testid`, `role`, `aria-label`, semantic attributes
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
+ ```
762
812
 
763
813
  #### generatePageObject ⭐ **NEW**
764
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.
@@ -915,6 +965,106 @@ If you don't need to see the browser window, you can use xvfb (virtual X server)
915
965
 
916
966
  This runs Chrome in GUI mode but on a virtual display (window is not visible).
917
967
 
968
+ ### Tool Filtering with ENABLED_TOOLS
969
+
970
+ By default, all tools are enabled. You can selectively enable only specific tool groups using the `ENABLED_TOOLS` environment variable.
971
+
972
+ **Why filter tools?**
973
+
974
+ Each tool definition is sent to the AI in every request, consuming context tokens. All 43 tools consume ~28k tokens (~14% of context window). By enabling only the groups you need, you can significantly reduce token usage:
975
+
976
+ - **Save tokens:** Fewer tools = less context consumed per request
977
+ - **Reduce costs:** Lower token usage means lower API costs
978
+ - **Improve focus:** AI sees only relevant tools for your workflow
979
+ - **Security/compliance:** Restrict available capabilities when needed
980
+
981
+ **Available Tool Groups:**
982
+
983
+ | Group | Description | Tools (count) |
984
+ |-------|-------------|---------------|
985
+ | `core` | Basic tools | `ping`, `openBrowser` (2) |
986
+ | `interaction` | User interaction | `click`, `type`, `scrollTo`, `waitForElement`, `hover` (5) |
987
+ | `inspection` | Page inspection | `getElement`, `getComputedCss`, `getBoxModel`, `screenshot`, `saveScreenshot` (5) |
988
+ | `debug` | Debugging & network | `getConsoleLogs`, `listNetworkRequests`, `getNetworkRequest`, `filterNetworkRequests` (4) |
989
+ | `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `getAllInteractiveElements`, `findElementsByText` (9) |
990
+ | `recorder` | Scenario recording | `enableRecorder`, `executeScenario`, `listScenarios`, `searchScenarios`, `getScenarioInfo`, `deleteScenario`, `exportScenarioAsCode`, `appendScenarioToFile`, `generatePageObject` (9) |
991
+ | `figma` | Figma integration | `getFigmaFrame`, `compareFigmaToElement`, `getFigmaSpecs`, `parseFigmaUrl`, `listFigmaPages`, `searchFigmaFrames`, `getFigmaComponents`, `getFigmaStyles`, `getFigmaColorPalette` (9) |
992
+
993
+ **Total:** 43 tools across 7 groups
994
+
995
+ **Configuration:**
996
+
997
+ **Claude Desktop** (`~/.claude/mcp_config.json`):
998
+
999
+ ```json
1000
+ {
1001
+ "mcpServers": {
1002
+ "chrometools": {
1003
+ "command": "npx",
1004
+ "args": ["-y", "chrometools-mcp"],
1005
+ "env": {
1006
+ "ENABLED_TOOLS": "core,interaction,inspection"
1007
+ }
1008
+ }
1009
+ }
1010
+ }
1011
+ ```
1012
+
1013
+ **Claude Code** (`~/.claude.json`):
1014
+
1015
+ ```json
1016
+ {
1017
+ "mcpServers": {
1018
+ "chrometools": {
1019
+ "type": "stdio",
1020
+ "command": "npx",
1021
+ "args": ["-y", "chrometools-mcp"],
1022
+ "env": {
1023
+ "ENABLED_TOOLS": "core,interaction,advanced"
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+ ```
1029
+
1030
+ **Format:**
1031
+ - Comma-separated list of group names (e.g., `"core,interaction,advanced"`)
1032
+ - Spaces are automatically trimmed
1033
+ - If not set or empty, all tools are enabled (default behavior)
1034
+
1035
+ **Example configurations:**
1036
+
1037
+ **Basic automation only:**
1038
+ ```json
1039
+ "ENABLED_TOOLS": "core,interaction,inspection"
1040
+ ```
1041
+
1042
+ **Advanced automation with AI:**
1043
+ ```json
1044
+ "ENABLED_TOOLS": "core,interaction,advanced"
1045
+ ```
1046
+
1047
+ **With debugging tools:**
1048
+ ```json
1049
+ "ENABLED_TOOLS": "core,interaction,inspection,debug"
1050
+ ```
1051
+
1052
+ **Figma design validation:**
1053
+ ```json
1054
+ "ENABLED_TOOLS": "core,figma"
1055
+ ```
1056
+
1057
+ **Full automation with recording:**
1058
+ ```json
1059
+ "ENABLED_TOOLS": "core,interaction,inspection,debug,advanced,recorder"
1060
+ ```
1061
+
1062
+ **All tools (default):**
1063
+ ```json
1064
+ "env": {}
1065
+ ```
1066
+ or omit the `env` field entirely.
1067
+
918
1068
  ### Figma API Token Setup
919
1069
 
920
1070
  To use Figma tools, you need to configure your Figma Personal Access Token.
package/index.js CHANGED
@@ -33,6 +33,7 @@ import {filterCssStyles} from './utils/css-utils.js';
33
33
  // Import tool schemas and definitions
34
34
  import * as schemas from './server/tool-schemas.js';
35
35
  import {toolDefinitions} from './server/tool-definitions.js';
36
+ import {getToolsFromGroups, getAllGroupNames} from './server/tool-groups.js';
36
37
 
37
38
  // Import element actions helper
38
39
  import {executeElementAction} from './utils/element-actions.js';
@@ -73,6 +74,16 @@ const debugLog = DEBUG_MODE ? console.error : () => {};
73
74
  // Figma token from environment variable (can be set in MCP config)
74
75
  const FIGMA_TOKEN = process.env.FIGMA_TOKEN || null;
75
76
 
77
+ // Tool filtering - read ENABLED_TOOLS environment variable
78
+ // If set, only enable specified tool groups (comma-separated list)
79
+ // If not set, enable all tools (default behavior)
80
+ const ENABLED_TOOLS = process.env.ENABLED_TOOLS;
81
+ let enabledToolsSet = null;
82
+
83
+ if (ENABLED_TOOLS) {
84
+ const groupNames = ENABLED_TOOLS.split(',').map(g => g.trim()).filter(g => g.length > 0);
85
+ enabledToolsSet = getToolsFromGroups(groupNames);
86
+ }
76
87
  // Get current directory for loading utils
77
88
  const __filename = fileURLToPath(import.meta.url);
78
89
  const __dirname = dirname(__filename);
@@ -164,7 +175,7 @@ try {
164
175
  const server = new Server(
165
176
  {
166
177
  name: "chrometools-mcp",
167
- version: "2.1.0",
178
+ version: "2.4.0",
168
179
  },
169
180
  {
170
181
  capabilities: {
@@ -175,8 +186,13 @@ const server = new Server(
175
186
 
176
187
  // List available tools
177
188
  server.setRequestHandler(ListToolsRequestSchema, async () => {
189
+ // Filter tools based on ENABLED_TOOLS environment variable
190
+ const tools = enabledToolsSet
191
+ ? toolDefinitions.filter(tool => enabledToolsSet.has(tool.name))
192
+ : toolDefinitions;
193
+
178
194
  return {
179
- tools: toolDefinitions,
195
+ tools,
180
196
  };
181
197
  });
182
198
 
@@ -1948,6 +1964,44 @@ async function executeToolInternal(name, args) {
1948
1964
  }
1949
1965
  }
1950
1966
 
1967
+ // Check if current page URL matches scenario's entryUrl
1968
+ // If not, navigate to entryUrl before executing scenario
1969
+ const entryUrl = scenario.metadata?.entryUrl;
1970
+ if (entryUrl) {
1971
+ try {
1972
+ const currentUrl = page.url();
1973
+
1974
+ // Normalize URLs for comparison (remove trailing slashes, hash, some query params)
1975
+ const normalizeUrl = (url) => {
1976
+ try {
1977
+ const urlObj = new URL(url);
1978
+ // Keep protocol, hostname, pathname - ignore some query params like nr, redirect_ts
1979
+ return `${urlObj.protocol}//${urlObj.hostname}${urlObj.pathname}`.replace(/\/$/, '');
1980
+ } catch (e) {
1981
+ return url;
1982
+ }
1983
+ };
1984
+
1985
+ const normalizedCurrent = normalizeUrl(currentUrl);
1986
+ const normalizedEntry = normalizeUrl(entryUrl);
1987
+
1988
+ if (normalizedCurrent !== normalizedEntry) {
1989
+ console.error(`[executeScenario] Current URL (${currentUrl}) doesn't match scenario entryUrl (${entryUrl})`);
1990
+ console.error(`[executeScenario] Navigating to entryUrl...`);
1991
+
1992
+ await page.goto(entryUrl, {
1993
+ waitUntil: 'networkidle2',
1994
+ timeout: 30000
1995
+ });
1996
+
1997
+ console.error(`[executeScenario] Navigation completed`);
1998
+ }
1999
+ } catch (navError) {
2000
+ console.error(`[executeScenario] Warning: Failed to navigate to entryUrl: ${navError.message}`);
2001
+ // Continue anyway - scenario might still work
2002
+ }
2003
+ }
2004
+
1951
2005
  const options = {};
1952
2006
 
1953
2007
  // Pass executeDependencies option if provided
@@ -2038,7 +2092,7 @@ async function executeToolInternal(name, args) {
2038
2092
  };
2039
2093
  }
2040
2094
 
2041
- if (name === "exportScenarioAsCode") {
2095
+ if (name === "appendScenarioToFile") {
2042
2096
  const scenario = await loadScenario(args.scenarioName, false, null);
2043
2097
 
2044
2098
  if (!scenario) {
@@ -2085,83 +2139,157 @@ async function executeToolInternal(name, args) {
2085
2139
  };
2086
2140
  }
2087
2141
 
2088
- // Check if append mode is requested
2089
- if (args.appendToFile) {
2090
- try {
2091
- // Validate file path and extension
2092
- FileAppender.validateFile(args.appendToFile, args.language);
2093
-
2094
- // Read existing file content
2095
- const existingContent = FileAppender.readFile(args.appendToFile);
2142
+ try {
2143
+ // Generate test code only (without imports)
2144
+ const testOnly = generator.generateTestOnly(scenario, {
2145
+ ...options,
2146
+ testName: args.testName
2147
+ });
2096
2148
 
2097
- // Check if file is empty - if so, generate full test with imports
2098
- if (FileAppender.isEmpty(existingContent)) {
2099
- const fullCode = generator.generate(scenario, options);
2100
- FileAppender.writeFile(args.appendToFile, fullCode);
2149
+ // Prepare append options for Claude Code
2150
+ const appendOptions = {
2151
+ insertPosition: args.insertPosition || 'end',
2152
+ referenceTestName: args.referenceTestName
2153
+ };
2101
2154
 
2102
- return {
2103
- content: [{
2104
- type: 'text',
2105
- text: JSON.stringify({
2106
- success: true,
2107
- mode: 'append',
2108
- file: args.appendToFile,
2109
- testName: args.testName || scenario.metadata?.name,
2110
- message: `Test written to empty file: ${args.appendToFile}`
2111
- }, null, 2)
2112
- }]
2113
- };
2155
+ // Generate Page Object if requested
2156
+ let pageObjectData = null;
2157
+ if (args.generatePageObject) {
2158
+ try {
2159
+ const entryUrl = scenario.metadata?.entryUrl;
2160
+ if (entryUrl) {
2161
+ let page;
2162
+ try {
2163
+ page = await getLastOpenPage();
2164
+ const currentUrl = page.url();
2165
+ if (currentUrl !== entryUrl) {
2166
+ await page.goto(entryUrl, { waitUntil: 'networkidle2' });
2167
+ }
2168
+ } catch (error) {
2169
+ page = await getOrCreatePage(entryUrl);
2170
+ }
2171
+
2172
+ const pageObjectOptions = {
2173
+ className: args.pageObjectClassName || null,
2174
+ framework: args.language,
2175
+ includeComments: args.includeComments !== false,
2176
+ groupElements: true
2177
+ };
2178
+
2179
+ const pageObjectResult = await generatePageObject(page, pageObjectOptions);
2180
+ if (pageObjectResult.success) {
2181
+ // Suggest filename based on className
2182
+ const extension = args.language.includes('typescript') ? '.ts' :
2183
+ args.language.includes('java') ? '.java' : '.py';
2184
+ pageObjectData = {
2185
+ code: pageObjectResult.code,
2186
+ className: pageObjectResult.className,
2187
+ suggestedFileName: `${pageObjectResult.className}${extension}`,
2188
+ elementCount: pageObjectResult.elementCount
2189
+ };
2190
+ }
2191
+ }
2192
+ } catch (error) {
2193
+ // Page Object generation failed, continue without it
2114
2194
  }
2195
+ }
2115
2196
 
2116
- // Generate only test function (without imports)
2117
- const testOnly = generator.generateTestOnly(scenario, {
2118
- ...options,
2119
- testName: args.testName
2120
- });
2197
+ // Return JSON with instructions for Claude Code to append the test
2198
+ const result = {
2199
+ action: 'append_test',
2200
+ targetFile: args.targetFile,
2201
+ testCode: testOnly, // Only test code, no imports
2202
+ testName: args.testName || scenario.metadata?.name,
2203
+ insertPosition: appendOptions.insertPosition,
2204
+ referenceTestName: appendOptions.referenceTestName,
2205
+ instruction: `Read file '${args.targetFile}', append the testCode at position '${appendOptions.insertPosition}', then write the file back.`
2206
+ };
2121
2207
 
2122
- // Append test to existing content
2123
- const appendOptions = {
2124
- insertPosition: args.insertPosition || 'end',
2125
- referenceTestName: args.referenceTestName
2126
- };
2208
+ if (pageObjectData) {
2209
+ result.pageObject = pageObjectData;
2210
+ result.instruction += ` Also create a Page Object file '${pageObjectData.suggestedFileName}' with the provided pageObject.code.`;
2211
+ }
2127
2212
 
2128
- const updatedContent = generator.appendTest(existingContent, testOnly, appendOptions);
2213
+ return {
2214
+ content: [{
2215
+ type: 'text',
2216
+ text: JSON.stringify(result, null, 2)
2217
+ }]
2218
+ };
2219
+ } catch (error) {
2220
+ return {
2221
+ content: [{
2222
+ type: 'text',
2223
+ text: JSON.stringify({
2224
+ error: error.message,
2225
+ action: 'append_test',
2226
+ targetFile: args.targetFile
2227
+ }, null, 2)
2228
+ }],
2229
+ isError: true
2230
+ };
2231
+ }
2232
+ }
2129
2233
 
2130
- // Write updated content to file
2131
- FileAppender.writeFile(args.appendToFile, updatedContent);
2234
+ if (name === "exportScenarioAsCode") {
2235
+ const scenario = await loadScenario(args.scenarioName, false, null);
2132
2236
 
2237
+ if (!scenario) {
2238
+ return {
2239
+ content: [{
2240
+ type: 'text',
2241
+ text: JSON.stringify({
2242
+ error: `Scenario "${args.scenarioName}" not found`
2243
+ }, null, 2)
2244
+ }],
2245
+ isError: true
2246
+ };
2247
+ }
2248
+
2249
+ // Select generator based on language
2250
+ let generator;
2251
+ const options = {
2252
+ cleanSelectors: args.cleanSelectors !== false, // default true
2253
+ includeComments: args.includeComments !== false, // default true
2254
+ };
2255
+
2256
+ switch (args.language) {
2257
+ case 'playwright-typescript':
2258
+ generator = new PlaywrightTypeScriptGenerator(options);
2259
+ break;
2260
+ case 'playwright-python':
2261
+ generator = new PlaywrightPythonGenerator(options);
2262
+ break;
2263
+ case 'selenium-python':
2264
+ generator = new SeleniumPythonGenerator(options);
2265
+ break;
2266
+ case 'selenium-java':
2267
+ generator = new SeleniumJavaGenerator(options);
2268
+ break;
2269
+ default:
2133
2270
  return {
2134
2271
  content: [{
2135
2272
  type: 'text',
2136
2273
  text: JSON.stringify({
2137
- success: true,
2138
- mode: 'append',
2139
- file: args.appendToFile,
2140
- testName: args.testName || scenario.metadata?.name,
2141
- insertPosition: appendOptions.insertPosition,
2142
- message: `Test '${args.testName || scenario.metadata?.name}' successfully appended to ${args.appendToFile}`
2143
- }, null, 2)
2144
- }]
2145
- };
2146
- } catch (error) {
2147
- return {
2148
- content: [{
2149
- type: 'text',
2150
- text: JSON.stringify({
2151
- success: false,
2152
- mode: 'append',
2153
- file: args.appendToFile,
2154
- error: error.message
2274
+ error: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
2155
2275
  }, null, 2)
2156
2276
  }],
2157
2277
  isError: true
2158
2278
  };
2159
- }
2160
2279
  }
2161
2280
 
2162
- // Non-append mode: Generate test code
2281
+ // Generate test code with full imports
2163
2282
  const testCode = generator.generate(scenario, options);
2164
2283
 
2284
+ // Generate suggested filename
2285
+ const testName = scenario.metadata?.name || 'test';
2286
+ const extension = args.language.includes('typescript') ? '.spec.ts' :
2287
+ args.language.includes('java') ? 'Test.java' :
2288
+ args.language.includes('python') ? '_test.py' : '.test.js';
2289
+ const suggestedFileName = args.language.includes('java')
2290
+ ? testName.charAt(0).toUpperCase() + testName.slice(1) + 'Test.java'
2291
+ : testName.replace(/\s+/g, '_').toLowerCase() + extension;
2292
+
2165
2293
  // If generatePageObject is requested, also generate Page Object class
2166
2294
  if (args.generatePageObject) {
2167
2295
  try {
@@ -2205,19 +2333,26 @@ async function executeToolInternal(name, args) {
2205
2333
  const pageObjectResult = await generatePageObject(page, pageObjectOptions);
2206
2334
 
2207
2335
  if (pageObjectResult.success) {
2336
+ // Suggest Page Object filename
2337
+ const poExtension = args.language.includes('typescript') ? '.ts' :
2338
+ args.language.includes('java') ? '.java' : '.py';
2339
+ const pageObjectFileName = `${pageObjectResult.className}${poExtension}`;
2340
+
2208
2341
  // Return both test code and Page Object code
2209
2342
  return {
2210
2343
  content: [{
2211
2344
  type: 'text',
2212
2345
  text: JSON.stringify({
2213
- success: true,
2346
+ action: 'create_new_file',
2347
+ suggestedFileName: suggestedFileName,
2214
2348
  testCode: testCode,
2215
- pageObjectCode: pageObjectResult.code,
2216
- pageObjectClassName: pageObjectResult.className,
2217
- framework: args.language,
2218
- scenarioName: args.scenarioName,
2219
- url: pageObjectResult.url,
2220
- elementCount: pageObjectResult.elementCount
2349
+ pageObject: {
2350
+ code: pageObjectResult.code,
2351
+ className: pageObjectResult.className,
2352
+ suggestedFileName: pageObjectFileName,
2353
+ elementCount: pageObjectResult.elementCount
2354
+ },
2355
+ instruction: `Create a new test file '${suggestedFileName}' with the testCode. Also create a Page Object file '${pageObjectFileName}' with the pageObject.code.`
2221
2356
  }, null, 2)
2222
2357
  }]
2223
2358
  };
@@ -2227,10 +2362,11 @@ async function executeToolInternal(name, args) {
2227
2362
  content: [{
2228
2363
  type: 'text',
2229
2364
  text: JSON.stringify({
2230
- success: true,
2365
+ action: 'create_new_file',
2366
+ suggestedFileName: suggestedFileName,
2231
2367
  testCode: testCode,
2232
- pageObjectCode: null,
2233
- warning: 'Page Object generation failed: ' + (pageObjectResult.error || 'Unknown error')
2368
+ warning: 'Page Object generation failed: ' + (pageObjectResult.error || 'Unknown error'),
2369
+ instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
2234
2370
  }, null, 2)
2235
2371
  }]
2236
2372
  };
@@ -2241,10 +2377,11 @@ async function executeToolInternal(name, args) {
2241
2377
  content: [{
2242
2378
  type: 'text',
2243
2379
  text: JSON.stringify({
2244
- success: true,
2380
+ action: 'create_new_file',
2381
+ suggestedFileName: suggestedFileName,
2245
2382
  testCode: testCode,
2246
- pageObjectCode: null,
2247
- warning: 'Page Object generation error: ' + error.message
2383
+ warning: 'Page Object generation error: ' + error.message,
2384
+ instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
2248
2385
  }, null, 2)
2249
2386
  }]
2250
2387
  };
@@ -2255,7 +2392,12 @@ async function executeToolInternal(name, args) {
2255
2392
  return {
2256
2393
  content: [{
2257
2394
  type: 'text',
2258
- text: testCode
2395
+ text: JSON.stringify({
2396
+ action: 'create_new_file',
2397
+ suggestedFileName: suggestedFileName,
2398
+ testCode: testCode,
2399
+ instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
2400
+ }, null, 2)
2259
2401
  }]
2260
2402
  };
2261
2403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrometools-mcp",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
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",
@@ -541,7 +541,7 @@ export const toolDefinitions = [
541
541
  },
542
542
  {
543
543
  name: "exportScenarioAsCode",
544
- description: "Export recorded scenario as executable test code for various frameworks. Automatically cleans unstable selectors (CSS modules, styled-components). Optionally generates Page Object class for the page. Can append to existing test files. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
544
+ description: "Export recorded scenario as executable test code for creating a NEW test file. Automatically cleans unstable selectors (CSS modules, styled-components). 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. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
545
545
  inputSchema: {
546
546
  type: "object",
547
547
  properties: {
@@ -570,9 +570,28 @@ export const toolDefinitions = [
570
570
  type: "string",
571
571
  description: "Page Object class name (optional, auto-generated if not provided)"
572
572
  },
573
- appendToFile: {
573
+ },
574
+ required: ["scenarioName", "language"],
575
+ },
576
+ },
577
+ {
578
+ name: "appendScenarioToFile",
579
+ description: "Append recorded scenario as test code to an EXISTING test file. Automatically cleans unstable selectors (CSS modules, styled-components). Optionally generates Page Object class. Returns JSON with test code and target file - Claude Code will append to the file without overwriting existing tests. To create NEW test files, use 'exportScenarioAsCode' instead. Scenarios are stored in ~/.config/chrometools-mcp/projects/{projectName}/scenarios/. Use global index at ~/.config/chrometools-mcp/index.json to discover available projects and scenarios.",
580
+ inputSchema: {
581
+ type: "object",
582
+ properties: {
583
+ scenarioName: {
584
+ type: "string",
585
+ description: "Name of scenario to export"
586
+ },
587
+ language: {
588
+ type: "string",
589
+ enum: ["playwright-typescript", "playwright-python", "selenium-python", "selenium-java"],
590
+ description: "Target test framework and language"
591
+ },
592
+ targetFile: {
574
593
  type: "string",
575
- description: "Path to existing test file to append to (enables append mode)"
594
+ description: "Path to existing test file to append to (REQUIRED)"
576
595
  },
577
596
  testName: {
578
597
  type: "string",
@@ -581,14 +600,30 @@ export const toolDefinitions = [
581
600
  insertPosition: {
582
601
  type: "string",
583
602
  enum: ["end", "before", "after"],
584
- description: "Where to insert test: 'end' (default), 'before', or 'after' a reference test"
603
+ description: "Where to insert test: 'end' (default - after all tests), 'before' (before reference test), or 'after' (after reference test)"
585
604
  },
586
605
  referenceTestName: {
587
606
  type: "string",
588
- description: "Reference test name for 'before'/'after' insertion"
607
+ description: "Reference test name for 'before'/'after' insertion. Required when insertPosition is 'before' or 'after'"
608
+ },
609
+ cleanSelectors: {
610
+ type: "boolean",
611
+ description: "Remove unstable CSS classes (default: true)"
612
+ },
613
+ includeComments: {
614
+ type: "boolean",
615
+ description: "Include descriptive comments (default: true)"
616
+ },
617
+ generatePageObject: {
618
+ type: "boolean",
619
+ description: "Also generate Page Object class for the page (default: false)"
620
+ },
621
+ pageObjectClassName: {
622
+ type: "string",
623
+ description: "Page Object class name (optional, auto-generated if not provided)"
589
624
  },
590
625
  },
591
- required: ["scenarioName", "language"],
626
+ required: ["scenarioName", "language", "targetFile"],
592
627
  },
593
628
  },
594
629
  {
@@ -0,0 +1,82 @@
1
+ /**
2
+ * server/tool-groups.js
3
+ *
4
+ * Tool group definitions for filtering
5
+ */
6
+
7
+ export const toolGroups = {
8
+ core: ['ping', 'openBrowser'],
9
+
10
+ interaction: ['click', 'type', 'scrollTo', 'waitForElement', 'hover'],
11
+
12
+ inspection: ['getElement', 'getComputedCss', 'getBoxModel', 'screenshot', 'saveScreenshot'],
13
+
14
+ debug: [
15
+ 'getConsoleLogs',
16
+ 'listNetworkRequests',
17
+ 'getNetworkRequest',
18
+ 'filterNetworkRequests'
19
+ ],
20
+
21
+ advanced: [
22
+ 'executeScript',
23
+ 'setStyles',
24
+ 'setViewport',
25
+ 'getViewport',
26
+ 'navigateTo',
27
+ 'smartFindElement',
28
+ 'analyzePage',
29
+ 'getAllInteractiveElements',
30
+ 'findElementsByText'
31
+ ],
32
+
33
+ recorder: [
34
+ 'enableRecorder',
35
+ 'executeScenario',
36
+ 'listScenarios',
37
+ 'searchScenarios',
38
+ 'getScenarioInfo',
39
+ 'deleteScenario',
40
+ 'exportScenarioAsCode',
41
+ 'appendScenarioToFile',
42
+ 'generatePageObject'
43
+ ],
44
+
45
+ figma: [
46
+ 'getFigmaFrame',
47
+ 'compareFigmaToElement',
48
+ 'getFigmaSpecs',
49
+ 'parseFigmaUrl',
50
+ 'listFigmaPages',
51
+ 'searchFigmaFrames',
52
+ 'getFigmaComponents',
53
+ 'getFigmaStyles',
54
+ 'getFigmaColorPalette'
55
+ ]
56
+ };
57
+
58
+ /**
59
+ * Get all tool names from specified groups
60
+ * @param {string[]} groupNames - Array of group names (e.g., ['core', 'interaction'])
61
+ * @returns {Set<string>} - Set of tool names
62
+ */
63
+ export function getToolsFromGroups(groupNames) {
64
+ const tools = new Set();
65
+
66
+ for (const groupName of groupNames) {
67
+ const group = toolGroups[groupName];
68
+ if (group) {
69
+ group.forEach(tool => tools.add(tool));
70
+ }
71
+ }
72
+
73
+ return tools;
74
+ }
75
+
76
+ /**
77
+ * Get all available group names
78
+ * @returns {string[]} - Array of group names
79
+ */
80
+ export function getAllGroupNames() {
81
+ return Object.keys(toolGroups);
82
+ }