chrome-devtools-mcp-for-extension 0.9.1 → 0.9.3
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.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/tools/iframe-popup-tools.ts
|
|
2
2
|
// Tools for inspecting & editing in-page iframe popups via CDP.
|
|
3
3
|
// These tools enable direct access to iframe-embedded extension popups.
|
|
4
|
+
// Uses OOPIF (Out-Of-Process iFrame) detection via Target.setAutoAttach.
|
|
4
5
|
import fs from 'node:fs/promises';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
export async function findExtensionIdViaTargets(cdp) {
|
|
@@ -10,85 +11,100 @@ export async function findExtensionIdViaTargets(cdp) {
|
|
|
10
11
|
throw new Error('Extension service worker not found');
|
|
11
12
|
return new URL(ext.url).host;
|
|
12
13
|
}
|
|
13
|
-
export async function
|
|
14
|
-
await cdp.send('
|
|
15
|
-
await cdp.send('
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export async function enableOopifAutoAttach(cdp) {
|
|
15
|
+
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
16
|
+
await cdp.send('Target.setAutoAttach', {
|
|
17
|
+
autoAttach: true,
|
|
18
|
+
waitForDebuggerOnStart: false,
|
|
19
|
+
flatten: true, // Essential for OOPIF detection
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function waitForExtensionChildTarget(cdp, pattern, timeoutMs = 8000) {
|
|
18
23
|
const start = Date.now();
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
}
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
let resolved = false;
|
|
26
|
+
const onAttach = (e) => {
|
|
27
|
+
const url = e?.targetInfo?.url || '';
|
|
28
|
+
if (pattern.test(url)) {
|
|
29
|
+
resolved = true;
|
|
30
|
+
cleanup();
|
|
31
|
+
resolve({
|
|
32
|
+
sessionId: e.sessionId,
|
|
33
|
+
targetId: e.targetInfo.targetId,
|
|
34
|
+
url,
|
|
35
|
+
});
|
|
50
36
|
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
37
|
+
};
|
|
38
|
+
const onTimeout = () => {
|
|
39
|
+
if (!resolved) {
|
|
40
|
+
cleanup();
|
|
41
|
+
reject(new Error(`Timeout: extension popup child target not found (pattern: ${pattern})`));
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const cleanup = () => {
|
|
45
|
+
cdp.off('Target.attachedToTarget', onAttach);
|
|
46
|
+
};
|
|
47
|
+
cdp.on('Target.attachedToTarget', onAttach);
|
|
48
|
+
setTimeout(onTimeout, Math.max(0, timeoutMs - (Date.now() - start)));
|
|
80
49
|
});
|
|
81
|
-
|
|
82
|
-
|
|
50
|
+
}
|
|
51
|
+
export async function inspectIframe(cdp, urlPattern, waitMs = 8000) {
|
|
52
|
+
// Enable OOPIF auto-attach
|
|
53
|
+
await enableOopifAutoAttach(cdp);
|
|
54
|
+
// Wait for child target (extension iframe)
|
|
55
|
+
const child = await waitForExtensionChildTarget(cdp, urlPattern, waitMs);
|
|
56
|
+
// Enable Page/Runtime in child session
|
|
57
|
+
await sendToChildSession(cdp, child.sessionId, 'Page.enable', {});
|
|
58
|
+
await sendToChildSession(cdp, child.sessionId, 'Runtime.enable', {});
|
|
59
|
+
// Evaluate outerHTML in child session
|
|
60
|
+
const htmlResult = await sendToChildSession(cdp, child.sessionId, 'Runtime.evaluate', {
|
|
83
61
|
expression: 'document.documentElement.outerHTML',
|
|
84
62
|
returnByValue: true,
|
|
85
63
|
});
|
|
64
|
+
const html = String(htmlResult?.result?.value ?? '');
|
|
86
65
|
return {
|
|
87
|
-
frameId,
|
|
88
|
-
frameUrl,
|
|
89
|
-
html
|
|
66
|
+
frameId: child.targetId, // Use targetId as frameId for OOPIF
|
|
67
|
+
frameUrl: child.url,
|
|
68
|
+
html,
|
|
90
69
|
};
|
|
91
70
|
}
|
|
71
|
+
async function sendToChildSession(cdp, sessionId, method, params) {
|
|
72
|
+
const id = Math.floor(Math.random() * 1000000);
|
|
73
|
+
const message = JSON.stringify({ id, method, params });
|
|
74
|
+
// Send message to child target
|
|
75
|
+
await cdp.send('Target.sendMessageToTarget', { sessionId, message });
|
|
76
|
+
// Wait for response from child target
|
|
77
|
+
return await new Promise((resolve, reject) => {
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
cleanup();
|
|
80
|
+
reject(new Error(`Timeout waiting for response from child session: ${method}`));
|
|
81
|
+
}, 5000);
|
|
82
|
+
const onMessage = (e) => {
|
|
83
|
+
if (e.sessionId === sessionId) {
|
|
84
|
+
try {
|
|
85
|
+
const response = JSON.parse(e.message);
|
|
86
|
+
if (response.id === id) {
|
|
87
|
+
cleanup();
|
|
88
|
+
if (response.error) {
|
|
89
|
+
reject(new Error(`CDP error: ${JSON.stringify(response.error)}`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
resolve(response.result);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
// Ignore parse errors for other messages
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const cleanup = () => {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
cdp.off('Target.receivedMessageFromTarget', onMessage);
|
|
104
|
+
};
|
|
105
|
+
cdp.on('Target.receivedMessageFromTarget', onMessage);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
92
108
|
export async function patchAndReload(cdp, extensionPath, patches) {
|
|
93
109
|
for (const p of patches) {
|
|
94
110
|
const abs = path.join(extensionPath, p.file);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-devtools-mcp-for-extension",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
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",
|