chrome-devtools-mcp-for-extension 0.7.2 → 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/build/src/tools/extensions.js +366 -5
- package/package.json +1 -1
|
@@ -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",
|