chrome-devtools-mcp-for-extension 0.18.10 → 0.19.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 +8 -4
- package/build/src/tools/extensions.js +41 -598
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -299,13 +299,17 @@ git push && git push --tags
|
|
|
299
299
|
|
|
300
300
|
| Tool | Purpose | Example |
|
|
301
301
|
|------|---------|---------|
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
304
|
-
| `
|
|
302
|
+
| `open_extension_popup` | Select popup window | "Open my extension popup" |
|
|
303
|
+
| `reload_iframe_extension` | Hot-reload via CDP | "Reload extension" |
|
|
304
|
+
| `patch_iframe_popup` | Edit & reload | "Patch popup.html" |
|
|
305
305
|
| `ask_chatgpt_web` | ChatGPT research | "Ask ChatGPT about..." |
|
|
306
306
|
| `take_snapshot` | Page analysis | "Snapshot current page" |
|
|
307
307
|
| `list_pages` | Browser tabs | "List open pages" |
|
|
308
308
|
|
|
309
|
+
**Note:** Extension tools use CDP (Chrome DevTools Protocol) for reliable operation.
|
|
310
|
+
Shadow DOM-based tools (`list_extensions`, `reload_extension`, etc.) were removed in v0.19.0
|
|
311
|
+
due to Chrome security restrictions.
|
|
312
|
+
|
|
309
313
|
**See also:** [Full Tool Documentation](docs/tools-reference.md)
|
|
310
314
|
|
|
311
315
|
---
|
|
@@ -315,7 +319,7 @@ git push && git push --tags
|
|
|
315
319
|
### Extension Not Loading
|
|
316
320
|
|
|
317
321
|
```
|
|
318
|
-
"
|
|
322
|
+
"Check extension popup for errors"
|
|
319
323
|
```
|
|
320
324
|
|
|
321
325
|
**Common fixes:**
|
|
@@ -8,444 +8,21 @@ import z from 'zod';
|
|
|
8
8
|
import { ToolCategories } from './categories.js';
|
|
9
9
|
import { defineTool } from './ToolDefinition.js';
|
|
10
10
|
// ========================================
|
|
11
|
-
//
|
|
11
|
+
// Chrome Extension Development Tools
|
|
12
|
+
// ========================================
|
|
13
|
+
//
|
|
14
|
+
// These tools use CDP (Chrome DevTools Protocol) and direct URL access
|
|
15
|
+
// to interact with extensions. Tools that relied on chrome://extensions
|
|
16
|
+
// Shadow DOM scraping have been removed as they are unreliable due to
|
|
17
|
+
// Chrome's security restrictions.
|
|
18
|
+
//
|
|
19
|
+
// Working tools:
|
|
20
|
+
// - openExtensionPopup: Uses page.goto('chrome-extension://ID/popup.html')
|
|
21
|
+
// - closeExtensionPopup: URL validation only
|
|
22
|
+
// - inspectIframePopup: CDP frame attachment
|
|
23
|
+
// - patchIframePopup: File I/O + CDP reload
|
|
24
|
+
// - reloadIframeExtension: CDP + chrome.runtime.reload()
|
|
12
25
|
// ========================================
|
|
13
|
-
export const listExtensions = defineTool({
|
|
14
|
-
name: 'list_extensions',
|
|
15
|
-
description: `List all installed Chrome extensions with their status, version, and ability to reload or debug them.`,
|
|
16
|
-
annotations: {
|
|
17
|
-
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
18
|
-
readOnlyHint: true,
|
|
19
|
-
},
|
|
20
|
-
schema: {},
|
|
21
|
-
handler: async (_request, response, context) => {
|
|
22
|
-
const page = context.getSelectedPage();
|
|
23
|
-
await context.waitForEventsAfterAction(async () => {
|
|
24
|
-
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
25
|
-
const extensions = await page.evaluate(() => {
|
|
26
|
-
const manager = document.querySelector('extensions-manager');
|
|
27
|
-
if (!manager?.shadowRoot)
|
|
28
|
-
return null;
|
|
29
|
-
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
|
|
30
|
-
if (!itemList?.shadowRoot)
|
|
31
|
-
return null;
|
|
32
|
-
const extensionCards = itemList.shadowRoot.querySelectorAll('extensions-item');
|
|
33
|
-
const results = [];
|
|
34
|
-
Array.from(extensionCards).forEach(card => {
|
|
35
|
-
const shadowRoot = card.shadowRoot;
|
|
36
|
-
if (shadowRoot) {
|
|
37
|
-
const name = shadowRoot.querySelector('#name')?.textContent?.trim() ||
|
|
38
|
-
'Unknown';
|
|
39
|
-
const description = shadowRoot.querySelector('#description')?.textContent?.trim() ||
|
|
40
|
-
'';
|
|
41
|
-
const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
|
|
42
|
-
let enableToggle = shadowRoot.querySelector('#enable-toggle');
|
|
43
|
-
if (!enableToggle) {
|
|
44
|
-
enableToggle = shadowRoot.querySelector('cr-toggle');
|
|
45
|
-
}
|
|
46
|
-
const enabled = enableToggle?.getAttribute('checked') === '';
|
|
47
|
-
const id = card.getAttribute('id') || 'unknown';
|
|
48
|
-
const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
|
|
49
|
-
const hasErrors = errorsBadge ? parseInt(errorsBadge.textContent?.trim() || '0') > 0 : false;
|
|
50
|
-
results.push({
|
|
51
|
-
id,
|
|
52
|
-
name,
|
|
53
|
-
enabled,
|
|
54
|
-
version,
|
|
55
|
-
description,
|
|
56
|
-
hasErrors,
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
return results;
|
|
61
|
-
});
|
|
62
|
-
if (!extensions) {
|
|
63
|
-
response.appendResponseLine('❌ Failed to query extensions page');
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
response.appendResponseLine('Installed Chrome Extensions:');
|
|
67
|
-
response.appendResponseLine('');
|
|
68
|
-
if (extensions.length === 0) {
|
|
69
|
-
response.appendResponseLine('No extensions found.');
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
extensions.forEach((ext, index) => {
|
|
73
|
-
response.appendResponseLine(`${index + 1}. **${ext.name}** v${ext.version}`);
|
|
74
|
-
response.appendResponseLine(` ID: ${ext.id}`);
|
|
75
|
-
response.appendResponseLine(` Status: ${ext.enabled ? '✅ Enabled' : '❌ Disabled'}${ext.hasErrors ? ' ⚠️ Has errors' : ''}`);
|
|
76
|
-
if (ext.description) {
|
|
77
|
-
response.appendResponseLine(` ${ext.description}`);
|
|
78
|
-
}
|
|
79
|
-
response.appendResponseLine('');
|
|
80
|
-
});
|
|
81
|
-
response.appendResponseLine('💡 Use `reload_extension` to reload or `inspect_service_worker` to debug');
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
export const getExtensionInfo = defineTool({
|
|
87
|
-
name: 'get_extension_info',
|
|
88
|
-
description: `Get detailed information about a specific Chrome extension including its current state, version, and any errors.`,
|
|
89
|
-
annotations: {
|
|
90
|
-
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
91
|
-
readOnlyHint: true,
|
|
92
|
-
},
|
|
93
|
-
schema: {
|
|
94
|
-
extensionName: z
|
|
95
|
-
.string()
|
|
96
|
-
.describe('The name or partial name of the extension to get info about'),
|
|
97
|
-
},
|
|
98
|
-
handler: async (request, response, context) => {
|
|
99
|
-
const page = context.getSelectedPage();
|
|
100
|
-
const { extensionName } = request.params;
|
|
101
|
-
await context.waitForEventsAfterAction(async () => {
|
|
102
|
-
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
103
|
-
const extensionInfo = await page.evaluate((searchName) => {
|
|
104
|
-
const manager = document.querySelector('extensions-manager');
|
|
105
|
-
if (!manager?.shadowRoot)
|
|
106
|
-
return null;
|
|
107
|
-
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
|
|
108
|
-
if (!itemList?.shadowRoot)
|
|
109
|
-
return null;
|
|
110
|
-
const extensionCards = itemList.shadowRoot.querySelectorAll('extensions-item');
|
|
111
|
-
for (const card of Array.from(extensionCards)) {
|
|
112
|
-
const shadowRoot = card.shadowRoot;
|
|
113
|
-
if (shadowRoot) {
|
|
114
|
-
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
115
|
-
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
116
|
-
const description = shadowRoot.querySelector('#description')?.textContent?.trim() ||
|
|
117
|
-
'';
|
|
118
|
-
const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
|
|
119
|
-
let enableToggle = shadowRoot.querySelector('#enable-toggle');
|
|
120
|
-
if (!enableToggle) {
|
|
121
|
-
enableToggle = shadowRoot.querySelector('cr-toggle');
|
|
122
|
-
}
|
|
123
|
-
const enabled = enableToggle?.getAttribute('checked') === '';
|
|
124
|
-
const id = card.getAttribute('id') || 'unknown';
|
|
125
|
-
const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
|
|
126
|
-
const hasErrors = errorsBadge
|
|
127
|
-
? parseInt(errorsBadge.textContent?.trim() || '0') > 0
|
|
128
|
-
: false;
|
|
129
|
-
// Get error details if available
|
|
130
|
-
const errors = [];
|
|
131
|
-
if (hasErrors) {
|
|
132
|
-
const errorsButton = shadowRoot.querySelector('#errors-button');
|
|
133
|
-
if (errorsButton) {
|
|
134
|
-
// Try to get error text from the errors section
|
|
135
|
-
const errorsList = shadowRoot.querySelectorAll('.error-list .error-message');
|
|
136
|
-
errorsList.forEach(err => {
|
|
137
|
-
const errorText = err.textContent?.trim();
|
|
138
|
-
if (errorText) {
|
|
139
|
-
errors.push(errorText);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// Check if this is a development extension
|
|
145
|
-
const detailsView = shadowRoot.querySelector('extensions-detail-view');
|
|
146
|
-
const isDevelopment = detailsView ?
|
|
147
|
-
detailsView.shadowRoot?.querySelector('#load-path')?.textContent?.trim() : undefined;
|
|
148
|
-
return {
|
|
149
|
-
found: true,
|
|
150
|
-
id,
|
|
151
|
-
name,
|
|
152
|
-
version,
|
|
153
|
-
description,
|
|
154
|
-
enabled,
|
|
155
|
-
hasErrors,
|
|
156
|
-
errors,
|
|
157
|
-
path: isDevelopment || 'Not a development extension',
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return { found: false };
|
|
163
|
-
}, extensionName);
|
|
164
|
-
if (!extensionInfo) {
|
|
165
|
-
response.appendResponseLine('❌ Failed to query extensions page');
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (extensionInfo.found) {
|
|
169
|
-
response.appendResponseLine(`## Extension: ${extensionInfo.name}`);
|
|
170
|
-
response.appendResponseLine('');
|
|
171
|
-
response.appendResponseLine(`**ID:** ${extensionInfo.id}`);
|
|
172
|
-
response.appendResponseLine(`**Version:** ${extensionInfo.version}`);
|
|
173
|
-
response.appendResponseLine(`**Status:** ${extensionInfo.enabled ? '✅ Enabled' : '❌ Disabled'}`);
|
|
174
|
-
if (extensionInfo.description) {
|
|
175
|
-
response.appendResponseLine(`**Description:** ${extensionInfo.description}`);
|
|
176
|
-
}
|
|
177
|
-
if (extensionInfo.path !== 'Not a development extension') {
|
|
178
|
-
response.appendResponseLine(`**Path:** ${extensionInfo.path}`);
|
|
179
|
-
}
|
|
180
|
-
response.appendResponseLine('');
|
|
181
|
-
if (extensionInfo.hasErrors) {
|
|
182
|
-
response.appendResponseLine('⚠️ **Errors:**');
|
|
183
|
-
if (extensionInfo.errors.length > 0) {
|
|
184
|
-
extensionInfo.errors.forEach(err => {
|
|
185
|
-
response.appendResponseLine(` - ${err}`);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
response.appendResponseLine(' Extension has errors (details not available)');
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
response.appendResponseLine('✅ No errors detected');
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
|
|
198
|
-
response.appendResponseLine('');
|
|
199
|
-
response.appendResponseLine('💡 Use `list_extensions` to see all installed extensions');
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
export const reloadExtension = defineTool({
|
|
205
|
-
name: 'reload_extension',
|
|
206
|
-
description: `Reload a Chrome extension to apply changes during development. Checks extension state before and after reload.`,
|
|
207
|
-
annotations: {
|
|
208
|
-
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
209
|
-
readOnlyHint: false,
|
|
210
|
-
},
|
|
211
|
-
schema: {
|
|
212
|
-
extensionName: z
|
|
213
|
-
.string()
|
|
214
|
-
.describe('The name or partial name of the extension to reload'),
|
|
215
|
-
},
|
|
216
|
-
handler: async (request, response, context) => {
|
|
217
|
-
const page = context.getSelectedPage();
|
|
218
|
-
const { extensionName } = request.params;
|
|
219
|
-
await context.waitForEventsAfterAction(async () => {
|
|
220
|
-
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
221
|
-
// Get extension info before reload
|
|
222
|
-
const beforeState = await page.evaluate((searchName) => {
|
|
223
|
-
const manager = document.querySelector('extensions-manager');
|
|
224
|
-
if (!manager?.shadowRoot)
|
|
225
|
-
return null;
|
|
226
|
-
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
|
|
227
|
-
if (!itemList?.shadowRoot)
|
|
228
|
-
return null;
|
|
229
|
-
const extensionCards = itemList.shadowRoot.querySelectorAll('extensions-item');
|
|
230
|
-
for (const card of Array.from(extensionCards)) {
|
|
231
|
-
const shadowRoot = card.shadowRoot;
|
|
232
|
-
if (shadowRoot) {
|
|
233
|
-
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
234
|
-
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
235
|
-
const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
|
|
236
|
-
let enableToggle = shadowRoot.querySelector('#enable-toggle');
|
|
237
|
-
if (!enableToggle) {
|
|
238
|
-
enableToggle = shadowRoot.querySelector('cr-toggle');
|
|
239
|
-
}
|
|
240
|
-
const enabled = enableToggle?.getAttribute('checked') === '';
|
|
241
|
-
const id = card.getAttribute('id') || 'unknown';
|
|
242
|
-
return {
|
|
243
|
-
found: true,
|
|
244
|
-
id,
|
|
245
|
-
name,
|
|
246
|
-
version,
|
|
247
|
-
enabled,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
return { found: false };
|
|
253
|
-
}, extensionName);
|
|
254
|
-
if (!beforeState) {
|
|
255
|
-
response.appendResponseLine('❌ Failed to query extensions page');
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
if (!beforeState.found) {
|
|
259
|
-
response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (!beforeState.enabled) {
|
|
263
|
-
response.appendResponseLine(`⚠️ Warning: Extension "${beforeState.name}" is currently disabled`);
|
|
264
|
-
}
|
|
265
|
-
response.appendResponseLine(`🔄 Reloading: ${beforeState.name} v${beforeState.version}`);
|
|
266
|
-
// Perform reload
|
|
267
|
-
const reloadResult = await page.evaluate((searchName) => {
|
|
268
|
-
const manager = document.querySelector('extensions-manager');
|
|
269
|
-
if (!manager?.shadowRoot)
|
|
270
|
-
return null;
|
|
271
|
-
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
|
|
272
|
-
if (!itemList?.shadowRoot)
|
|
273
|
-
return null;
|
|
274
|
-
const extensionCards = itemList.shadowRoot.querySelectorAll('extensions-item');
|
|
275
|
-
for (const card of Array.from(extensionCards)) {
|
|
276
|
-
const shadowRoot = card.shadowRoot;
|
|
277
|
-
if (shadowRoot) {
|
|
278
|
-
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
279
|
-
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
280
|
-
// Try multiple selectors for reload button (dev-reload-button in developer mode)
|
|
281
|
-
let reloadButton = shadowRoot.querySelector('#dev-reload-button');
|
|
282
|
-
if (!reloadButton) {
|
|
283
|
-
reloadButton = shadowRoot.querySelector('#reload-button');
|
|
284
|
-
}
|
|
285
|
-
if (!reloadButton) {
|
|
286
|
-
// Try finding by aria-label (supports both English and Japanese)
|
|
287
|
-
reloadButton = shadowRoot.querySelector('[aria-label*="再読み込み"]');
|
|
288
|
-
}
|
|
289
|
-
if (!reloadButton) {
|
|
290
|
-
reloadButton = shadowRoot.querySelector('[aria-label*="Reload"]');
|
|
291
|
-
}
|
|
292
|
-
if (reloadButton && !reloadButton.hasAttribute('hidden')) {
|
|
293
|
-
reloadButton.click();
|
|
294
|
-
return { success: true };
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
return {
|
|
298
|
-
success: false,
|
|
299
|
-
reason: 'Reload button not available (extension not in developer mode or button hidden)',
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return { success: false, reason: 'Extension not found' };
|
|
306
|
-
}, extensionName);
|
|
307
|
-
if (!reloadResult) {
|
|
308
|
-
response.appendResponseLine('❌ Failed to execute reload');
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
if (!reloadResult.success) {
|
|
312
|
-
response.appendResponseLine(`❌ Failed: ${reloadResult.reason}`);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
// Wait for reload to complete
|
|
316
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
317
|
-
// Check for errors after reload
|
|
318
|
-
const afterState = await page.evaluate((searchName) => {
|
|
319
|
-
const manager = document.querySelector('extensions-manager');
|
|
320
|
-
if (!manager?.shadowRoot)
|
|
321
|
-
return null;
|
|
322
|
-
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
|
|
323
|
-
if (!itemList?.shadowRoot)
|
|
324
|
-
return null;
|
|
325
|
-
const extensionCards = itemList.shadowRoot.querySelectorAll('extensions-item');
|
|
326
|
-
for (const card of Array.from(extensionCards)) {
|
|
327
|
-
const shadowRoot = card.shadowRoot;
|
|
328
|
-
if (shadowRoot) {
|
|
329
|
-
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
330
|
-
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
331
|
-
const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
|
|
332
|
-
const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
|
|
333
|
-
const hasErrors = errorsBadge
|
|
334
|
-
? parseInt(errorsBadge.textContent?.trim() || '0') > 0
|
|
335
|
-
: false;
|
|
336
|
-
return {
|
|
337
|
-
found: true,
|
|
338
|
-
version,
|
|
339
|
-
hasErrors,
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return { found: false, hasErrors: false };
|
|
345
|
-
}, extensionName);
|
|
346
|
-
if (!afterState) {
|
|
347
|
-
response.appendResponseLine('⚠️ Warning: Could not verify reload status');
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
response.appendResponseLine('');
|
|
351
|
-
if (afterState.hasErrors) {
|
|
352
|
-
response.appendResponseLine(`⚠️ Extension reloaded but has errors (v${afterState.version})`);
|
|
353
|
-
response.appendResponseLine('💡 Use `get_extension_info` to see error details');
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
response.appendResponseLine(`✅ Successfully reloaded: ${beforeState.name} v${afterState.version}`);
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
},
|
|
360
|
-
});
|
|
361
|
-
export const toggleExtensionState = defineTool({
|
|
362
|
-
name: 'toggle_extension_state',
|
|
363
|
-
description: `Safely enable or disable a Chrome extension. Always checks current state before toggling to prevent accidental changes.`,
|
|
364
|
-
annotations: {
|
|
365
|
-
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
366
|
-
readOnlyHint: false,
|
|
367
|
-
},
|
|
368
|
-
schema: {
|
|
369
|
-
extensionName: z
|
|
370
|
-
.string()
|
|
371
|
-
.describe('The name or partial name of the extension'),
|
|
372
|
-
state: z
|
|
373
|
-
.enum(['enable', 'disable'])
|
|
374
|
-
.describe('Desired state: "enable" or "disable"'),
|
|
375
|
-
},
|
|
376
|
-
handler: async (request, response, context) => {
|
|
377
|
-
const page = context.getSelectedPage();
|
|
378
|
-
const { extensionName, state } = request.params;
|
|
379
|
-
const desiredEnabled = state === 'enable';
|
|
380
|
-
await context.waitForEventsAfterAction(async () => {
|
|
381
|
-
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
382
|
-
const result = await page.evaluate((searchName, targetEnabled) => {
|
|
383
|
-
const manager = document.querySelector('extensions-manager');
|
|
384
|
-
if (!manager?.shadowRoot)
|
|
385
|
-
return null;
|
|
386
|
-
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
|
|
387
|
-
if (!itemList?.shadowRoot)
|
|
388
|
-
return null;
|
|
389
|
-
const extensionCards = itemList.shadowRoot.querySelectorAll('extensions-item');
|
|
390
|
-
for (const card of Array.from(extensionCards)) {
|
|
391
|
-
const shadowRoot = card.shadowRoot;
|
|
392
|
-
if (shadowRoot) {
|
|
393
|
-
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
394
|
-
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
395
|
-
// Try both selectors: #enable-toggle (older Chrome) and cr-toggle (newer Chrome)
|
|
396
|
-
let enableToggle = shadowRoot.querySelector('#enable-toggle');
|
|
397
|
-
if (!enableToggle) {
|
|
398
|
-
enableToggle = shadowRoot.querySelector('cr-toggle');
|
|
399
|
-
}
|
|
400
|
-
const currentEnabled = enableToggle?.getAttribute('checked') === '';
|
|
401
|
-
// Check if already in desired state
|
|
402
|
-
if (currentEnabled === targetEnabled) {
|
|
403
|
-
return {
|
|
404
|
-
success: true,
|
|
405
|
-
alreadyInState: true,
|
|
406
|
-
extensionName: name,
|
|
407
|
-
currentState: currentEnabled,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
// Toggle the state
|
|
411
|
-
if (enableToggle) {
|
|
412
|
-
enableToggle.click();
|
|
413
|
-
return {
|
|
414
|
-
success: true,
|
|
415
|
-
alreadyInState: false,
|
|
416
|
-
extensionName: name,
|
|
417
|
-
previousState: currentEnabled,
|
|
418
|
-
newState: targetEnabled,
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
return {
|
|
423
|
-
success: false,
|
|
424
|
-
reason: 'Enable/disable toggle not found (tried #enable-toggle and cr-toggle)',
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return { success: false, reason: 'Extension not found' };
|
|
431
|
-
}, extensionName, desiredEnabled);
|
|
432
|
-
if (!result) {
|
|
433
|
-
response.appendResponseLine('❌ Failed to query extensions page');
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (!result.success) {
|
|
437
|
-
response.appendResponseLine(`❌ Failed: ${result.reason}`);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
if (result.alreadyInState) {
|
|
441
|
-
response.appendResponseLine(`ℹ️ Extension "${result.extensionName}" is already ${result.currentState ? 'enabled' : 'disabled'}`);
|
|
442
|
-
}
|
|
443
|
-
else {
|
|
444
|
-
response.appendResponseLine(`✅ ${result.extensionName}: ${result.previousState ? 'Enabled' : 'Disabled'} → ${result.newState ? 'Enabled' : 'Disabled'}`);
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
},
|
|
448
|
-
});
|
|
449
26
|
export const openExtensionPopup = defineTool({
|
|
450
27
|
name: 'open_extension_popup',
|
|
451
28
|
description: `Select an already-opened Chrome extension popup window for testing. If no extension name is provided, it will automatically detect and select the currently active popup window. If an extension name is provided, it will search for that specific extension's popup. After selection, you can use take_snapshot, click, evaluate_script, etc. on the popup.`,
|
|
@@ -518,100 +95,38 @@ export const openExtensionPopup = defineTool({
|
|
|
518
95
|
response.appendResponseLine('💡 Please manually click the extension icon to open the popup first.');
|
|
519
96
|
return;
|
|
520
97
|
}
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
536
|
-
const id = card.getAttribute('id') || '';
|
|
537
|
-
return { found: true, id, name };
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
return { found: false };
|
|
542
|
-
}, extensionName);
|
|
543
|
-
if (!extensionInfo) {
|
|
544
|
-
response.appendResponseLine('❌ Failed to query extensions page');
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (!extensionInfo.found) {
|
|
548
|
-
response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
response.appendResponseLine(`🔍 Found extension: ${extensionInfo.name} (${extensionInfo.id})`);
|
|
552
|
-
try {
|
|
553
|
-
const browser = page.browser();
|
|
554
|
-
if (!browser) {
|
|
555
|
-
response.appendResponseLine('❌ Failed to get browser instance.');
|
|
98
|
+
// If extensionName is provided, search for popup containing that name in URL
|
|
99
|
+
// This uses URL-based detection, not chrome://extensions Shadow DOM
|
|
100
|
+
response.appendResponseLine(`🔍 Searching for popup matching: "${extensionName}"`);
|
|
101
|
+
const pages = await browser.pages();
|
|
102
|
+
for (let i = 0; i < pages.length; i++) {
|
|
103
|
+
const p = pages[i];
|
|
104
|
+
const url = p.url();
|
|
105
|
+
if (url.startsWith('chrome-extension://') &&
|
|
106
|
+
url.toLowerCase().includes(extensionName.toLowerCase())) {
|
|
107
|
+
context.setSelectedPageIdx(i);
|
|
108
|
+
response.appendResponseLine('✅ Found and selected matching popup window');
|
|
109
|
+
response.appendResponseLine(`📄 Popup URL: ${url}`);
|
|
110
|
+
response.appendResponseLine('');
|
|
111
|
+
response.appendResponseLine('💡 You can now use take_snapshot, click, evaluate_script, etc. on the popup');
|
|
556
112
|
return;
|
|
557
113
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
popupIndex = i;
|
|
570
|
-
break;
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
if (!popupPage) {
|
|
574
|
-
// Check for iframe-embedded popup with this extension ID
|
|
575
|
-
response.appendResponseLine('🔧 Checking for iframe-embedded popup...');
|
|
576
|
-
// Go back to the original page to check for iframes
|
|
577
|
-
await page.goBack();
|
|
578
|
-
const iframePopups = await page.evaluate((extId) => {
|
|
579
|
-
return Array.from(document.querySelectorAll('iframe'))
|
|
580
|
-
.filter((iframe) => iframe.src.includes(extId))
|
|
581
|
-
.map((iframe) => ({
|
|
582
|
-
src: iframe.src,
|
|
583
|
-
id: iframe.id,
|
|
584
|
-
className: iframe.className,
|
|
585
|
-
}));
|
|
586
|
-
}, extensionInfo.id);
|
|
587
|
-
if (iframePopups.length > 0) {
|
|
588
|
-
response.appendResponseLine('');
|
|
589
|
-
response.appendResponseLine(`✅ Extension popup found (embedded as iframe): ${extensionInfo.name}`);
|
|
590
|
-
response.appendResponseLine(`📄 Popup URL: ${iframePopups[0].src}`);
|
|
591
|
-
response.appendResponseLine('');
|
|
592
|
-
response.appendResponseLine('💡 This popup is embedded in the current page as an iframe.');
|
|
593
|
-
response.appendResponseLine(' You can interact with it using regular page tools:');
|
|
594
|
-
response.appendResponseLine(' - take_snapshot (includes iframe content)');
|
|
595
|
-
response.appendResponseLine(' - click on elements');
|
|
596
|
-
response.appendResponseLine(' - fill forms');
|
|
597
|
-
response.appendResponseLine(' - evaluate_script');
|
|
598
|
-
return;
|
|
599
|
-
}
|
|
600
|
-
response.appendResponseLine('❌ Popup window not found.');
|
|
601
|
-
response.appendResponseLine('💡 Please manually click the extension icon to open the popup first.');
|
|
114
|
+
}
|
|
115
|
+
// Check for any extension popup if exact match not found
|
|
116
|
+
for (let i = 0; i < pages.length; i++) {
|
|
117
|
+
const p = pages[i];
|
|
118
|
+
const url = p.url();
|
|
119
|
+
if (url.startsWith('chrome-extension://')) {
|
|
120
|
+
context.setSelectedPageIdx(i);
|
|
121
|
+
response.appendResponseLine(`⚠️ No popup matching "${extensionName}" found, but found another extension popup`);
|
|
122
|
+
response.appendResponseLine(`📄 Popup URL: ${url}`);
|
|
123
|
+
response.appendResponseLine('');
|
|
124
|
+
response.appendResponseLine('💡 You can now use take_snapshot, click, evaluate_script, etc. on the popup');
|
|
602
125
|
return;
|
|
603
126
|
}
|
|
604
|
-
// Select the popup page
|
|
605
|
-
context.setSelectedPageIdx(popupIndex);
|
|
606
|
-
response.appendResponseLine('');
|
|
607
|
-
response.appendResponseLine(`✅ Popup window selected: ${extensionInfo.name}`);
|
|
608
|
-
response.appendResponseLine(`📄 Popup URL: ${popupPage.url()}`);
|
|
609
|
-
response.appendResponseLine('');
|
|
610
|
-
response.appendResponseLine('💡 You can now use take_snapshot, click, evaluate_script, etc. on the popup');
|
|
611
|
-
}
|
|
612
|
-
catch (error) {
|
|
613
|
-
response.appendResponseLine(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
614
127
|
}
|
|
128
|
+
response.appendResponseLine(`❌ No extension popup found matching: "${extensionName}"`);
|
|
129
|
+
response.appendResponseLine('💡 Please manually click the extension icon to open the popup first.');
|
|
615
130
|
});
|
|
616
131
|
},
|
|
617
132
|
});
|
|
@@ -640,78 +155,6 @@ export const closeExtensionPopup = defineTool({
|
|
|
640
155
|
}
|
|
641
156
|
},
|
|
642
157
|
});
|
|
643
|
-
export const inspectServiceWorker = defineTool({
|
|
644
|
-
name: 'inspect_service_worker',
|
|
645
|
-
description: `Open DevTools for an extension's service worker to debug background scripts.`,
|
|
646
|
-
annotations: {
|
|
647
|
-
category: ToolCategories.EXTENSION_DEVELOPMENT,
|
|
648
|
-
readOnlyHint: false,
|
|
649
|
-
},
|
|
650
|
-
schema: {
|
|
651
|
-
extensionName: z
|
|
652
|
-
.string()
|
|
653
|
-
.describe('The name or partial name of the extension to debug'),
|
|
654
|
-
},
|
|
655
|
-
handler: async (request, response, context) => {
|
|
656
|
-
const page = context.getSelectedPage();
|
|
657
|
-
const { extensionName } = request.params;
|
|
658
|
-
await context.waitForEventsAfterAction(async () => {
|
|
659
|
-
await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
|
|
660
|
-
const inspectResult = await page.evaluate((searchName) => {
|
|
661
|
-
const manager = document.querySelector('extensions-manager');
|
|
662
|
-
if (!manager?.shadowRoot)
|
|
663
|
-
return null;
|
|
664
|
-
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
|
|
665
|
-
if (!itemList?.shadowRoot)
|
|
666
|
-
return null;
|
|
667
|
-
const extensionCards = itemList.shadowRoot.querySelectorAll('extensions-item');
|
|
668
|
-
for (const card of Array.from(extensionCards)) {
|
|
669
|
-
const shadowRoot = card.shadowRoot;
|
|
670
|
-
if (shadowRoot) {
|
|
671
|
-
const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
|
|
672
|
-
if (name.toLowerCase().includes(searchName.toLowerCase())) {
|
|
673
|
-
// Look for service worker link
|
|
674
|
-
const serviceWorkerLink = shadowRoot.querySelector('a[href*="service_worker"]');
|
|
675
|
-
if (serviceWorkerLink) {
|
|
676
|
-
serviceWorkerLink.click();
|
|
677
|
-
return {
|
|
678
|
-
success: true,
|
|
679
|
-
extensionName: name,
|
|
680
|
-
type: 'service_worker',
|
|
681
|
-
};
|
|
682
|
-
}
|
|
683
|
-
// Look for background page link
|
|
684
|
-
const backgroundLink = shadowRoot.querySelector('a[href*="background"]');
|
|
685
|
-
if (backgroundLink) {
|
|
686
|
-
backgroundLink.click();
|
|
687
|
-
return {
|
|
688
|
-
success: true,
|
|
689
|
-
extensionName: name,
|
|
690
|
-
type: 'background_page',
|
|
691
|
-
};
|
|
692
|
-
}
|
|
693
|
-
return {
|
|
694
|
-
success: false,
|
|
695
|
-
reason: 'No service worker or background page found',
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return { success: false, reason: 'Extension not found' };
|
|
701
|
-
}, extensionName);
|
|
702
|
-
if (!inspectResult) {
|
|
703
|
-
response.appendResponseLine('❌ Failed to find extension');
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (inspectResult.success) {
|
|
707
|
-
response.appendResponseLine(`✅ Opened DevTools for ${inspectResult.type} of: ${inspectResult.extensionName}`);
|
|
708
|
-
}
|
|
709
|
-
else {
|
|
710
|
-
response.appendResponseLine(`❌ Failed: ${inspectResult.reason}`);
|
|
711
|
-
}
|
|
712
|
-
});
|
|
713
|
-
},
|
|
714
|
-
});
|
|
715
158
|
// Import iframe popup tools
|
|
716
159
|
import * as iframePopupTools from './iframe-popup-tools.js';
|
|
717
160
|
export const inspectIframePopup = defineTool({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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": "./scripts/cli.mjs",
|