chrome-devtools-mcp-for-extension 0.7.2 → 0.8.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.
@@ -33,9 +33,8 @@ export const listExtensions = defineTool({
33
33
  const description = shadowRoot.querySelector('#description')?.textContent?.trim() ||
34
34
  '';
35
35
  const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
36
- const enabled = !shadowRoot
37
- .querySelector('#enable-toggle')
38
- ?.hasAttribute('disabled');
36
+ const enableToggle = shadowRoot.querySelector('#enable-toggle');
37
+ const enabled = enableToggle?.getAttribute('checked') === '';
39
38
  const id = card.getAttribute('id') || 'unknown';
40
39
  const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
41
40
  const hasErrors = errorsBadge ? parseInt(errorsBadge.textContent?.trim() || '0') > 0 : false;
@@ -71,9 +70,114 @@ export const listExtensions = defineTool({
71
70
  });
72
71
  },
73
72
  });
73
+ export const getExtensionInfo = defineTool({
74
+ name: 'get_extension_info',
75
+ description: `Get detailed information about a specific Chrome extension including its current state, version, and any errors.`,
76
+ annotations: {
77
+ category: ToolCategories.EXTENSION_DEVELOPMENT,
78
+ readOnlyHint: true,
79
+ },
80
+ schema: {
81
+ extensionName: z
82
+ .string()
83
+ .describe('The name or partial name of the extension to get info about'),
84
+ },
85
+ handler: async (request, response, context) => {
86
+ const page = context.getSelectedPage();
87
+ const { extensionName } = request.params;
88
+ await context.waitForEventsAfterAction(async () => {
89
+ await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
90
+ const extensionInfo = await page.evaluate((searchName) => {
91
+ const extensionCards = document.querySelectorAll('extensions-item');
92
+ for (const card of Array.from(extensionCards)) {
93
+ const shadowRoot = card.shadowRoot;
94
+ if (shadowRoot) {
95
+ const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
96
+ if (name.toLowerCase().includes(searchName.toLowerCase())) {
97
+ const description = shadowRoot.querySelector('#description')?.textContent?.trim() ||
98
+ '';
99
+ const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
100
+ const enableToggle = shadowRoot.querySelector('#enable-toggle');
101
+ const enabled = enableToggle?.getAttribute('checked') === '';
102
+ const id = card.getAttribute('id') || 'unknown';
103
+ const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
104
+ const hasErrors = errorsBadge
105
+ ? parseInt(errorsBadge.textContent?.trim() || '0') > 0
106
+ : false;
107
+ // Get error details if available
108
+ const errors = [];
109
+ if (hasErrors) {
110
+ const errorsButton = shadowRoot.querySelector('#errors-button');
111
+ if (errorsButton) {
112
+ // Try to get error text from the errors section
113
+ const errorsList = shadowRoot.querySelectorAll('.error-list .error-message');
114
+ errorsList.forEach(err => {
115
+ const errorText = err.textContent?.trim();
116
+ if (errorText) {
117
+ errors.push(errorText);
118
+ }
119
+ });
120
+ }
121
+ }
122
+ // Check if this is a development extension
123
+ const detailsView = shadowRoot.querySelector('extensions-detail-view');
124
+ const isDevelopment = detailsView ?
125
+ detailsView.shadowRoot?.querySelector('#load-path')?.textContent?.trim() : undefined;
126
+ return {
127
+ found: true,
128
+ id,
129
+ name,
130
+ version,
131
+ description,
132
+ enabled,
133
+ hasErrors,
134
+ errors,
135
+ path: isDevelopment || 'Not a development extension',
136
+ };
137
+ }
138
+ }
139
+ }
140
+ return { found: false };
141
+ }, extensionName);
142
+ if (extensionInfo.found) {
143
+ response.appendResponseLine(`## Extension: ${extensionInfo.name}`);
144
+ response.appendResponseLine('');
145
+ response.appendResponseLine(`**ID:** ${extensionInfo.id}`);
146
+ response.appendResponseLine(`**Version:** ${extensionInfo.version}`);
147
+ response.appendResponseLine(`**Status:** ${extensionInfo.enabled ? '✅ Enabled' : '❌ Disabled'}`);
148
+ if (extensionInfo.description) {
149
+ response.appendResponseLine(`**Description:** ${extensionInfo.description}`);
150
+ }
151
+ if (extensionInfo.path !== 'Not a development extension') {
152
+ response.appendResponseLine(`**Path:** ${extensionInfo.path}`);
153
+ }
154
+ response.appendResponseLine('');
155
+ if (extensionInfo.hasErrors) {
156
+ response.appendResponseLine('⚠️ **Errors:**');
157
+ if (extensionInfo.errors.length > 0) {
158
+ extensionInfo.errors.forEach(err => {
159
+ response.appendResponseLine(` - ${err}`);
160
+ });
161
+ }
162
+ else {
163
+ response.appendResponseLine(' Extension has errors (details not available)');
164
+ }
165
+ }
166
+ else {
167
+ response.appendResponseLine('✅ No errors detected');
168
+ }
169
+ }
170
+ else {
171
+ response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
172
+ response.appendResponseLine('');
173
+ response.appendResponseLine('💡 Use `list_extensions` to see all installed extensions');
174
+ }
175
+ });
176
+ },
177
+ });
74
178
  export const reloadExtension = defineTool({
75
179
  name: 'reload_extension',
76
- description: `Reload a Chrome extension to apply changes during development.`,
180
+ description: `Reload a Chrome extension to apply changes during development. Checks extension state before and after reload.`,
77
181
  annotations: {
78
182
  category: ToolCategories.EXTENSION_DEVELOPMENT,
79
183
  readOnlyHint: false,
@@ -88,6 +192,39 @@ export const reloadExtension = defineTool({
88
192
  const { extensionName } = request.params;
89
193
  await context.waitForEventsAfterAction(async () => {
90
194
  await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
195
+ // Get extension info before reload
196
+ const beforeState = await page.evaluate((searchName) => {
197
+ const extensionCards = document.querySelectorAll('extensions-item');
198
+ for (const card of Array.from(extensionCards)) {
199
+ const shadowRoot = card.shadowRoot;
200
+ if (shadowRoot) {
201
+ const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
202
+ if (name.toLowerCase().includes(searchName.toLowerCase())) {
203
+ const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
204
+ const enableToggle = shadowRoot.querySelector('#enable-toggle');
205
+ const enabled = enableToggle?.getAttribute('checked') === '';
206
+ const id = card.getAttribute('id') || 'unknown';
207
+ return {
208
+ found: true,
209
+ id,
210
+ name,
211
+ version,
212
+ enabled,
213
+ };
214
+ }
215
+ }
216
+ }
217
+ return { found: false };
218
+ }, extensionName);
219
+ if (!beforeState.found) {
220
+ response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
221
+ return;
222
+ }
223
+ if (!beforeState.enabled) {
224
+ response.appendResponseLine(`⚠️ Warning: Extension "${beforeState.name}" is currently disabled`);
225
+ }
226
+ response.appendResponseLine(`🔄 Reloading: ${beforeState.name} v${beforeState.version}`);
227
+ // Perform reload
91
228
  const reloadResult = await page.evaluate((searchName) => {
92
229
  const extensionCards = document.querySelectorAll('extensions-item');
93
230
  for (const card of Array.from(extensionCards)) {
@@ -98,7 +235,7 @@ export const reloadExtension = defineTool({
98
235
  const reloadButton = shadowRoot.querySelector('#reload-button');
99
236
  if (reloadButton && !reloadButton.hasAttribute('hidden')) {
100
237
  reloadButton.click();
101
- return { success: true, extensionName: name };
238
+ return { success: true };
102
239
  }
103
240
  else {
104
241
  return {
@@ -111,15 +248,238 @@ export const reloadExtension = defineTool({
111
248
  }
112
249
  return { success: false, reason: 'Extension not found' };
113
250
  }, extensionName);
114
- if (reloadResult.success) {
115
- response.appendResponseLine(`✅ Reloaded: ${reloadResult.extensionName}`);
251
+ if (!reloadResult.success) {
252
+ response.appendResponseLine(`❌ Failed: ${reloadResult.reason}`);
253
+ return;
254
+ }
255
+ // Wait for reload to complete
256
+ await new Promise(resolve => setTimeout(resolve, 1000));
257
+ // Check for errors after reload
258
+ const afterState = await page.evaluate((searchName) => {
259
+ const extensionCards = document.querySelectorAll('extensions-item');
260
+ for (const card of Array.from(extensionCards)) {
261
+ const shadowRoot = card.shadowRoot;
262
+ if (shadowRoot) {
263
+ const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
264
+ if (name.toLowerCase().includes(searchName.toLowerCase())) {
265
+ const version = shadowRoot.querySelector('#version')?.textContent?.trim() || '';
266
+ const errorsBadge = shadowRoot.querySelector('#errors-button .badge');
267
+ const hasErrors = errorsBadge
268
+ ? parseInt(errorsBadge.textContent?.trim() || '0') > 0
269
+ : false;
270
+ return {
271
+ found: true,
272
+ version,
273
+ hasErrors,
274
+ };
275
+ }
276
+ }
277
+ }
278
+ return { found: false, hasErrors: false };
279
+ }, extensionName);
280
+ response.appendResponseLine('');
281
+ if (afterState.hasErrors) {
282
+ response.appendResponseLine(`⚠️ Extension reloaded but has errors (v${afterState.version})`);
283
+ response.appendResponseLine('💡 Use `get_extension_info` to see error details');
116
284
  }
117
285
  else {
118
- response.appendResponseLine(`❌ Failed: ${reloadResult.reason}`);
286
+ response.appendResponseLine(`✅ Successfully reloaded: ${beforeState.name} v${afterState.version}`);
287
+ }
288
+ });
289
+ },
290
+ });
291
+ export const toggleExtensionState = defineTool({
292
+ name: 'toggle_extension_state',
293
+ description: `Safely enable or disable a Chrome extension. Always checks current state before toggling to prevent accidental changes.`,
294
+ annotations: {
295
+ category: ToolCategories.EXTENSION_DEVELOPMENT,
296
+ readOnlyHint: false,
297
+ },
298
+ schema: {
299
+ extensionName: z
300
+ .string()
301
+ .describe('The name or partial name of the extension'),
302
+ state: z
303
+ .enum(['enable', 'disable'])
304
+ .describe('Desired state: "enable" or "disable"'),
305
+ },
306
+ handler: async (request, response, context) => {
307
+ const page = context.getSelectedPage();
308
+ const { extensionName, state } = request.params;
309
+ const desiredEnabled = state === 'enable';
310
+ await context.waitForEventsAfterAction(async () => {
311
+ await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
312
+ const result = await page.evaluate((searchName, targetEnabled) => {
313
+ const extensionCards = document.querySelectorAll('extensions-item');
314
+ for (const card of Array.from(extensionCards)) {
315
+ const shadowRoot = card.shadowRoot;
316
+ if (shadowRoot) {
317
+ const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
318
+ if (name.toLowerCase().includes(searchName.toLowerCase())) {
319
+ const enableToggle = shadowRoot.querySelector('#enable-toggle');
320
+ const currentEnabled = enableToggle?.getAttribute('checked') === '';
321
+ // Check if already in desired state
322
+ if (currentEnabled === targetEnabled) {
323
+ return {
324
+ success: true,
325
+ alreadyInState: true,
326
+ extensionName: name,
327
+ currentState: currentEnabled,
328
+ };
329
+ }
330
+ // Toggle the state
331
+ if (enableToggle) {
332
+ enableToggle.click();
333
+ return {
334
+ success: true,
335
+ alreadyInState: false,
336
+ extensionName: name,
337
+ previousState: currentEnabled,
338
+ newState: targetEnabled,
339
+ };
340
+ }
341
+ else {
342
+ return {
343
+ success: false,
344
+ reason: 'Enable/disable toggle not found',
345
+ };
346
+ }
347
+ }
348
+ }
349
+ }
350
+ return { success: false, reason: 'Extension not found' };
351
+ }, extensionName, desiredEnabled);
352
+ if (!result.success) {
353
+ response.appendResponseLine(`❌ Failed: ${result.reason}`);
354
+ return;
355
+ }
356
+ if (result.alreadyInState) {
357
+ response.appendResponseLine(`ℹ️ Extension "${result.extensionName}" is already ${result.currentState ? 'enabled' : 'disabled'}`);
358
+ }
359
+ else {
360
+ response.appendResponseLine(`✅ ${result.extensionName}: ${result.previousState ? 'Enabled' : 'Disabled'} → ${result.newState ? 'Enabled' : 'Disabled'}`);
361
+ }
362
+ });
363
+ },
364
+ });
365
+ export const openExtensionPopup = defineTool({
366
+ name: 'open_extension_popup',
367
+ 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.`,
368
+ annotations: {
369
+ category: ToolCategories.EXTENSION_DEVELOPMENT,
370
+ readOnlyHint: false,
371
+ },
372
+ schema: {
373
+ extensionName: z
374
+ .string()
375
+ .describe('The name or partial name of the extension'),
376
+ },
377
+ handler: async (request, response, context) => {
378
+ const page = context.getSelectedPage();
379
+ const { extensionName } = request.params;
380
+ await context.waitForEventsAfterAction(async () => {
381
+ // First, get extension ID
382
+ await page.goto('chrome://extensions/', { waitUntil: 'networkidle0' });
383
+ const extensionInfo = await page.evaluate((searchName) => {
384
+ const extensionCards = document.querySelectorAll('extensions-item');
385
+ for (const card of Array.from(extensionCards)) {
386
+ const shadowRoot = card.shadowRoot;
387
+ if (shadowRoot) {
388
+ const name = shadowRoot.querySelector('#name')?.textContent?.trim() || '';
389
+ if (name.toLowerCase().includes(searchName.toLowerCase())) {
390
+ const id = card.getAttribute('id') || '';
391
+ return { found: true, id, name };
392
+ }
393
+ }
394
+ }
395
+ return { found: false };
396
+ }, extensionName);
397
+ if (!extensionInfo.found) {
398
+ response.appendResponseLine(`❌ Extension not found: "${extensionName}"`);
399
+ return;
400
+ }
401
+ response.appendResponseLine(`🔍 Found extension: ${extensionInfo.name} (${extensionInfo.id})`);
402
+ try {
403
+ // Find service worker target
404
+ const browser = page.browser();
405
+ if (!browser) {
406
+ response.appendResponseLine('❌ Failed to get browser instance.');
407
+ return;
408
+ }
409
+ const targets = await browser.targets();
410
+ const workerTarget = targets.find((target) => target.type() === 'service_worker' &&
411
+ target.url().includes(extensionInfo.id));
412
+ if (!workerTarget) {
413
+ response.appendResponseLine('❌ Service worker not found. Extension may not have a service worker (MV2 extensions are not supported).');
414
+ return;
415
+ }
416
+ const worker = await workerTarget.worker();
417
+ if (!worker) {
418
+ response.appendResponseLine('❌ Failed to get service worker context.');
419
+ return;
420
+ }
421
+ response.appendResponseLine('🔧 Opening popup via service worker...');
422
+ // Open popup
423
+ await worker.evaluate('chrome.action.openPopup();');
424
+ // Wait for popup target
425
+ const popupTarget = await browser.waitForTarget((target) => target.type() === 'page' &&
426
+ target.url().includes(extensionInfo.id) &&
427
+ target.url().includes('popup'), { timeout: 5000 });
428
+ if (!popupTarget) {
429
+ response.appendResponseLine('❌ Popup did not open within timeout.');
430
+ return;
431
+ }
432
+ const popupPage = await popupTarget.page();
433
+ if (!popupPage) {
434
+ response.appendResponseLine('❌ Failed to get popup page reference.');
435
+ return;
436
+ }
437
+ // Add popup page to context and select it
438
+ const pages = await browser.pages();
439
+ const popupIndex = pages.indexOf(popupPage);
440
+ if (popupIndex !== -1) {
441
+ context.setSelectedPageIdx(popupIndex);
442
+ response.appendResponseLine('');
443
+ response.appendResponseLine(`✅ Popup opened: ${extensionInfo.name}`);
444
+ response.appendResponseLine(`📄 Popup URL: ${popupPage.url()}`);
445
+ response.appendResponseLine('');
446
+ response.appendResponseLine('💡 You can now use take_snapshot, click, evaluate_script, etc. on the popup');
447
+ }
448
+ else {
449
+ response.appendResponseLine('⚠️ Popup opened but could not be selected automatically.');
450
+ }
451
+ }
452
+ catch (error) {
453
+ response.appendResponseLine(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
119
454
  }
120
455
  });
121
456
  },
122
457
  });
458
+ export const closeExtensionPopup = defineTool({
459
+ name: 'close_extension_popup',
460
+ description: `Close the currently selected extension popup page.`,
461
+ annotations: {
462
+ category: ToolCategories.EXTENSION_DEVELOPMENT,
463
+ readOnlyHint: false,
464
+ },
465
+ schema: {},
466
+ handler: async (_request, response, context) => {
467
+ const page = context.getSelectedPage();
468
+ const url = page.url();
469
+ if (!url.startsWith('chrome-extension://')) {
470
+ response.appendResponseLine('❌ Current page is not an extension popup');
471
+ response.appendResponseLine(`Current URL: ${url}`);
472
+ return;
473
+ }
474
+ try {
475
+ await page.close();
476
+ response.appendResponseLine('✅ Extension popup closed');
477
+ }
478
+ catch (error) {
479
+ response.appendResponseLine(`❌ Failed to close popup: ${error instanceof Error ? error.message : String(error)}`);
480
+ }
481
+ },
482
+ });
123
483
  export const inspectServiceWorker = defineTool({
124
484
  name: 'inspect_service_worker',
125
485
  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.7.2",
3
+ "version": "0.8.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": "./build/src/index.js",