chrome-devtools-mcp-for-extension 0.8.10 → 0.9.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.
@@ -712,3 +712,120 @@ export const inspectServiceWorker = defineTool({
712
712
  });
713
713
  },
714
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,121 @@
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
+ await cdp.send('Runtime.enable');
16
+ await cdp.send('DOM.enable');
17
+ // Strategy: Find iframe in DOM, then match Frame ID
18
+ const start = Date.now();
19
+ while (Date.now() - start < timeoutMs) {
20
+ try {
21
+ // Get document root
22
+ const { root } = await cdp.send('DOM.getDocument', { depth: -1 });
23
+ // Query all iframes
24
+ const { nodeIds } = await cdp.send('DOM.querySelectorAll', {
25
+ nodeId: root.nodeId,
26
+ selector: 'iframe',
27
+ });
28
+ // Check each iframe's src
29
+ for (const nodeId of nodeIds) {
30
+ const attrs = await cdp.send('DOM.getAttributes', { nodeId });
31
+ const srcIndex = attrs.attributes.indexOf('src');
32
+ if (srcIndex >= 0 && srcIndex + 1 < attrs.attributes.length) {
33
+ const src = attrs.attributes[srcIndex + 1];
34
+ if (pattern.test(src)) {
35
+ // Get contentDocument frame ID
36
+ const { node } = await cdp.send('DOM.describeNode', { nodeId });
37
+ if (node.contentDocument) {
38
+ const frameId = node.contentDocument.frameId || node.frameId;
39
+ if (frameId) {
40
+ return { frameId, frameUrl: src };
41
+ }
42
+ }
43
+ // Fallback: try Frame Tree match
44
+ const tree = await cdp.send('Page.getFrameTree');
45
+ const hit = findFrameByUrl(tree.frameTree, src);
46
+ if (hit)
47
+ return hit;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ catch (e) {
53
+ // DOM may not be ready yet, continue waiting
54
+ }
55
+ // Wait a bit before retry
56
+ await new Promise(resolve => setTimeout(resolve, 100));
57
+ }
58
+ throw new Error(`Timeout waiting for iframe by url match: ${pattern}`);
59
+ function findFrameByUrl(node, url) {
60
+ if (node?.frame?.url === url) {
61
+ return { frameId: node.frame.id, frameUrl: node.frame.url };
62
+ }
63
+ for (const c of node.childFrames ?? []) {
64
+ const r = findFrameByUrl(c, url);
65
+ if (r)
66
+ return r;
67
+ }
68
+ return null;
69
+ }
70
+ }
71
+ export async function inspectIframe(cdp, urlPattern, waitMs = 5000) {
72
+ await cdp.send('Runtime.enable');
73
+ await cdp.send('DOM.enable');
74
+ await cdp.send('Log.enable');
75
+ const { frameId, frameUrl } = await waitForFrameByUrlMatch(cdp, urlPattern, waitMs);
76
+ const { executionContextId } = await cdp.send('Page.createIsolatedWorld', {
77
+ frameId,
78
+ worldName: 'mcp',
79
+ // grantUniveralAccess is a known CDP option in some builds. It's optional here.
80
+ });
81
+ const { result } = await cdp.send('Runtime.evaluate', {
82
+ contextId: executionContextId,
83
+ expression: 'document.documentElement.outerHTML',
84
+ returnByValue: true,
85
+ });
86
+ return {
87
+ frameId,
88
+ frameUrl,
89
+ html: String(result.value ?? ''),
90
+ };
91
+ }
92
+ export async function patchAndReload(cdp, extensionPath, patches) {
93
+ for (const p of patches) {
94
+ const abs = path.join(extensionPath, p.file);
95
+ const src = await fs.readFile(abs, 'utf8');
96
+ const rx = new RegExp(p.find, 'g');
97
+ const out = src.replace(rx, p.replace);
98
+ if (out != src)
99
+ await fs.writeFile(abs, out, 'utf8');
100
+ }
101
+ await reloadExtension(cdp);
102
+ }
103
+ export async function reloadExtension(cdp) {
104
+ const { targetInfos } = await cdp.send('Target.getTargets');
105
+ const sw = targetInfos.find((t) => t.type === 'service_worker' && t.url.startsWith('chrome-extension://'));
106
+ if (!sw)
107
+ throw new Error('Extension service worker not found for reload');
108
+ // Attach to the service worker and execute chrome.runtime.reload()
109
+ const { sessionId } = await cdp.send('Target.attachToTarget', {
110
+ targetId: sw.targetId,
111
+ flatten: true,
112
+ });
113
+ await cdp.send('Target.sendMessageToTarget', {
114
+ sessionId,
115
+ message: JSON.stringify({
116
+ id: 1,
117
+ method: 'Runtime.evaluate',
118
+ params: { expression: 'chrome.runtime.reload()' },
119
+ }),
120
+ });
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-devtools-mcp-for-extension",
3
- "version": "0.8.10",
3
+ "version": "0.9.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",