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.
@@ -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, extensionName: name };
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(`✅ Reloaded: ${reloadResult.extensionName}`);
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(`❌ Failed: ${reloadResult.reason}`);
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.7.2",
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",