chrome-devtools-mcp-for-extension 0.18.10 → 0.19.1
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/build/src/tools/gemini-web.js +43 -0
- 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({
|
|
@@ -269,6 +269,49 @@ export const askGeminiWeb = defineTool({
|
|
|
269
269
|
// Wait for response using actual Gemini UI indicators:
|
|
270
270
|
// - Generating: "回答を停止" button appears, "Gemini が入力中です" text
|
|
271
271
|
// - Complete: "Gemini が回答しました" text appears
|
|
272
|
+
// First, wait for Gemini to start generating (Stop button/icon to appear)
|
|
273
|
+
// This can take 2-5 seconds after sending
|
|
274
|
+
const maxWaitForStart = 15000; // 15 seconds max to start generating
|
|
275
|
+
const startWaitTime = Date.now();
|
|
276
|
+
let generationStarted = false;
|
|
277
|
+
while (Date.now() - startWaitTime < maxWaitForStart) {
|
|
278
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
279
|
+
const hasStarted = await page.evaluate(() => {
|
|
280
|
+
// Check for stop icon (Gemini's generating indicator)
|
|
281
|
+
const stopIcon = document.querySelector('.stop-icon mat-icon[fonticon="stop"]') ||
|
|
282
|
+
document.querySelector('mat-icon[data-mat-icon-name="stop"]') ||
|
|
283
|
+
document.querySelector('.blue-circle.stop-icon');
|
|
284
|
+
// Check for stop button
|
|
285
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
286
|
+
const stopButton = buttons.find(b => {
|
|
287
|
+
const text = b.textContent || '';
|
|
288
|
+
const ariaLabel = b.getAttribute('aria-label') || '';
|
|
289
|
+
return text.includes('回答を停止') || text.includes('Stop') ||
|
|
290
|
+
ariaLabel.includes('Stop') || ariaLabel.includes('停止');
|
|
291
|
+
});
|
|
292
|
+
// Check for typing/thinking indicators
|
|
293
|
+
const bodyText = document.body.innerText;
|
|
294
|
+
const isTyping = bodyText.includes('Gemini が入力中です') ||
|
|
295
|
+
bodyText.includes('Gemini is typing') ||
|
|
296
|
+
bodyText.includes('Analyzing') ||
|
|
297
|
+
bodyText.includes('分析中') ||
|
|
298
|
+
bodyText.includes('Thinking') ||
|
|
299
|
+
bodyText.includes('思考中');
|
|
300
|
+
// Check for loading spinners
|
|
301
|
+
const hasSpinner = document.querySelector('[role="progressbar"]') !== null ||
|
|
302
|
+
document.querySelector('[aria-busy="true"]') !== null;
|
|
303
|
+
// Check for model-response appearing (even without stop button)
|
|
304
|
+
const hasNewResponse = document.querySelectorAll('model-response').length > 0;
|
|
305
|
+
return !!stopIcon || !!stopButton || isTyping || hasSpinner || hasNewResponse;
|
|
306
|
+
});
|
|
307
|
+
if (hasStarted) {
|
|
308
|
+
generationStarted = true;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!generationStarted) {
|
|
313
|
+
response.appendResponseLine('⚠️ 生成開始を検出できませんでした(続行します)');
|
|
314
|
+
}
|
|
272
315
|
const startTime = Date.now();
|
|
273
316
|
let stableCount = 0;
|
|
274
317
|
let lastResponseText = '';
|
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.1",
|
|
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",
|