chrome-devtools-mcp-for-extension 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://npmjs.org/package/chrome-devtools-mcp-for-extension)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**AI-powered Chrome extension development with automated testing, debugging, and Web Store submission**
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
System extensions auto-load, development extensions easy to configure.
|
|
8
8
|
|
|
9
9
|
**Built for:** Claude Code, Cursor, VS Code Copilot, Cline, and other MCP-compatible AI tools
|
|
10
10
|
|
|
@@ -18,46 +18,75 @@ AI-powered Chrome extension development with automated testing, debugging, and W
|
|
|
18
18
|
- **Extension Development**: Can't test in real user environments
|
|
19
19
|
|
|
20
20
|
### The Solution
|
|
21
|
-
- ✅ **
|
|
21
|
+
- ✅ **System extensions auto-load**: Your installed Chrome extensions work automatically
|
|
22
|
+
- ✅ **Easy dev extension setup**: Simple `--loadExtensionsDir` configuration for development
|
|
22
23
|
- ✅ **Real environment**: Tests with your actual extensions and settings
|
|
23
|
-
- ✅ **
|
|
24
|
-
- ✅ **Always enabled**: Extensions work automatically, no configuration needed
|
|
24
|
+
- ✅ **Independent instance**: Runs alongside your regular Chrome without conflicts
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
28
|
## 🚀 Quick Start
|
|
29
29
|
|
|
30
|
-
### 1.
|
|
30
|
+
### 1. Add Configuration
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
Add the following to `~/.claude.json`:
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"mcpServers": {
|
|
37
|
+
"chrome-devtools-extension": {
|
|
38
|
+
"type": "stdio",
|
|
39
|
+
"command": "npx",
|
|
40
|
+
"args": ["chrome-devtools-mcp-for-extension@latest"],
|
|
41
|
+
"env": {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Note**: For other MCP clients (Cursor, VS Code Copilot, Cline), add to your client's global configuration file.
|
|
48
|
+
|
|
49
|
+
### 2. Restart Your AI Client
|
|
50
|
+
|
|
51
|
+
### 3. Test It
|
|
52
|
+
|
|
53
|
+
Ask your AI:
|
|
54
|
+
```
|
|
55
|
+
"List all my Chrome extensions"
|
|
35
56
|
```
|
|
36
57
|
|
|
37
|
-
|
|
58
|
+
✅ You should see your installed Chrome extensions
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 🔧 Load Development Extensions (Optional)
|
|
38
63
|
|
|
39
|
-
|
|
64
|
+
To test your own extensions under development, add `--loadExtensionsDir`:
|
|
40
65
|
|
|
41
66
|
```json
|
|
42
67
|
{
|
|
43
68
|
"mcpServers": {
|
|
44
69
|
"chrome-devtools-extension": {
|
|
45
70
|
"command": "npx",
|
|
46
|
-
"args": [
|
|
71
|
+
"args": [
|
|
72
|
+
"chrome-devtools-mcp-for-extension@latest",
|
|
73
|
+
"--loadExtensionsDir=/path/to/your/extensions"
|
|
74
|
+
]
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
77
|
}
|
|
50
78
|
```
|
|
51
79
|
|
|
80
|
+
**Directory structure example:**
|
|
81
|
+
```
|
|
82
|
+
/path/to/your/extensions/
|
|
83
|
+
├── my-extension-1/
|
|
84
|
+
│ └── manifest.json
|
|
85
|
+
├── my-extension-2/
|
|
86
|
+
│ └── manifest.json
|
|
87
|
+
```
|
|
52
88
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
### 3. Verify installation
|
|
56
|
-
Ask your AI: **"List all my Chrome extensions"**
|
|
57
|
-
✅ You should see your installed extensions listed
|
|
58
|
-
|
|
59
|
-
### 4. Start developing
|
|
60
|
-
See [Common Workflows](#-common-workflows) below for typical use cases
|
|
89
|
+
**More options**: See [MCP Configuration Guide](docs/mcp-configuration-guide.md)
|
|
61
90
|
|
|
62
91
|
---
|
|
63
92
|
|
|
@@ -159,15 +188,20 @@ Quick reference for the 3 core extension tools:
|
|
|
159
188
|
<summary>⚙️ Advanced Configuration</summary>
|
|
160
189
|
|
|
161
190
|
## Auto-load Development Extension
|
|
191
|
+
|
|
192
|
+
Add to `~/.claude.json`:
|
|
193
|
+
|
|
162
194
|
```json
|
|
163
195
|
{
|
|
164
196
|
"mcpServers": {
|
|
165
197
|
"chrome-devtools-extension": {
|
|
198
|
+
"type": "stdio",
|
|
166
199
|
"command": "npx",
|
|
167
200
|
"args": [
|
|
168
201
|
"chrome-devtools-mcp-for-extension@latest",
|
|
169
202
|
"--loadExtension=/path/to/your/extension"
|
|
170
|
-
]
|
|
203
|
+
],
|
|
204
|
+
"env": {}
|
|
171
205
|
}
|
|
172
206
|
}
|
|
173
207
|
}
|
|
@@ -176,10 +210,14 @@ Quick reference for the 3 core extension tools:
|
|
|
176
210
|
⚠️ **Note**: `--loadExtension` flag may be deprecated in Chrome 137+. Using system profile (default) is recommended.
|
|
177
211
|
|
|
178
212
|
## Debug Mode
|
|
213
|
+
|
|
214
|
+
Add to `~/.claude.json`:
|
|
215
|
+
|
|
179
216
|
```json
|
|
180
217
|
{
|
|
181
218
|
"mcpServers": {
|
|
182
219
|
"chrome-devtools-extension": {
|
|
220
|
+
"type": "stdio",
|
|
183
221
|
"command": "npx",
|
|
184
222
|
"args": ["chrome-devtools-mcp-for-extension@latest"],
|
|
185
223
|
"env": {
|
|
@@ -191,15 +229,20 @@ Quick reference for the 3 core extension tools:
|
|
|
191
229
|
```
|
|
192
230
|
|
|
193
231
|
## Custom Chrome Channel
|
|
232
|
+
|
|
233
|
+
Add to `~/.claude.json`:
|
|
234
|
+
|
|
194
235
|
```json
|
|
195
236
|
{
|
|
196
237
|
"mcpServers": {
|
|
197
238
|
"chrome-devtools-extension": {
|
|
239
|
+
"type": "stdio",
|
|
198
240
|
"command": "npx",
|
|
199
241
|
"args": [
|
|
200
242
|
"chrome-devtools-mcp-for-extension@latest",
|
|
201
243
|
"--channel=canary"
|
|
202
|
-
]
|
|
244
|
+
],
|
|
245
|
+
"env": {}
|
|
203
246
|
}
|
|
204
247
|
}
|
|
205
248
|
}
|
|
@@ -208,15 +251,20 @@ Quick reference for the 3 core extension tools:
|
|
|
208
251
|
Options: `stable` (default), `beta`, `dev`, `canary`
|
|
209
252
|
|
|
210
253
|
## Isolated Profile Mode
|
|
254
|
+
|
|
255
|
+
Add to `~/.claude.json`:
|
|
256
|
+
|
|
211
257
|
```json
|
|
212
258
|
{
|
|
213
259
|
"mcpServers": {
|
|
214
260
|
"chrome-devtools-extension": {
|
|
261
|
+
"type": "stdio",
|
|
215
262
|
"command": "npx",
|
|
216
263
|
"args": [
|
|
217
264
|
"chrome-devtools-mcp-for-extension@latest",
|
|
218
265
|
"--isolated"
|
|
219
|
-
]
|
|
266
|
+
],
|
|
267
|
+
"env": {}
|
|
220
268
|
}
|
|
221
269
|
}
|
|
222
270
|
}
|
|
@@ -408,9 +456,22 @@ interface ManifestValidation {
|
|
|
408
456
|
- Not: `/your-extension/dist/manifest.json`
|
|
409
457
|
|
|
410
458
|
**Solution:**
|
|
459
|
+
|
|
460
|
+
Update `~/.claude.json`:
|
|
411
461
|
```json
|
|
412
|
-
|
|
413
|
-
"
|
|
462
|
+
{
|
|
463
|
+
"mcpServers": {
|
|
464
|
+
"chrome-devtools-extension": {
|
|
465
|
+
"type": "stdio",
|
|
466
|
+
"command": "npx",
|
|
467
|
+
"args": [
|
|
468
|
+
"chrome-devtools-mcp-for-extension@latest",
|
|
469
|
+
"--loadExtension=/correct/path"
|
|
470
|
+
],
|
|
471
|
+
"env": {}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
414
475
|
```
|
|
415
476
|
|
|
416
477
|
## Service Worker Not Inspecting
|
|
@@ -493,34 +554,66 @@ npx chrome-devtools-mcp-for-extension@latest --isolated
|
|
|
493
554
|
|
|
494
555
|
**Chrome拡張機能開発用のAI支援MCPサーバー**
|
|
495
556
|
|
|
496
|
-
|
|
557
|
+
システム拡張機能を自動ロード、開発用拡張機能も簡単設定
|
|
497
558
|
|
|
498
|
-
##
|
|
559
|
+
## クイックスタート
|
|
499
560
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
claude
|
|
561
|
+
### 1. 設定を追加
|
|
562
|
+
|
|
563
|
+
`~/.claude.json` に以下を追加:
|
|
564
|
+
|
|
565
|
+
```json
|
|
566
|
+
{
|
|
567
|
+
"mcpServers": {
|
|
568
|
+
"chrome-devtools-extension": {
|
|
569
|
+
"type": "stdio",
|
|
570
|
+
"command": "npx",
|
|
571
|
+
"args": ["chrome-devtools-mcp-for-extension@latest"],
|
|
572
|
+
"env": {}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### 2. AIクライアントを再起動
|
|
579
|
+
|
|
580
|
+
### 3. 動作確認
|
|
581
|
+
|
|
582
|
+
AIに質問:
|
|
583
|
+
```
|
|
584
|
+
「Chrome拡張機能を一覧表示して」
|
|
503
585
|
```
|
|
504
586
|
|
|
505
|
-
|
|
587
|
+
✅ インストール済みの拡張機能が表示されます
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## 開発用拡張機能のロード(--loadExtensionsDirあり)
|
|
592
|
+
|
|
593
|
+
開発中の拡張機能をテストする場合:
|
|
594
|
+
|
|
506
595
|
```json
|
|
507
596
|
{
|
|
508
597
|
"mcpServers": {
|
|
509
598
|
"chrome-devtools-extension": {
|
|
510
599
|
"command": "npx",
|
|
511
|
-
"args": [
|
|
600
|
+
"args": [
|
|
601
|
+
"chrome-devtools-mcp-for-extension@latest",
|
|
602
|
+
"--loadExtensionsDir=/path/to/your/extensions"
|
|
603
|
+
]
|
|
512
604
|
}
|
|
513
605
|
}
|
|
514
606
|
}
|
|
515
607
|
```
|
|
516
608
|
|
|
609
|
+
---
|
|
610
|
+
|
|
517
611
|
## 主な機能
|
|
518
612
|
|
|
519
613
|
- 🧩 **拡張機能の開発・デバッグ・リロード**: ライブ開発環境
|
|
520
614
|
- 🏪 **Chrome Web Store への自動申請**: スクリーンショット生成付き
|
|
521
615
|
- 🔧 **実環境でのブラウザテスト**: 既存の拡張機能と共存
|
|
522
616
|
- 🐛 **高度なデバッグ**: サービスワーカー検査、コンソール監視
|
|
523
|
-
- 📸 **ストア用画像の自動生成**: 複数サイズ対応
|
|
524
617
|
|
|
525
618
|
## 使用例
|
|
526
619
|
|
|
@@ -531,14 +624,7 @@ claude mcp add --scope user chrome-devtools-extension npx chrome-devtools-mcp-fo
|
|
|
531
624
|
「Web Storeに申請して」
|
|
532
625
|
```
|
|
533
626
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
- ✅ **設定不要**: システムのChromeプロファイルを自動使用
|
|
537
|
-
- ✅ **実環境テスト**: 実際の拡張機能・設定でテスト可能
|
|
538
|
-
- ✅ **コピー不要**: プロファイルの直接アクセス、即座に同期
|
|
539
|
-
- ✅ **常時有効**: 拡張機能が自動的に有効化
|
|
540
|
-
|
|
541
|
-
詳細は英語セクションを参照してください。
|
|
627
|
+
その他の詳細は英語セクションを参照してください。
|
|
542
628
|
|
|
543
629
|
---
|
|
544
630
|
|
package/build/src/McpResponse.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { formatConsoleEvent } from './formatters/consoleFormatter.js';
|
|
2
2
|
import { getFormattedHeaderValue, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
|
|
3
3
|
import { formatA11ySnapshot } from './formatters/snapshotFormatter.js';
|
|
4
|
+
import { formatExtensionsPage } from './formatters/extensionsPageFormatter.js';
|
|
5
|
+
import { getDevelopmentExtensionPaths } from './browser.js';
|
|
4
6
|
import { paginate } from './utils/pagination.js';
|
|
5
7
|
export class McpResponse {
|
|
6
8
|
#includePages = false;
|
|
@@ -120,9 +122,21 @@ Call browser_handle_dialog to handle it before continuing.`);
|
|
|
120
122
|
if (this.#includeSnapshot) {
|
|
121
123
|
const snapshot = context.getTextSnapshot();
|
|
122
124
|
if (snapshot) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
const pageUrl = context.getSelectedPage().url();
|
|
126
|
+
const isExtensionsPage = pageUrl.startsWith('chrome://extensions');
|
|
127
|
+
let formattedSnapshot;
|
|
128
|
+
if (isExtensionsPage) {
|
|
129
|
+
// Use structured extensions page formatter
|
|
130
|
+
const developmentPaths = getDevelopmentExtensionPaths();
|
|
131
|
+
formattedSnapshot = formatExtensionsPage(snapshot.root, developmentPaths);
|
|
132
|
+
response.push(formattedSnapshot);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Use standard accessibility tree formatter
|
|
136
|
+
formattedSnapshot = formatA11ySnapshot(snapshot.root);
|
|
137
|
+
response.push('## Page content');
|
|
138
|
+
response.push(formattedSnapshot);
|
|
139
|
+
}
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
response.push(...this.#getIncludeNetworkRequestsData(context));
|
package/build/src/browser.js
CHANGED
|
@@ -225,8 +225,15 @@ function discoverSystemExtensions(channel) {
|
|
|
225
225
|
}
|
|
226
226
|
return extensionPaths;
|
|
227
227
|
}
|
|
228
|
+
// Store development extension paths globally for later retrieval
|
|
229
|
+
let developmentExtensionPaths = [];
|
|
230
|
+
export function getDevelopmentExtensionPaths() {
|
|
231
|
+
return developmentExtensionPaths;
|
|
232
|
+
}
|
|
228
233
|
export async function launch(options) {
|
|
229
234
|
const { channel, executablePath, customDevTools, headless, isolated, loadExtension, loadExtensionsDir, loadSystemExtensions, } = options;
|
|
235
|
+
// Reset development extension paths
|
|
236
|
+
developmentExtensionPaths = [];
|
|
230
237
|
const profileDirName = channel && channel !== 'stable'
|
|
231
238
|
? `chrome-profile-${channel}`
|
|
232
239
|
: 'chrome-profile';
|
|
@@ -260,6 +267,7 @@ export async function launch(options) {
|
|
|
260
267
|
const manifest = JSON.parse(manifestContent);
|
|
261
268
|
if (manifest.manifest_version) {
|
|
262
269
|
extensionPaths.push(loadExtension);
|
|
270
|
+
developmentExtensionPaths.push(loadExtension); // Track as development extension
|
|
263
271
|
console.error(`✅ Single extension validated: ${loadExtension}`);
|
|
264
272
|
}
|
|
265
273
|
else {
|
|
@@ -281,6 +289,7 @@ export async function launch(options) {
|
|
|
281
289
|
if (loadExtensionsDir) {
|
|
282
290
|
const scannedExtensions = scanExtensionsDirectory(loadExtensionsDir);
|
|
283
291
|
extensionPaths.push(...scannedExtensions);
|
|
292
|
+
developmentExtensionPaths.push(...scannedExtensions); // Track as development extensions
|
|
284
293
|
}
|
|
285
294
|
// System extension discovery (default: true unless isolated flag is set)
|
|
286
295
|
const shouldLoadSystemExtensions = loadSystemExtensions ?? !isolated;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format chrome://extensions page with structured output
|
|
3
|
+
*/
|
|
4
|
+
export function formatExtensionsPage(root, developmentExtensionPaths) {
|
|
5
|
+
const groups = groupExtensions(root, developmentExtensionPaths);
|
|
6
|
+
let result = '# Chrome Extensions Page\n\n';
|
|
7
|
+
// Developer mode section
|
|
8
|
+
const devModeSwitch = findDeveloperModeSwitch(root);
|
|
9
|
+
if (devModeSwitch) {
|
|
10
|
+
const isChecked = devModeSwitch.checked === true;
|
|
11
|
+
result += `## Developer Mode: ${isChecked ? 'ON ✓' : 'OFF ✗'}\n`;
|
|
12
|
+
result += ` uid=${devModeSwitch.id} switch "${devModeSwitch.name || devModeSwitch.description || 'Developer mode'}" ${isChecked ? '[checked]' : '[unchecked]'}\n\n`;
|
|
13
|
+
}
|
|
14
|
+
// Development extensions first
|
|
15
|
+
const devGroups = groups.filter(g => g.isDevelopment);
|
|
16
|
+
const systemGroups = groups.filter(g => !g.isDevelopment);
|
|
17
|
+
if (devGroups.length > 0) {
|
|
18
|
+
result += `## 🔧 Development Extensions (Your Project)\n\n`;
|
|
19
|
+
for (const group of devGroups) {
|
|
20
|
+
result += formatExtensionGroup(group);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (systemGroups.length > 0) {
|
|
24
|
+
result += `## 📦 System Extensions\n\n`;
|
|
25
|
+
for (const group of systemGroups) {
|
|
26
|
+
result += formatExtensionGroup(group);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
function formatExtensionGroup(group) {
|
|
32
|
+
let result = '';
|
|
33
|
+
// Header with status indicators
|
|
34
|
+
const statusIcon = group.enabled ? '✓' : '✗';
|
|
35
|
+
const errorIcon = group.errors.length > 0 ? ' ⚠️ HAS ERRORS' : '';
|
|
36
|
+
const devIcon = group.isDevelopment ? ' ⭐' : '';
|
|
37
|
+
result += `### Extension: "${group.name}" v${group.version} [${group.enabled ? 'ENABLED' : 'DISABLED'}]${statusIcon}${devIcon}${errorIcon}\n`;
|
|
38
|
+
if (group.location) {
|
|
39
|
+
result += ` Location: ${group.location}\n`;
|
|
40
|
+
}
|
|
41
|
+
// Find and label key buttons
|
|
42
|
+
const buttons = {
|
|
43
|
+
details: findButton(group.nodes, ['Details', 'View', 'Show details']),
|
|
44
|
+
remove: findButton(group.nodes, ['Remove', 'Delete', 'Uninstall']),
|
|
45
|
+
reload: findButton(group.nodes, ['Reload', 'Refresh']),
|
|
46
|
+
errors: findButton(group.nodes, ['Errors', 'Error', 'View errors']),
|
|
47
|
+
enabled: findSwitch(group.nodes, ['Enabled', 'Enable']),
|
|
48
|
+
};
|
|
49
|
+
// Display buttons with clear labels
|
|
50
|
+
if (buttons.details) {
|
|
51
|
+
result += ` uid=${buttons.details.id} button "Details"\n`;
|
|
52
|
+
}
|
|
53
|
+
if (buttons.remove) {
|
|
54
|
+
result += ` uid=${buttons.remove.id} button "Remove"\n`;
|
|
55
|
+
}
|
|
56
|
+
if (buttons.reload) {
|
|
57
|
+
result += ` uid=${buttons.reload.id} button "Reload" ${group.isDevelopment ? '← USE THIS TO RELOAD YOUR EXTENSION' : ''}\n`;
|
|
58
|
+
}
|
|
59
|
+
if (buttons.errors) {
|
|
60
|
+
result += ` uid=${buttons.errors.id} button "Errors" [clickable]\n`;
|
|
61
|
+
}
|
|
62
|
+
if (buttons.enabled) {
|
|
63
|
+
const isChecked = buttons.enabled.checked === true;
|
|
64
|
+
result += ` uid=${buttons.enabled.id} switch "Enabled" ${isChecked ? '[checked]' : '[unchecked]'}\n`;
|
|
65
|
+
}
|
|
66
|
+
// Display errors if any
|
|
67
|
+
if (group.errors.length > 0) {
|
|
68
|
+
result += `\n ⚠️ Errors:\n`;
|
|
69
|
+
for (const error of group.errors) {
|
|
70
|
+
result += ` - ${error}\n`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
result += '\n';
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function groupExtensions(root, developmentPaths) {
|
|
77
|
+
const groups = [];
|
|
78
|
+
// Find extension cards (typically have role="article" or contain extension name as heading)
|
|
79
|
+
const findExtensionCards = (node) => {
|
|
80
|
+
// Look for extension name patterns
|
|
81
|
+
if (node.role === 'heading' &&
|
|
82
|
+
node.level === 3 &&
|
|
83
|
+
node.name &&
|
|
84
|
+
!node.name.includes('Chrome Web Store')) {
|
|
85
|
+
// This is likely an extension name
|
|
86
|
+
const extensionName = node.name;
|
|
87
|
+
// Find sibling nodes that belong to this extension
|
|
88
|
+
const siblings = [];
|
|
89
|
+
let version = '';
|
|
90
|
+
let enabled = true;
|
|
91
|
+
let location = '';
|
|
92
|
+
const errors = [];
|
|
93
|
+
// Collect related nodes (buttons, switches, text)
|
|
94
|
+
const collectNodes = (n, depth = 0) => {
|
|
95
|
+
if (depth > 5)
|
|
96
|
+
return; // Limit depth
|
|
97
|
+
siblings.push(n);
|
|
98
|
+
// Extract version
|
|
99
|
+
if (n.role === 'text' && n.name && /^v?\d+\.\d+/.test(n.name)) {
|
|
100
|
+
version = n.name;
|
|
101
|
+
}
|
|
102
|
+
// Extract location for development extensions
|
|
103
|
+
if (n.role === 'text' &&
|
|
104
|
+
n.name &&
|
|
105
|
+
(n.name.includes('/') || n.name.includes('\\'))) {
|
|
106
|
+
location = n.name;
|
|
107
|
+
}
|
|
108
|
+
// Extract errors
|
|
109
|
+
if ((n.role === 'text' || n.role === 'paragraph') &&
|
|
110
|
+
n.name &&
|
|
111
|
+
(n.name.toLowerCase().includes('error') ||
|
|
112
|
+
n.name.toLowerCase().includes('failed') ||
|
|
113
|
+
n.name.toLowerCase().includes('warning'))) {
|
|
114
|
+
errors.push(n.name);
|
|
115
|
+
}
|
|
116
|
+
for (const child of n.children) {
|
|
117
|
+
collectNodes(child, depth + 1);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
// Collect from parent's children
|
|
121
|
+
collectNodes(node);
|
|
122
|
+
// Determine if it's a development extension
|
|
123
|
+
const isDevelopment = developmentPaths.some(path => location.includes(path) ||
|
|
124
|
+
extensionName.toLowerCase().includes('development'));
|
|
125
|
+
groups.push({
|
|
126
|
+
name: extensionName,
|
|
127
|
+
version: version || '0.0.0',
|
|
128
|
+
enabled,
|
|
129
|
+
isDevelopment,
|
|
130
|
+
location: location || undefined,
|
|
131
|
+
nodes: siblings,
|
|
132
|
+
errors,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
for (const child of node.children) {
|
|
136
|
+
findExtensionCards(child);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
findExtensionCards(root);
|
|
140
|
+
return groups;
|
|
141
|
+
}
|
|
142
|
+
function findButton(nodes, keywords) {
|
|
143
|
+
for (const node of nodes) {
|
|
144
|
+
if (node.role === 'button') {
|
|
145
|
+
const text = node.name || node.description || node.roledescription || '';
|
|
146
|
+
for (const keyword of keywords) {
|
|
147
|
+
if (text.toLowerCase().includes(keyword.toLowerCase())) {
|
|
148
|
+
return node;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
function findSwitch(nodes, keywords) {
|
|
156
|
+
for (const node of nodes) {
|
|
157
|
+
if (node.role === 'switch') {
|
|
158
|
+
const text = node.name || node.description || node.roledescription || '';
|
|
159
|
+
for (const keyword of keywords) {
|
|
160
|
+
if (text.toLowerCase().includes(keyword.toLowerCase())) {
|
|
161
|
+
return node;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
function findDeveloperModeSwitch(root) {
|
|
169
|
+
const search = (node) => {
|
|
170
|
+
if (node.role === 'switch') {
|
|
171
|
+
const text = node.name || node.description || node.roledescription || '';
|
|
172
|
+
if (text.toLowerCase().includes('developer')) {
|
|
173
|
+
return node;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const child of node.children) {
|
|
177
|
+
const found = search(child);
|
|
178
|
+
if (found)
|
|
179
|
+
return found;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
};
|
|
183
|
+
return search(root);
|
|
184
|
+
}
|
|
@@ -9,10 +9,15 @@ export function formatA11ySnapshot(serializedAXNodeRoot, depth = 0) {
|
|
|
9
9
|
return result;
|
|
10
10
|
}
|
|
11
11
|
function getAttributes(serializedAXNodeRoot) {
|
|
12
|
+
// Prefer description for buttons/switches with empty names
|
|
13
|
+
const displayLabel = serializedAXNodeRoot.name ||
|
|
14
|
+
serializedAXNodeRoot.description ||
|
|
15
|
+
serializedAXNodeRoot.roledescription ||
|
|
16
|
+
'';
|
|
12
17
|
const attributes = [
|
|
13
18
|
`uid=${serializedAXNodeRoot.id}`,
|
|
14
19
|
serializedAXNodeRoot.role,
|
|
15
|
-
`"${
|
|
20
|
+
`"${displayLabel}"`,
|
|
16
21
|
];
|
|
17
22
|
// Value properties
|
|
18
23
|
const valueProperties = [
|
|
@@ -71,9 +71,114 @@ export const listExtensions = defineTool({
|
|
|
71
71
|
});
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
|
+
export const getExtensionInfo = defineTool({
|
|
75
|
+
name: 'get_extension_info',
|
|
76
|
+
description: `Get detailed information about a specific Chrome extension including its current state, version, and any errors.`,
|
|
77
|
+
annotations: {
|
|
78
|
+
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
79
|
+
readOnlyHint: true,
|
|
80
|
+
},
|
|
81
|
+
schema: {
|
|
82
|
+
extensionName: z
|
|
83
|
+
.string()
|
|
84
|
+
.describe('The name or partial name of the extension to get info about'),
|
|
85
|
+
},
|
|
86
|
+
handler: async (request, response, context) => {
|
|
87
|
+
const page = context.getSelectedPage();
|
|
88
|
+
const { extensionName } = request.params;
|
|
89
|
+
await context.waitForEventsAfterAction(async () => {
|
|
90
|
+
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
91
|
+
const extensionInfo = await page.evaluate((searchName) => {
|
|
92
|
+
const extensionCards = document.querySelectorAll('extensions-item');
|
|
93
|
+
for (const card of Array.from(extensionCards)) {
|
|
94
|
+
const shadowRoot = card.shadowRoot;
|
|
95
|
+
if (shadowRoot) {
|
|
96
|
+
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
97
|
+
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
98
|
+
const description = shadowRoot.querySelector('#description')?.textContent?.trim() ||
|
|
99
|
+
'';
|
|
100
|
+
const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
|
|
101
|
+
const enableToggle = shadowRoot.querySelector('#enable-toggle');
|
|
102
|
+
const enabled = enableToggle?.getAttribute('checked') === '';
|
|
103
|
+
const id = card.getAttribute('id') || 'unknown';
|
|
104
|
+
const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
|
|
105
|
+
const hasErrors = errorsBadge
|
|
106
|
+
? parseInt(errorsBadge.textContent?.trim() || '0') > 0
|
|
107
|
+
: false;
|
|
108
|
+
// Get error details if available
|
|
109
|
+
const errors = [];
|
|
110
|
+
if (hasErrors) {
|
|
111
|
+
const errorsButton = shadowRoot.querySelector('#errors-button');
|
|
112
|
+
if (errorsButton) {
|
|
113
|
+
// Try to get error text from the errors section
|
|
114
|
+
const errorsList = shadowRoot.querySelectorAll('.error-list .error-message');
|
|
115
|
+
errorsList.forEach(err => {
|
|
116
|
+
const errorText = err.textContent?.trim();
|
|
117
|
+
if (errorText) {
|
|
118
|
+
errors.push(errorText);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Check if this is a development extension
|
|
124
|
+
const detailsView = shadowRoot.querySelector('extensions-detail-view');
|
|
125
|
+
const isDevelopment = detailsView ?
|
|
126
|
+
detailsView.shadowRoot?.querySelector('#load-path')?.textContent?.trim() : undefined;
|
|
127
|
+
return {
|
|
128
|
+
found: true,
|
|
129
|
+
id,
|
|
130
|
+
name,
|
|
131
|
+
version,
|
|
132
|
+
description,
|
|
133
|
+
enabled,
|
|
134
|
+
hasErrors,
|
|
135
|
+
errors,
|
|
136
|
+
path: isDevelopment || 'Not a development extension',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return { found: false };
|
|
142
|
+
}, extensionName);
|
|
143
|
+
if (extensionInfo.found) {
|
|
144
|
+
response.appendResponseLine(`## Extension: ${extensionInfo.name}`);
|
|
145
|
+
response.appendResponseLine('');
|
|
146
|
+
response.appendResponseLine(`**ID:** ${extensionInfo.id}`);
|
|
147
|
+
response.appendResponseLine(`**Version:** ${extensionInfo.version}`);
|
|
148
|
+
response.appendResponseLine(`**Status:** ${extensionInfo.enabled ? '✅ Enabled' : '❌ Disabled'}`);
|
|
149
|
+
if (extensionInfo.description) {
|
|
150
|
+
response.appendResponseLine(`**Description:** ${extensionInfo.description}`);
|
|
151
|
+
}
|
|
152
|
+
if (extensionInfo.path !== 'Not a development extension') {
|
|
153
|
+
response.appendResponseLine(`**Path:** ${extensionInfo.path}`);
|
|
154
|
+
}
|
|
155
|
+
response.appendResponseLine('');
|
|
156
|
+
if (extensionInfo.hasErrors) {
|
|
157
|
+
response.appendResponseLine('⚠️ **Errors:**');
|
|
158
|
+
if (extensionInfo.errors.length > 0) {
|
|
159
|
+
extensionInfo.errors.forEach(err => {
|
|
160
|
+
response.appendResponseLine(` - ${err}`);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
response.appendResponseLine(' Extension has errors (details not available)');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
response.appendResponseLine('✅ No errors detected');
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
|
|
173
|
+
response.appendResponseLine('');
|
|
174
|
+
response.appendResponseLine('💡 Use `list_extensions` to see all installed extensions');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
});
|
|
74
179
|
export const reloadExtension = defineTool({
|
|
75
180
|
name: 'reload_extension',
|
|
76
|
-
description: `Reload a Chrome extension to apply changes during development.`,
|
|
181
|
+
description: `Reload a Chrome extension to apply changes during development. Checks extension state before and after reload.`,
|
|
77
182
|
annotations: {
|
|
78
183
|
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
79
184
|
readOnlyHint: false,
|
|
@@ -88,6 +193,39 @@ export const reloadExtension = defineTool({
|
|
|
88
193
|
const { extensionName } = request.params;
|
|
89
194
|
await context.waitForEventsAfterAction(async () => {
|
|
90
195
|
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
196
|
+
// Get extension info before reload
|
|
197
|
+
const beforeState = await page.evaluate((searchName) => {
|
|
198
|
+
const extensionCards = document.querySelectorAll('extensions-item');
|
|
199
|
+
for (const card of Array.from(extensionCards)) {
|
|
200
|
+
const shadowRoot = card.shadowRoot;
|
|
201
|
+
if (shadowRoot) {
|
|
202
|
+
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
203
|
+
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
204
|
+
const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
|
|
205
|
+
const enableToggle = shadowRoot.querySelector('#enable-toggle');
|
|
206
|
+
const enabled = enableToggle?.getAttribute('checked') === '';
|
|
207
|
+
const id = card.getAttribute('id') || 'unknown';
|
|
208
|
+
return {
|
|
209
|
+
found: true,
|
|
210
|
+
id,
|
|
211
|
+
name,
|
|
212
|
+
version,
|
|
213
|
+
enabled,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { found: false };
|
|
219
|
+
}, extensionName);
|
|
220
|
+
if (!beforeState.found) {
|
|
221
|
+
response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (!beforeState.enabled) {
|
|
225
|
+
response.appendResponseLine(`⚠️ Warning: Extension "${beforeState.name}" is currently disabled`);
|
|
226
|
+
}
|
|
227
|
+
response.appendResponseLine(`🔄 Reloading: ${beforeState.name} v${beforeState.version}`);
|
|
228
|
+
// Perform reload
|
|
91
229
|
const reloadResult = await page.evaluate((searchName) => {
|
|
92
230
|
const extensionCards = document.querySelectorAll('extensions-item');
|
|
93
231
|
for (const card of Array.from(extensionCards)) {
|
|
@@ -98,7 +236,7 @@ export const reloadExtension = defineTool({
|
|
|
98
236
|
const reloadButton = shadowRoot.querySelector('#reload-button');
|
|
99
237
|
if (reloadButton && !reloadButton.hasAttribute('hidden')) {
|
|
100
238
|
reloadButton.click();
|
|
101
|
-
return { success: true
|
|
239
|
+
return { success: true };
|
|
102
240
|
}
|
|
103
241
|
else {
|
|
104
242
|
return {
|
|
@@ -111,15 +249,238 @@ export const reloadExtension = defineTool({
|
|
|
111
249
|
}
|
|
112
250
|
return { success: false, reason: 'Extension not found' };
|
|
113
251
|
}, extensionName);
|
|
114
|
-
if (reloadResult.success) {
|
|
115
|
-
response.appendResponseLine(
|
|
252
|
+
if (!reloadResult.success) {
|
|
253
|
+
response.appendResponseLine(`❌ Failed: ${reloadResult.reason}`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
// Wait for reload to complete
|
|
257
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
258
|
+
// Check for errors after reload
|
|
259
|
+
const afterState = await page.evaluate((searchName) => {
|
|
260
|
+
const extensionCards = document.querySelectorAll('extensions-item');
|
|
261
|
+
for (const card of Array.from(extensionCards)) {
|
|
262
|
+
const shadowRoot = card.shadowRoot;
|
|
263
|
+
if (shadowRoot) {
|
|
264
|
+
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
265
|
+
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
266
|
+
const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
|
|
267
|
+
const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
|
|
268
|
+
const hasErrors = errorsBadge
|
|
269
|
+
? parseInt(errorsBadge.textContent?.trim() || '0') > 0
|
|
270
|
+
: false;
|
|
271
|
+
return {
|
|
272
|
+
found: true,
|
|
273
|
+
version,
|
|
274
|
+
hasErrors,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return { found: false, hasErrors: false };
|
|
280
|
+
}, extensionName);
|
|
281
|
+
response.appendResponseLine('');
|
|
282
|
+
if (afterState.hasErrors) {
|
|
283
|
+
response.appendResponseLine(`⚠️ Extension reloaded but has errors (v${afterState.version})`);
|
|
284
|
+
response.appendResponseLine('💡 Use `get_extension_info` to see error details');
|
|
116
285
|
}
|
|
117
286
|
else {
|
|
118
|
-
response.appendResponseLine(
|
|
287
|
+
response.appendResponseLine(`✅ Successfully reloaded: ${beforeState.name} v${afterState.version}`);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
export const toggleExtensionState = defineTool({
|
|
293
|
+
name: 'toggle_extension_state',
|
|
294
|
+
description: `Safely enable or disable a Chrome extension. Always checks current state before toggling to prevent accidental changes.`,
|
|
295
|
+
annotations: {
|
|
296
|
+
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
297
|
+
readOnlyHint: false,
|
|
298
|
+
},
|
|
299
|
+
schema: {
|
|
300
|
+
extensionName: z
|
|
301
|
+
.string()
|
|
302
|
+
.describe('The name or partial name of the extension'),
|
|
303
|
+
state: z
|
|
304
|
+
.enum(['enable', 'disable'])
|
|
305
|
+
.describe('Desired state: "enable" or "disable"'),
|
|
306
|
+
},
|
|
307
|
+
handler: async (request, response, context) => {
|
|
308
|
+
const page = context.getSelectedPage();
|
|
309
|
+
const { extensionName, state } = request.params;
|
|
310
|
+
const desiredEnabled = state === 'enable';
|
|
311
|
+
await context.waitForEventsAfterAction(async () => {
|
|
312
|
+
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
313
|
+
const result = await page.evaluate((searchName, targetEnabled) => {
|
|
314
|
+
const extensionCards = document.querySelectorAll('extensions-item');
|
|
315
|
+
for (const card of Array.from(extensionCards)) {
|
|
316
|
+
const shadowRoot = card.shadowRoot;
|
|
317
|
+
if (shadowRoot) {
|
|
318
|
+
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
319
|
+
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
320
|
+
const enableToggle = shadowRoot.querySelector('#enable-toggle');
|
|
321
|
+
const currentEnabled = enableToggle?.getAttribute('checked') === '';
|
|
322
|
+
// Check if already in desired state
|
|
323
|
+
if (currentEnabled === targetEnabled) {
|
|
324
|
+
return {
|
|
325
|
+
success: true,
|
|
326
|
+
alreadyInState: true,
|
|
327
|
+
extensionName: name,
|
|
328
|
+
currentState: currentEnabled,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// Toggle the state
|
|
332
|
+
if (enableToggle) {
|
|
333
|
+
enableToggle.click();
|
|
334
|
+
return {
|
|
335
|
+
success: true,
|
|
336
|
+
alreadyInState: false,
|
|
337
|
+
extensionName: name,
|
|
338
|
+
previousState: currentEnabled,
|
|
339
|
+
newState: targetEnabled,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
return {
|
|
344
|
+
success: false,
|
|
345
|
+
reason: 'Enable/disable toggle not found',
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { success: false, reason: 'Extension not found' };
|
|
352
|
+
}, extensionName, desiredEnabled);
|
|
353
|
+
if (!result.success) {
|
|
354
|
+
response.appendResponseLine(`❌ Failed: ${result.reason}`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (result.alreadyInState) {
|
|
358
|
+
response.appendResponseLine(`ℹ️ Extension "${result.extensionName}" is already ${result.currentState ? 'enabled' : 'disabled'}`);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
response.appendResponseLine(`✅ ${result.extensionName}: ${result.previousState ? 'Enabled' : 'Disabled'} → ${result.newState ? 'Enabled' : 'Disabled'}`);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
export const openExtensionPopup = defineTool({
|
|
367
|
+
name: 'open_extension_popup',
|
|
368
|
+
description: `Open a Chrome extension's popup in a testable context. The popup will be opened as a page that can be interacted with using standard tools like take_snapshot, click, and evaluate_script.`,
|
|
369
|
+
annotations: {
|
|
370
|
+
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
371
|
+
readOnlyHint: false,
|
|
372
|
+
},
|
|
373
|
+
schema: {
|
|
374
|
+
extensionName: z
|
|
375
|
+
.string()
|
|
376
|
+
.describe('The name or partial name of the extension'),
|
|
377
|
+
},
|
|
378
|
+
handler: async (request, response, context) => {
|
|
379
|
+
const page = context.getSelectedPage();
|
|
380
|
+
const { extensionName } = request.params;
|
|
381
|
+
await context.waitForEventsAfterAction(async () => {
|
|
382
|
+
// First, get extension ID
|
|
383
|
+
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
384
|
+
const extensionInfo = await page.evaluate((searchName) => {
|
|
385
|
+
const extensionCards = document.querySelectorAll('extensions-item');
|
|
386
|
+
for (const card of Array.from(extensionCards)) {
|
|
387
|
+
const shadowRoot = card.shadowRoot;
|
|
388
|
+
if (shadowRoot) {
|
|
389
|
+
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
390
|
+
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
391
|
+
const id = card.getAttribute('id') || '';
|
|
392
|
+
return { found: true, id, name };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return { found: false };
|
|
397
|
+
}, extensionName);
|
|
398
|
+
if (!extensionInfo.found) {
|
|
399
|
+
response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
response.appendResponseLine(`🔍 Found extension: ${extensionInfo.name} (${extensionInfo.id})`);
|
|
403
|
+
try {
|
|
404
|
+
// Find service worker target
|
|
405
|
+
const browser = page.browser();
|
|
406
|
+
if (!browser) {
|
|
407
|
+
response.appendResponseLine('❌ Failed to get browser instance.');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const targets = await browser.targets();
|
|
411
|
+
const workerTarget = targets.find((target) => target.type() === 'service_worker' &&
|
|
412
|
+
target.url().includes(extensionInfo.id));
|
|
413
|
+
if (!workerTarget) {
|
|
414
|
+
response.appendResponseLine('❌ Service worker not found. Extension may not have a service worker (MV2 extensions are not supported).');
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const worker = await workerTarget.worker();
|
|
418
|
+
if (!worker) {
|
|
419
|
+
response.appendResponseLine('❌ Failed to get service worker context.');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
response.appendResponseLine('🔧 Opening popup via service worker...');
|
|
423
|
+
// Open popup
|
|
424
|
+
await worker.evaluate('chrome.action.openPopup();');
|
|
425
|
+
// Wait for popup target
|
|
426
|
+
const popupTarget = await browser.waitForTarget((target) => target.type() === 'page' &&
|
|
427
|
+
target.url().includes(extensionInfo.id) &&
|
|
428
|
+
target.url().includes('popup'), { timeout: 5000 });
|
|
429
|
+
if (!popupTarget) {
|
|
430
|
+
response.appendResponseLine('❌ Popup did not open within timeout.');
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const popupPage = await popupTarget.page();
|
|
434
|
+
if (!popupPage) {
|
|
435
|
+
response.appendResponseLine('❌ Failed to get popup page reference.');
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Add popup page to context and select it
|
|
439
|
+
const pages = await browser.pages();
|
|
440
|
+
const popupIndex = pages.indexOf(popupPage);
|
|
441
|
+
if (popupIndex !== -1) {
|
|
442
|
+
context.setSelectedPageIdx(popupIndex);
|
|
443
|
+
response.appendResponseLine('');
|
|
444
|
+
response.appendResponseLine(`✅ Popup opened: ${extensionInfo.name}`);
|
|
445
|
+
response.appendResponseLine(`📄 Popup URL: ${popupPage.url()}`);
|
|
446
|
+
response.appendResponseLine('');
|
|
447
|
+
response.appendResponseLine('💡 You can now use take_snapshot, click, evaluate_script, etc. on the popup');
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
response.appendResponseLine('⚠️ Popup opened but could not be selected automatically.');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
response.appendResponseLine(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
119
455
|
}
|
|
120
456
|
});
|
|
121
457
|
},
|
|
122
458
|
});
|
|
459
|
+
export const closeExtensionPopup = defineTool({
|
|
460
|
+
name: 'close_extension_popup',
|
|
461
|
+
description: `Close the currently selected extension popup page.`,
|
|
462
|
+
annotations: {
|
|
463
|
+
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
464
|
+
readOnlyHint: false,
|
|
465
|
+
},
|
|
466
|
+
schema: {},
|
|
467
|
+
handler: async (_request, response, context) => {
|
|
468
|
+
const page = context.getSelectedPage();
|
|
469
|
+
const url = page.url();
|
|
470
|
+
if (!url.startsWith('chrome-extension://')) {
|
|
471
|
+
response.appendResponseLine('❌ Current page is not an extension popup');
|
|
472
|
+
response.appendResponseLine(`Current URL: ${url}`);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
await page.close();
|
|
477
|
+
response.appendResponseLine('✅ Extension popup closed');
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
response.appendResponseLine(`❌ Failed to close popup: ${error instanceof Error ? error.message : String(error)}`);
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
});
|
|
123
484
|
export const inspectServiceWorker = defineTool({
|
|
124
485
|
name: 'inspect_service_worker',
|
|
125
486
|
description: `Open DevTools for an extension's service worker to debug background scripts.`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "MCP server for Chrome extension development with Web Store automation. Fork of chrome-devtools-mcp with extension-specific tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "./build/src/index.js",
|