chrome-devtools-mcp-for-extension 0.8.9 → 0.9.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.
@@ -478,7 +478,29 @@ export const openExtensionPopup = defineTool({
478
478
  response.appendResponseLine('💡 You can now use take_snapshot, click, evaluate_script, etc. on the popup');
479
479
  return;
480
480
  }
481
- // If not on popup, try to find any open popup
481
+ // Check for iframe-embedded popup in current page
482
+ const iframePopups = await page.evaluate(() => {
483
+ return Array.from(document.querySelectorAll('iframe'))
484
+ .filter((iframe) => iframe.src.startsWith('chrome-extension://'))
485
+ .map((iframe) => ({
486
+ src: iframe.src,
487
+ id: iframe.id,
488
+ className: iframe.className,
489
+ }));
490
+ });
491
+ if (iframePopups.length > 0) {
492
+ response.appendResponseLine('✅ Extension popup found (embedded as iframe)');
493
+ response.appendResponseLine(`📄 Popup URL: ${iframePopups[0].src}`);
494
+ response.appendResponseLine('');
495
+ response.appendResponseLine('💡 This popup is embedded in the current page as an iframe.');
496
+ response.appendResponseLine(' You can interact with it using regular page tools:');
497
+ response.appendResponseLine(' - take_snapshot (includes iframe content)');
498
+ response.appendResponseLine(' - click on elements');
499
+ response.appendResponseLine(' - fill forms');
500
+ response.appendResponseLine(' - evaluate_script');
501
+ return;
502
+ }
503
+ // If not on popup or iframe, try to find any open popup window
482
504
  const pages = await browser.pages();
483
505
  for (let i = 0; i < pages.length; i++) {
484
506
  const p = pages[i];
@@ -549,6 +571,32 @@ export const openExtensionPopup = defineTool({
549
571
  }
550
572
  }
551
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
+ }
552
600
  response.appendResponseLine('❌ Popup window not found.');
553
601
  response.appendResponseLine('💡 Please manually click the extension icon to open the popup first.');
554
602
  return;
@@ -664,3 +712,120 @@ export const inspectServiceWorker = defineTool({
664
712
  });
665
713
  },
666
714
  });
715
+ // Import iframe popup tools
716
+ import * as iframePopupTools from './iframe-popup-tools.js';
717
+ export const inspectIframePopup = defineTool({
718
+ name: 'inspect_iframe_popup',
719
+ description: `Inspect an iframe-embedded extension popup using CDP. This tool can access iframe content that normal Puppeteer cannot reach. Returns the full HTML of the iframe popup.`,
720
+ annotations: {
721
+ category: ToolCategories.EXTENSION_DEVELOPMENT,
722
+ readOnlyHint: true,
723
+ },
724
+ schema: {
725
+ urlPattern: z
726
+ .string()
727
+ .describe('Regular expression pattern to match the iframe URL (e.g., "chrome-extension://[^/]+/popup\\.html$")'),
728
+ waitMs: z
729
+ .number()
730
+ .optional()
731
+ .describe('Maximum time to wait for iframe (default: 5000ms)'),
732
+ },
733
+ handler: async (request, response, context) => {
734
+ const page = context.getSelectedPage();
735
+ const { urlPattern, waitMs } = request.params;
736
+ await context.waitForEventsAfterAction(async () => {
737
+ try {
738
+ const cdp = await page.createCDPSession();
739
+ const pattern = new RegExp(urlPattern);
740
+ const result = await iframePopupTools.inspectIframe(cdp, pattern, waitMs ?? 5000);
741
+ response.appendResponseLine('✅ Successfully inspected iframe popup');
742
+ response.appendResponseLine('');
743
+ response.appendResponseLine(`📄 Frame URL: ${result.frameUrl}`);
744
+ response.appendResponseLine(`🆔 Frame ID: ${result.frameId}`);
745
+ response.appendResponseLine('');
746
+ response.appendResponseLine('📝 HTML Content:');
747
+ response.appendResponseLine('```html');
748
+ response.appendResponseLine(result.html.length > 2000
749
+ ? result.html.substring(0, 2000) + '\n... (truncated)'
750
+ : result.html);
751
+ response.appendResponseLine('```');
752
+ await cdp.detach();
753
+ }
754
+ catch (error) {
755
+ response.appendResponseLine(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
756
+ }
757
+ });
758
+ },
759
+ });
760
+ export const patchIframePopup = defineTool({
761
+ name: 'patch_iframe_popup',
762
+ description: `Patch local extension source files and reload the extension. This allows live editing of iframe-embedded popups. The extension must be loaded from a local directory.`,
763
+ annotations: {
764
+ category: ToolCategories.EXTENSION_DEVELOPMENT,
765
+ readOnlyHint: false,
766
+ },
767
+ schema: {
768
+ extensionPath: z
769
+ .string()
770
+ .describe('Absolute path to the extension directory'),
771
+ patches: z
772
+ .array(z.object({
773
+ file: z
774
+ .string()
775
+ .describe('Relative path to file within extension (e.g., "popup.html")'),
776
+ find: z
777
+ .string()
778
+ .describe('Regular expression pattern to find'),
779
+ replace: z.string().describe('Replacement string'),
780
+ }))
781
+ .describe('Array of patches to apply'),
782
+ },
783
+ handler: async (request, response, context) => {
784
+ const page = context.getSelectedPage();
785
+ const { extensionPath, patches } = request.params;
786
+ await context.waitForEventsAfterAction(async () => {
787
+ try {
788
+ const cdp = await page.createCDPSession();
789
+ response.appendResponseLine(`🔧 Applying ${patches.length} patch(es) to extension...`);
790
+ await iframePopupTools.patchAndReload(cdp, extensionPath, patches);
791
+ response.appendResponseLine('');
792
+ response.appendResponseLine('✅ Patches applied successfully');
793
+ response.appendResponseLine('🔄 Extension reloaded');
794
+ response.appendResponseLine('');
795
+ response.appendResponseLine('Applied patches:');
796
+ for (const p of patches) {
797
+ response.appendResponseLine(` • ${p.file}: "${p.find}" → "${p.replace}"`);
798
+ }
799
+ await cdp.detach();
800
+ }
801
+ catch (error) {
802
+ response.appendResponseLine(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
803
+ }
804
+ });
805
+ },
806
+ });
807
+ export const reloadIframeExtension = defineTool({
808
+ name: 'reload_iframe_extension',
809
+ description: `Reload the extension via its service worker using chrome.runtime.reload(). Useful after manually editing extension files.`,
810
+ annotations: {
811
+ category: ToolCategories.EXTENSION_DEVELOPMENT,
812
+ readOnlyHint: false,
813
+ },
814
+ schema: {},
815
+ handler: async (request, response, context) => {
816
+ const page = context.getSelectedPage();
817
+ await context.waitForEventsAfterAction(async () => {
818
+ try {
819
+ const cdp = await page.createCDPSession();
820
+ response.appendResponseLine('🔄 Reloading extension...');
821
+ await iframePopupTools.reloadExtension(cdp);
822
+ response.appendResponseLine('');
823
+ response.appendResponseLine('✅ Extension reloaded successfully');
824
+ await cdp.detach();
825
+ }
826
+ catch (error) {
827
+ response.appendResponseLine(`❌ Error: ${error instanceof Error ? error.message : String(error)}`);
828
+ }
829
+ });
830
+ },
831
+ });
@@ -0,0 +1,103 @@
1
+ // src/tools/iframe-popup-tools.ts
2
+ // Tools for inspecting & editing in-page iframe popups via CDP.
3
+ // These tools enable direct access to iframe-embedded extension popups.
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ export async function findExtensionIdViaTargets(cdp) {
7
+ const { targetInfos } = await cdp.send('Target.getTargets');
8
+ const ext = targetInfos.find((t) => t.type === 'service_worker' && t.url.startsWith('chrome-extension://'));
9
+ if (!ext)
10
+ throw new Error('Extension service worker not found');
11
+ return new URL(ext.url).host;
12
+ }
13
+ export async function waitForFrameByUrlMatch(cdp, pattern, timeoutMs = 5000) {
14
+ await cdp.send('Page.enable');
15
+ // First quick scan
16
+ const tree = await cdp.send('Page.getFrameTree');
17
+ const hit = scanTree(tree.frameTree, pattern);
18
+ if (hit)
19
+ return hit;
20
+ // Then wait for navigation events
21
+ const start = Date.now();
22
+ return await new Promise((resolve, reject) => {
23
+ function onNav(ev) {
24
+ const { frame } = ev;
25
+ if (frame?.url && pattern.test(frame.url)) {
26
+ cleanup();
27
+ resolve({ frameId: frame.id, frameUrl: frame.url });
28
+ }
29
+ }
30
+ function onTimeout() {
31
+ cleanup();
32
+ reject(new Error(`Timeout waiting for frame by url match: ${pattern}`));
33
+ }
34
+ function cleanup() {
35
+ cdp.off('Page.frameNavigated', onNav);
36
+ }
37
+ cdp.on('Page.frameNavigated', onNav);
38
+ const left = Math.max(0, timeoutMs - (Date.now() - start));
39
+ setTimeout(onTimeout, left);
40
+ });
41
+ function scanTree(node, rx) {
42
+ if (node?.frame?.url && rx.test(node.frame.url)) {
43
+ return { frameId: node.frame.id, frameUrl: node.frame.url };
44
+ }
45
+ for (const c of node.childFrames ?? []) {
46
+ const r = scanTree(c, rx);
47
+ if (r)
48
+ return r;
49
+ }
50
+ return null;
51
+ }
52
+ }
53
+ export async function inspectIframe(cdp, urlPattern, waitMs = 5000) {
54
+ await cdp.send('Runtime.enable');
55
+ await cdp.send('DOM.enable');
56
+ await cdp.send('Log.enable');
57
+ const { frameId, frameUrl } = await waitForFrameByUrlMatch(cdp, urlPattern, waitMs);
58
+ const { executionContextId } = await cdp.send('Page.createIsolatedWorld', {
59
+ frameId,
60
+ worldName: 'mcp',
61
+ // grantUniveralAccess is a known CDP option in some builds. It's optional here.
62
+ });
63
+ const { result } = await cdp.send('Runtime.evaluate', {
64
+ contextId: executionContextId,
65
+ expression: 'document.documentElement.outerHTML',
66
+ returnByValue: true,
67
+ });
68
+ return {
69
+ frameId,
70
+ frameUrl,
71
+ html: String(result.value ?? ''),
72
+ };
73
+ }
74
+ export async function patchAndReload(cdp, extensionPath, patches) {
75
+ for (const p of patches) {
76
+ const abs = path.join(extensionPath, p.file);
77
+ const src = await fs.readFile(abs, 'utf8');
78
+ const rx = new RegExp(p.find, 'g');
79
+ const out = src.replace(rx, p.replace);
80
+ if (out != src)
81
+ await fs.writeFile(abs, out, 'utf8');
82
+ }
83
+ await reloadExtension(cdp);
84
+ }
85
+ export async function reloadExtension(cdp) {
86
+ const { targetInfos } = await cdp.send('Target.getTargets');
87
+ const sw = targetInfos.find((t) => t.type === 'service_worker' && t.url.startsWith('chrome-extension://'));
88
+ if (!sw)
89
+ throw new Error('Extension service worker not found for reload');
90
+ // Attach to the service worker and execute chrome.runtime.reload()
91
+ const { sessionId } = await cdp.send('Target.attachToTarget', {
92
+ targetId: sw.targetId,
93
+ flatten: true,
94
+ });
95
+ await cdp.send('Target.sendMessageToTarget', {
96
+ sessionId,
97
+ message: JSON.stringify({
98
+ id: 1,
99
+ method: 'Runtime.evaluate',
100
+ params: { expression: 'chrome.runtime.reload()' },
101
+ }),
102
+ });
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.8.9",
3
+ "version": "0.9.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",