@extension.dev/mcp 5.5.2 → 5.6.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +74 -0
- package/dist/module.js +679 -162
- package/dist/src/lib/cdp-port.d.ts +8 -0
- package/dist/src/lib/console-summary.d.ts +5 -0
- package/dist/src/lib/rdp.d.ts +50 -0
- package/dist/src/tools/source-inspect-gecko.d.ts +8 -0
- package/package.json +4 -4
- package/server.json +2 -2
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "extension-mcp",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "MCP tools for browser extension development: scaffold from 60+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox.",
|
|
13
|
-
"version": "5.
|
|
13
|
+
"version": "5.6.1",
|
|
14
14
|
"category": "development",
|
|
15
15
|
"author": {
|
|
16
16
|
"name": "Cezar Augusto"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extension-mcp",
|
|
3
3
|
"description": "MCP tools for browser extension development: scaffold from 60+ templates, run the dev server with HMR, inspect the live DOM and logs, and publish store-ready builds for Chrome, Edge, and Firefox. Ships /extension, /extension-add, /extension-debug, and /extension-publish commands.",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.6.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cezar Augusto",
|
|
7
7
|
"email": "hello@extension.dev",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.6.1
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- `extension_deploy` and `extension_store_status` no longer advertise
|
|
8
|
+
`safari` as a store. The Safari/App Store submission lane does not
|
|
9
|
+
exist yet (the platform now also rejects such submissions server-side
|
|
10
|
+
with `SAFARI_LANE_DISABLED`), so the tool schemas stop inviting agents
|
|
11
|
+
to try it. Safari as a build/run target is unchanged.
|
|
12
|
+
|
|
13
|
+
## 5.6.0
|
|
14
|
+
|
|
15
|
+
Firefox reaches full protocol parity: every formerly Chromium-only
|
|
16
|
+
feature now works on Gecko, over RDP or the agent bridge.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `extension_list_extensions` works on Firefox-family sessions. It rides
|
|
21
|
+
the Remote Debugging Protocol root actor's `listAddons` over the
|
|
22
|
+
`rdpPort` the engine stamps into ready.json from extension.js 4.0.15
|
|
23
|
+
on (upstream entry 78). Entries list INSTALLED add-ons regardless of
|
|
24
|
+
live contexts, `temporarilyInstalled` marks temporary loads, and the
|
|
25
|
+
dev session's extension is flagged `ownExtension` by matching the
|
|
26
|
+
ready contract's identity (with a lone temporary install as the
|
|
27
|
+
fallback signal). Add-on targets are never attached to or evaluated
|
|
28
|
+
in; system and hidden add-ons are filtered out. Older engines get a
|
|
29
|
+
hint naming the 4.0.15 requirement instead of a generic failure. A
|
|
30
|
+
minimal RDP client (`src/lib/rdp.ts`) carries the handshake; legacy
|
|
31
|
+
RDP was chosen over WebDriver BiDi on purpose, since BiDi is
|
|
32
|
+
single-session and would block attaching alongside other consumers.
|
|
33
|
+
- `extension_dom_inspect` listTargets works on Firefox: RDP tab
|
|
34
|
+
descriptors as `{actor,url,title,type}`, with the same two-id-space
|
|
35
|
+
warning as the CDP path (an actor id is not a chrome.tabs id).
|
|
36
|
+
Discovery therefore needs no allowControl on Gecko, unlike listTabs.
|
|
37
|
+
- `extension_source_inspect` closes its four Gecko gaps. dom_snapshot
|
|
38
|
+
and extension_roots ride the bridge eval, embedding the same CDP page
|
|
39
|
+
scripts Chromium uses (the bridge html path also gained the
|
|
40
|
+
shadow-aware serializer, so open extension-root shadow content is in
|
|
41
|
+
the markup now). console rides the RDP watcher's cached-resource
|
|
42
|
+
replay (`getWatcher` with server target switching, then
|
|
43
|
+
watchResources; verified live that `getCachedMessages` is a dead end
|
|
44
|
+
on current Firefox), summarized in the same shape as the CDP console
|
|
45
|
+
buffer. deepDom walks CLOSED shadow roots through
|
|
46
|
+
`tabs.executeScript`, where Firefox exposes
|
|
47
|
+
`Element.openOrClosedShadowRoot` to content scripts, so it needs an
|
|
48
|
+
MV2 session with host permissions for the target url; a failed walk
|
|
49
|
+
reports why in `notes` instead of silently dropping the field.
|
|
50
|
+
- MV2 page-eval fallback: the engine's page-context eval needs
|
|
51
|
+
chrome.scripting, an MV3-only API, so Firefox MV2 sessions reported
|
|
52
|
+
every bridge inspection as Unsupported. The inspect expression now
|
|
53
|
+
falls back to compiling in the tab's content-script sandbox via
|
|
54
|
+
`tabs.executeScript`, which reads the identical DOM; callers see the
|
|
55
|
+
same result shape on both paths.
|
|
56
|
+
|
|
57
|
+
### Fixed
|
|
58
|
+
|
|
59
|
+
- Act-verb CLI output over ~8KB no longer truncates. The engine CLI
|
|
60
|
+
exits without draining stdout, and the socketpair pipe Node hands a
|
|
61
|
+
child buffers about 8KB, so any larger `--output json` frame (a DOM
|
|
62
|
+
snapshot, a big html capture) arrived cut mid-JSON and surfaced as
|
|
63
|
+
"CliError: extension exited with code 0". The MCP now hands the child
|
|
64
|
+
file descriptors and reads them after exit, which no pipe buffer can
|
|
65
|
+
truncate. Filed upstream as extension.js ledger entry 79; the
|
|
66
|
+
workaround stays until the engine flushes before exiting.
|
|
67
|
+
|
|
68
|
+
### Known limitation
|
|
69
|
+
|
|
70
|
+
- Firefox MV3 sessions cannot use the bridge extras: the MV3 event page
|
|
71
|
+
CSP blocks the eval the control bridge dispatches through the
|
|
72
|
+
background, exactly like Chromium MV3 service workers. MV2 sessions
|
|
73
|
+
are fully covered via the executeScript fallback. The RDP paths
|
|
74
|
+
(list_extensions, listTargets, console) work on both manifest
|
|
75
|
+
versions.
|
|
76
|
+
|
|
3
77
|
## 5.5.2
|
|
4
78
|
|
|
5
79
|
Honest browser support wording ahead of the Safari lane landing.
|
package/dist/module.js
CHANGED
|
@@ -12,6 +12,7 @@ import { execFile, execFileSync } from "node:child_process";
|
|
|
12
12
|
import { filterKeysForThisBrowser } from "browser-extension-manifest-fields";
|
|
13
13
|
import ws_0 from "ws";
|
|
14
14
|
import node_http from "node:http";
|
|
15
|
+
import node_net from "node:net";
|
|
15
16
|
import { extensionInstall, extensionUninstall, getManagedBrowsersCacheRoot } from "extension-install";
|
|
16
17
|
import { promisify } from "node:util";
|
|
17
18
|
var add_feature_namespaceObject = {};
|
|
@@ -219,7 +220,7 @@ __webpack_require__.d(whoami_namespaceObject, {
|
|
|
219
220
|
handler: ()=>whoami_handler,
|
|
220
221
|
schema: ()=>whoami_schema
|
|
221
222
|
});
|
|
222
|
-
var package_namespaceObject = JSON.parse('{"rE":"5.
|
|
223
|
+
var package_namespaceObject = JSON.parse('{"rE":"5.6.1","El":{"OP":"^4.0.15"}}');
|
|
223
224
|
function scaffoldEnginePin(projectPath) {
|
|
224
225
|
try {
|
|
225
226
|
const pkg = JSON.parse(node_fs.readFileSync(node_path.join(projectPath, "package.json"), "utf8"));
|
|
@@ -563,6 +564,42 @@ function resolveExtensionInvocation(projectDir) {
|
|
|
563
564
|
function runExtensionCli(args, options) {
|
|
564
565
|
const { command, prefixArgs } = resolveExtensionInvocation(options?.cwd);
|
|
565
566
|
return new Promise((resolve)=>{
|
|
567
|
+
let ioDir;
|
|
568
|
+
let outFd;
|
|
569
|
+
let errFd;
|
|
570
|
+
try {
|
|
571
|
+
ioDir = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "extension-mcp-io-"));
|
|
572
|
+
outFd = node_fs.openSync(node_path.join(ioDir, "stdout"), "a");
|
|
573
|
+
errFd = node_fs.openSync(node_path.join(ioDir, "stderr"), "a");
|
|
574
|
+
} catch (err) {
|
|
575
|
+
resolve({
|
|
576
|
+
code: 1,
|
|
577
|
+
stdout: "",
|
|
578
|
+
stderr: String(err)
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const readAll = (name)=>{
|
|
583
|
+
try {
|
|
584
|
+
return node_fs.readFileSync(node_path.join(ioDir, name), "utf8");
|
|
585
|
+
} catch {
|
|
586
|
+
return "";
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
const cleanup = ()=>{
|
|
590
|
+
for (const fd of [
|
|
591
|
+
outFd,
|
|
592
|
+
errFd
|
|
593
|
+
])try {
|
|
594
|
+
node_fs.closeSync(fd);
|
|
595
|
+
} catch {}
|
|
596
|
+
try {
|
|
597
|
+
node_fs.rmSync(ioDir, {
|
|
598
|
+
recursive: true,
|
|
599
|
+
force: true
|
|
600
|
+
});
|
|
601
|
+
} catch {}
|
|
602
|
+
};
|
|
566
603
|
const child = cross_spawn(command, [
|
|
567
604
|
...prefixArgs,
|
|
568
605
|
...args
|
|
@@ -570,8 +607,8 @@ function runExtensionCli(args, options) {
|
|
|
570
607
|
cwd: options?.cwd,
|
|
571
608
|
stdio: [
|
|
572
609
|
"ignore",
|
|
573
|
-
|
|
574
|
-
|
|
610
|
+
outFd,
|
|
611
|
+
errFd
|
|
575
612
|
],
|
|
576
613
|
env: {
|
|
577
614
|
...process.env,
|
|
@@ -580,20 +617,26 @@ function runExtensionCli(args, options) {
|
|
|
580
617
|
},
|
|
581
618
|
timeout: options?.timeoutMs ?? 30000
|
|
582
619
|
});
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
620
|
+
child.on("close", (code)=>{
|
|
621
|
+
const stdout = readAll("stdout");
|
|
622
|
+
const stderr = readAll("stderr");
|
|
623
|
+
cleanup();
|
|
624
|
+
resolve({
|
|
588
625
|
code,
|
|
589
626
|
stdout,
|
|
590
627
|
stderr
|
|
591
|
-
})
|
|
592
|
-
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
child.on("error", (err)=>{
|
|
631
|
+
const stdout = readAll("stdout");
|
|
632
|
+
const stderr = readAll("stderr");
|
|
633
|
+
cleanup();
|
|
634
|
+
resolve({
|
|
593
635
|
code: 1,
|
|
594
636
|
stdout,
|
|
595
637
|
stderr: stderr || String(err)
|
|
596
|
-
})
|
|
638
|
+
});
|
|
639
|
+
});
|
|
597
640
|
});
|
|
598
641
|
}
|
|
599
642
|
function spawnExtensionCli(args, options) {
|
|
@@ -2557,6 +2600,32 @@ async function inspect_handler(args) {
|
|
|
2557
2600
|
};
|
|
2558
2601
|
return JSON.stringify(result);
|
|
2559
2602
|
}
|
|
2603
|
+
function summarizeConsoleMessages(messages) {
|
|
2604
|
+
const counts = {};
|
|
2605
|
+
const uniqueByLevel = {};
|
|
2606
|
+
for (const msg of messages){
|
|
2607
|
+
counts[msg.level] = (counts[msg.level] ?? 0) + 1;
|
|
2608
|
+
if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
|
|
2609
|
+
const key = msg.text.slice(0, 200);
|
|
2610
|
+
uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
|
|
2611
|
+
}
|
|
2612
|
+
const topMessages = [];
|
|
2613
|
+
for (const [level, msgs] of Object.entries(uniqueByLevel)){
|
|
2614
|
+
const sorted = [
|
|
2615
|
+
...msgs.entries()
|
|
2616
|
+
].sort((a, b)=>b[1] - a[1]);
|
|
2617
|
+
for (const [text, count] of sorted.slice(0, 5))topMessages.push({
|
|
2618
|
+
level,
|
|
2619
|
+
text,
|
|
2620
|
+
count
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
return {
|
|
2624
|
+
total: messages.length,
|
|
2625
|
+
counts,
|
|
2626
|
+
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
2627
|
+
};
|
|
2628
|
+
}
|
|
2560
2629
|
function _define_property(obj, key, value) {
|
|
2561
2630
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
2562
2631
|
value: value,
|
|
@@ -2666,30 +2735,7 @@ class CDPConnection {
|
|
|
2666
2735
|
];
|
|
2667
2736
|
}
|
|
2668
2737
|
getConsoleSummary() {
|
|
2669
|
-
|
|
2670
|
-
const uniqueByLevel = {};
|
|
2671
|
-
for (const msg of this.consoleMessages){
|
|
2672
|
-
counts[msg.level] = (counts[msg.level] ?? 0) + 1;
|
|
2673
|
-
if (!uniqueByLevel[msg.level]) uniqueByLevel[msg.level] = new Map();
|
|
2674
|
-
const key = msg.text.slice(0, 200);
|
|
2675
|
-
uniqueByLevel[msg.level].set(key, (uniqueByLevel[msg.level].get(key) ?? 0) + 1);
|
|
2676
|
-
}
|
|
2677
|
-
const topMessages = [];
|
|
2678
|
-
for (const [level, msgs] of Object.entries(uniqueByLevel)){
|
|
2679
|
-
const sorted = [
|
|
2680
|
-
...msgs.entries()
|
|
2681
|
-
].sort((a, b)=>b[1] - a[1]);
|
|
2682
|
-
for (const [text, count] of sorted.slice(0, 5))topMessages.push({
|
|
2683
|
-
level,
|
|
2684
|
-
text,
|
|
2685
|
-
count
|
|
2686
|
-
});
|
|
2687
|
-
}
|
|
2688
|
-
return {
|
|
2689
|
-
total: this.consoleMessages.length,
|
|
2690
|
-
counts,
|
|
2691
|
-
topMessages: topMessages.sort((a, b)=>b.count - a.count).slice(0, 10)
|
|
2692
|
-
};
|
|
2738
|
+
return summarizeConsoleMessages(this.consoleMessages);
|
|
2693
2739
|
}
|
|
2694
2740
|
onEvent(handler) {
|
|
2695
2741
|
this.eventListeners.add(handler);
|
|
@@ -2964,7 +3010,7 @@ class CDPClient extends CDPConnection {
|
|
|
2964
3010
|
return result ?? null;
|
|
2965
3011
|
}
|
|
2966
3012
|
}
|
|
2967
|
-
async function
|
|
3013
|
+
async function resolveContractPort(projectPath, browser, field, options) {
|
|
2968
3014
|
const waitMs = options?.waitMs ?? 20000;
|
|
2969
3015
|
const graceMs = options?.graceMs ?? 2500;
|
|
2970
3016
|
const readyPath = node_path.resolve(projectPath, "dist", "extension-js", browser, "ready.json");
|
|
@@ -2976,9 +3022,9 @@ async function resolveCdpPort(projectPath, browser, options) {
|
|
|
2976
3022
|
const contract = JSON.parse(node_fs.readFileSync(readyPath, "utf8"));
|
|
2977
3023
|
contractSeen = true;
|
|
2978
3024
|
if (null == contractSeenAt) contractSeenAt = Date.now();
|
|
2979
|
-
if ("number" == typeof contract
|
|
2980
|
-
port: contract
|
|
2981
|
-
|
|
3025
|
+
if ("number" == typeof contract[field]) return {
|
|
3026
|
+
port: contract[field],
|
|
3027
|
+
contractSeen
|
|
2982
3028
|
};
|
|
2983
3029
|
} catch {
|
|
2984
3030
|
if (!contractSeen) break;
|
|
@@ -2987,13 +3033,32 @@ async function resolveCdpPort(projectPath, browser, options) {
|
|
|
2987
3033
|
if (Date.now() >= effectiveDeadline) break;
|
|
2988
3034
|
await new Promise((resolve)=>setTimeout(resolve, 500));
|
|
2989
3035
|
}
|
|
3036
|
+
return {
|
|
3037
|
+
port: null,
|
|
3038
|
+
contractSeen
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
async function resolveCdpPort(projectPath, browser, options) {
|
|
3042
|
+
const { port, contractSeen } = await resolveContractPort(projectPath, browser, "cdpPort", options);
|
|
3043
|
+
if (null != port) return {
|
|
3044
|
+
port,
|
|
3045
|
+
source: "contract"
|
|
3046
|
+
};
|
|
2990
3047
|
if (!contractSeen && await isCdpEndpoint(9222)) return {
|
|
2991
3048
|
port: 9222,
|
|
2992
3049
|
source: "default-probe"
|
|
2993
3050
|
};
|
|
2994
3051
|
return null;
|
|
2995
3052
|
}
|
|
3053
|
+
async function resolveRdpPort(projectPath, browser, options) {
|
|
3054
|
+
const { port } = await resolveContractPort(projectPath, browser, "rdpPort", options);
|
|
3055
|
+
return null != port ? {
|
|
3056
|
+
port,
|
|
3057
|
+
source: "contract"
|
|
3058
|
+
} : null;
|
|
3059
|
+
}
|
|
2996
3060
|
const CDP_PORT_MISSING_HINT = "The session's ready contract has no CDP port (the browser may still be binding its debug port, or was launched without one). Confirm the session with extension_wait, give it a moment, and retry.";
|
|
3061
|
+
const RDP_PORT_MISSING_HINT = "The session's ready contract has no rdpPort. Firefox sessions publish one from extension.js 4.0.15 on; upgrade the project's extension dependency (or remove the local install so the MCP's pinned CLI drives the session) and restart the dev session.";
|
|
2997
3062
|
function isCdpEndpoint(port) {
|
|
2998
3063
|
return new Promise((resolve)=>{
|
|
2999
3064
|
const req = node_http.get({
|
|
@@ -3181,9 +3246,471 @@ async function resolveBridgeBaseUrl(projectPath, browser, timeout) {
|
|
|
3181
3246
|
} catch {}
|
|
3182
3247
|
return null;
|
|
3183
3248
|
}
|
|
3249
|
+
function rdp_define_property(obj, key, value) {
|
|
3250
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
3251
|
+
value: value,
|
|
3252
|
+
enumerable: true,
|
|
3253
|
+
configurable: true,
|
|
3254
|
+
writable: true
|
|
3255
|
+
});
|
|
3256
|
+
else obj[key] = value;
|
|
3257
|
+
return obj;
|
|
3258
|
+
}
|
|
3259
|
+
function encodeRdpPacket(packet) {
|
|
3260
|
+
const json = Buffer.from(JSON.stringify(packet), "utf8");
|
|
3261
|
+
return Buffer.concat([
|
|
3262
|
+
Buffer.from(`${json.length}:`, "ascii"),
|
|
3263
|
+
json
|
|
3264
|
+
]);
|
|
3265
|
+
}
|
|
3266
|
+
class RdpPacketDecoder {
|
|
3267
|
+
push(chunk) {
|
|
3268
|
+
this.buffer = Buffer.concat([
|
|
3269
|
+
this.buffer,
|
|
3270
|
+
chunk
|
|
3271
|
+
]);
|
|
3272
|
+
const packets = [];
|
|
3273
|
+
for(;;){
|
|
3274
|
+
const colon = this.buffer.indexOf(0x3a);
|
|
3275
|
+
if (-1 === colon) {
|
|
3276
|
+
if (this.buffer.length > 16) throw new Error("RDP stream corrupt: no length prefix");
|
|
3277
|
+
break;
|
|
3278
|
+
}
|
|
3279
|
+
const prefix = this.buffer.subarray(0, colon).toString("ascii");
|
|
3280
|
+
if (!/^\d+$/.test(prefix)) throw new Error(`RDP stream corrupt: bad length prefix "${prefix}"`);
|
|
3281
|
+
const length = Number(prefix);
|
|
3282
|
+
if (this.buffer.length < colon + 1 + length) break;
|
|
3283
|
+
const json = this.buffer.subarray(colon + 1, colon + 1 + length);
|
|
3284
|
+
this.buffer = this.buffer.subarray(colon + 1 + length);
|
|
3285
|
+
packets.push(JSON.parse(json.toString("utf8")));
|
|
3286
|
+
}
|
|
3287
|
+
return packets;
|
|
3288
|
+
}
|
|
3289
|
+
constructor(){
|
|
3290
|
+
rdp_define_property(this, "buffer", Buffer.alloc(0));
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
class RdpSession {
|
|
3294
|
+
static connect(port, timeoutMs = 10000) {
|
|
3295
|
+
return new Promise((resolve, reject)=>{
|
|
3296
|
+
const socket = node_net.connect({
|
|
3297
|
+
host: "127.0.0.1",
|
|
3298
|
+
port
|
|
3299
|
+
});
|
|
3300
|
+
const session = new RdpSession(socket);
|
|
3301
|
+
const decoder = new RdpPacketDecoder();
|
|
3302
|
+
let settledConnect = false;
|
|
3303
|
+
const timer = setTimeout(()=>{
|
|
3304
|
+
if (!settledConnect) {
|
|
3305
|
+
settledConnect = true;
|
|
3306
|
+
session.close();
|
|
3307
|
+
reject(new Error(`RDP connect timed out after ${timeoutMs}ms`));
|
|
3308
|
+
}
|
|
3309
|
+
}, timeoutMs);
|
|
3310
|
+
socket.on("data", (chunk)=>{
|
|
3311
|
+
let packets;
|
|
3312
|
+
try {
|
|
3313
|
+
packets = decoder.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3314
|
+
} catch {
|
|
3315
|
+
session.close();
|
|
3316
|
+
return;
|
|
3317
|
+
}
|
|
3318
|
+
for (const packet of packets){
|
|
3319
|
+
if (!settledConnect && "root" === packet.from) {
|
|
3320
|
+
settledConnect = true;
|
|
3321
|
+
clearTimeout(timer);
|
|
3322
|
+
resolve(session);
|
|
3323
|
+
continue;
|
|
3324
|
+
}
|
|
3325
|
+
for (const tap of session.taps)tap(packet);
|
|
3326
|
+
for(let i = 0; i < session.waiters.length; i++)if (session.waiters[i].match(packet)) {
|
|
3327
|
+
session.waiters.splice(i, 1)[0].resolve(packet);
|
|
3328
|
+
break;
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
socket.on("error", (error)=>{
|
|
3333
|
+
if (!settledConnect) {
|
|
3334
|
+
settledConnect = true;
|
|
3335
|
+
clearTimeout(timer);
|
|
3336
|
+
reject(error);
|
|
3337
|
+
}
|
|
3338
|
+
});
|
|
3339
|
+
socket.on("close", ()=>{
|
|
3340
|
+
session.closed = true;
|
|
3341
|
+
for (const waiter of session.waiters.splice(0))waiter.reject(new Error("RDP connection closed before a reply arrived"));
|
|
3342
|
+
});
|
|
3343
|
+
});
|
|
3344
|
+
}
|
|
3345
|
+
request(actor, packet, timeoutMs = 10000) {
|
|
3346
|
+
return new Promise((resolve, reject)=>{
|
|
3347
|
+
if (this.closed) return void reject(new Error("RDP connection is closed"));
|
|
3348
|
+
const waiter = {
|
|
3349
|
+
match: (p)=>p.from === actor && void 0 === p.type,
|
|
3350
|
+
resolve: (p)=>{
|
|
3351
|
+
clearTimeout(timer);
|
|
3352
|
+
if ("string" == typeof p.error) reject(new Error(`RDP actor error: ${p.error}${"string" == typeof p.message ? ` (${p.message})` : ""}`));
|
|
3353
|
+
else resolve(p);
|
|
3354
|
+
},
|
|
3355
|
+
reject: (error)=>{
|
|
3356
|
+
clearTimeout(timer);
|
|
3357
|
+
reject(error);
|
|
3358
|
+
}
|
|
3359
|
+
};
|
|
3360
|
+
const timer = setTimeout(()=>{
|
|
3361
|
+
const at = this.waiters.indexOf(waiter);
|
|
3362
|
+
if (-1 !== at) this.waiters.splice(at, 1);
|
|
3363
|
+
reject(new Error(`RDP request ${String(packet.type)} to ${actor} timed out after ${timeoutMs}ms`));
|
|
3364
|
+
}, timeoutMs);
|
|
3365
|
+
this.waiters.push(waiter);
|
|
3366
|
+
this.socket.write(encodeRdpPacket({
|
|
3367
|
+
to: actor,
|
|
3368
|
+
...packet
|
|
3369
|
+
}));
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
tap(handler) {
|
|
3373
|
+
this.taps.add(handler);
|
|
3374
|
+
return ()=>this.taps.delete(handler);
|
|
3375
|
+
}
|
|
3376
|
+
close() {
|
|
3377
|
+
this.closed = true;
|
|
3378
|
+
this.socket.destroy();
|
|
3379
|
+
}
|
|
3380
|
+
constructor(socket){
|
|
3381
|
+
rdp_define_property(this, "socket", void 0);
|
|
3382
|
+
rdp_define_property(this, "waiters", void 0);
|
|
3383
|
+
rdp_define_property(this, "taps", void 0);
|
|
3384
|
+
rdp_define_property(this, "closed", void 0);
|
|
3385
|
+
this.socket = socket;
|
|
3386
|
+
this.waiters = [];
|
|
3387
|
+
this.taps = new Set();
|
|
3388
|
+
this.closed = false;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
async function withSession(port, timeoutMs, work) {
|
|
3392
|
+
const session = await RdpSession.connect(port, timeoutMs);
|
|
3393
|
+
try {
|
|
3394
|
+
return await work(session);
|
|
3395
|
+
} finally{
|
|
3396
|
+
session.close();
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
async function rdpListAddons(port, options) {
|
|
3400
|
+
const timeoutMs = options?.timeoutMs ?? 10000;
|
|
3401
|
+
return withSession(port, timeoutMs, async (session)=>{
|
|
3402
|
+
const reply = await session.request("root", {
|
|
3403
|
+
type: "listAddons"
|
|
3404
|
+
}, timeoutMs);
|
|
3405
|
+
return reply.addons ?? [];
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
async function rdpListTabs(port, options) {
|
|
3409
|
+
const timeoutMs = options?.timeoutMs ?? 10000;
|
|
3410
|
+
return withSession(port, timeoutMs, async (session)=>{
|
|
3411
|
+
const reply = await session.request("root", {
|
|
3412
|
+
type: "listTabs"
|
|
3413
|
+
}, timeoutMs);
|
|
3414
|
+
return reply.tabs ?? [];
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
function formatConsoleArg(arg) {
|
|
3418
|
+
if (null == arg) return String(arg);
|
|
3419
|
+
if ("object" == typeof arg) {
|
|
3420
|
+
const cls = arg.class;
|
|
3421
|
+
return "string" == typeof cls ? `[${cls}]` : "[object]";
|
|
3422
|
+
}
|
|
3423
|
+
return String(arg);
|
|
3424
|
+
}
|
|
3425
|
+
async function rdpCollectConsoleMessages(port, options) {
|
|
3426
|
+
const timeoutMs = options?.timeoutMs ?? 10000;
|
|
3427
|
+
const settleMs = options?.settleMs ?? 1000;
|
|
3428
|
+
return withSession(port, timeoutMs, async (session)=>{
|
|
3429
|
+
const tabsReply = await session.request("root", {
|
|
3430
|
+
type: "listTabs"
|
|
3431
|
+
}, timeoutMs);
|
|
3432
|
+
const tabs = tabsReply.tabs ?? [];
|
|
3433
|
+
const wanted = options?.urlFilter?.toLowerCase();
|
|
3434
|
+
const tab = wanted ? tabs.find((t)=>String(t.url ?? "").toLowerCase().includes(wanted)) : tabs.find((t)=>true === t.selected) ?? tabs[0];
|
|
3435
|
+
if (!tab?.actor) throw new Error(wanted ? `no open tab matches url: ${options?.urlFilter}` : "no open tabs");
|
|
3436
|
+
const watcherReply = await session.request(String(tab.actor), {
|
|
3437
|
+
type: "getWatcher",
|
|
3438
|
+
isServerTargetSwitchingEnabled: true
|
|
3439
|
+
}, timeoutMs);
|
|
3440
|
+
const watcherActor = String(watcherReply.actor ?? "");
|
|
3441
|
+
if (!watcherActor) throw new Error("tab descriptor returned no watcher");
|
|
3442
|
+
const messages = [];
|
|
3443
|
+
const untap = session.tap((packet)=>{
|
|
3444
|
+
if ("resources-available-array" !== packet.type) return;
|
|
3445
|
+
const array = Array.isArray(packet.array) ? packet.array : [];
|
|
3446
|
+
for (const entry of array){
|
|
3447
|
+
if (!Array.isArray(entry) || entry.length < 2) continue;
|
|
3448
|
+
const [resourceType, resources] = entry;
|
|
3449
|
+
if (Array.isArray(resources)) for (const raw of resources){
|
|
3450
|
+
const resource = raw;
|
|
3451
|
+
if ("console-message" === resourceType) {
|
|
3452
|
+
const args = Array.isArray(resource.arguments) ? resource.arguments : [];
|
|
3453
|
+
messages.push({
|
|
3454
|
+
level: String(resource.level ?? "log"),
|
|
3455
|
+
text: args.map(formatConsoleArg).join(" ")
|
|
3456
|
+
});
|
|
3457
|
+
} else if ("error-message" === resourceType) {
|
|
3458
|
+
const pageError = resource.pageError ?? {};
|
|
3459
|
+
messages.push({
|
|
3460
|
+
level: true === pageError.warning ? "warn" : "error",
|
|
3461
|
+
text: String(pageError.errorMessage ?? "")
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
});
|
|
3467
|
+
try {
|
|
3468
|
+
await session.request(watcherActor, {
|
|
3469
|
+
type: "watchTargets",
|
|
3470
|
+
targetType: "frame"
|
|
3471
|
+
}, timeoutMs);
|
|
3472
|
+
await session.request(watcherActor, {
|
|
3473
|
+
type: "watchResources",
|
|
3474
|
+
resourceTypes: [
|
|
3475
|
+
"console-message",
|
|
3476
|
+
"error-message"
|
|
3477
|
+
]
|
|
3478
|
+
}, timeoutMs);
|
|
3479
|
+
await new Promise((resolve)=>setTimeout(resolve, settleMs));
|
|
3480
|
+
} finally{
|
|
3481
|
+
untap();
|
|
3482
|
+
}
|
|
3483
|
+
return messages;
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
function buildBridgeInspectExpression(opts) {
|
|
3487
|
+
const parts = [
|
|
3488
|
+
"const out = {};"
|
|
3489
|
+
];
|
|
3490
|
+
if (opts.meta) parts.push("try { out.meta = { url: location.href, title: document.title, readyState: document.readyState }; } catch (e) {}");
|
|
3491
|
+
if (opts.summary) parts.push(`try {
|
|
3492
|
+
const roots = document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])');
|
|
3493
|
+
out.summary = {
|
|
3494
|
+
htmlLength: document.documentElement.outerHTML.length,
|
|
3495
|
+
scriptCount: document.querySelectorAll('script').length,
|
|
3496
|
+
styleCount: document.querySelectorAll('style').length,
|
|
3497
|
+
linkCount: document.querySelectorAll('link').length,
|
|
3498
|
+
extensionRootCount: roots.length,
|
|
3499
|
+
bodyChildCount: document.body ? document.body.children.length : 0
|
|
3500
|
+
};
|
|
3501
|
+
} catch (e) { out.summary = {}; }`);
|
|
3502
|
+
if (opts.html) parts.push(`try {
|
|
3503
|
+
const html = ${PAGE_HTML_SCRIPT};
|
|
3504
|
+
const cap = ${JSON.stringify(opts.maxBytes)};
|
|
3505
|
+
out.htmlTruncated = cap > 0 && html.length > cap;
|
|
3506
|
+
out.html = out.htmlTruncated ? html.slice(0, cap) : html;
|
|
3507
|
+
} catch (e) {}`);
|
|
3508
|
+
if (opts.domSnapshot) parts.push(`try { out.domSnapshot = ${domSnapshotScript(500)}; } catch (e) {}`);
|
|
3509
|
+
if (opts.extensionRoots) parts.push(`try { out.extensionRoots = ${EXTENSION_ROOT_META_SCRIPT}; } catch (e) {}`);
|
|
3510
|
+
if (opts.probes.length) parts.push(`out.probes = {};
|
|
3511
|
+
for (const sel of ${JSON.stringify(opts.probes)}) {
|
|
3512
|
+
try {
|
|
3513
|
+
const nodes = document.querySelectorAll(sel);
|
|
3514
|
+
const first = nodes[0];
|
|
3515
|
+
out.probes[sel] = { count: nodes.length, sample: first ? String(first.outerHTML || "").slice(0, 200) : null };
|
|
3516
|
+
} catch (e) { out.probes[sel] = { error: String((e && e.message) || e) }; }
|
|
3517
|
+
}`);
|
|
3518
|
+
parts.push("return out;");
|
|
3519
|
+
return `(() => { ${parts.join("\n")} })()`;
|
|
3520
|
+
}
|
|
3521
|
+
function closedShadowWalkerCode(cap) {
|
|
3522
|
+
return `
|
|
3523
|
+
(function() {
|
|
3524
|
+
var out = { api: ("openOrClosedShadowRoot" in Element.prototype), closed: [] };
|
|
3525
|
+
function walk(node) {
|
|
3526
|
+
if (!node || node.nodeType !== 1) return;
|
|
3527
|
+
var sr = null;
|
|
3528
|
+
try { sr = node.openOrClosedShadowRoot || null; } catch (e) {}
|
|
3529
|
+
if (sr && sr.mode !== "open") out.closed.push({ host: node.tagName.toLowerCase(), html: String(sr.innerHTML).slice(0, ${cap}) });
|
|
3530
|
+
var kids = node.children;
|
|
3531
|
+
for (var i = 0; i < kids.length; i++) walk(kids[i]);
|
|
3532
|
+
if (sr) { var sk = sr.children; for (var j = 0; j < sk.length; j++) walk(sk[j]); }
|
|
3533
|
+
}
|
|
3534
|
+
walk(document.documentElement);
|
|
3535
|
+
return out;
|
|
3536
|
+
})();
|
|
3537
|
+
`;
|
|
3538
|
+
}
|
|
3539
|
+
function executeScriptExpression(urlFilter, code) {
|
|
3540
|
+
const pick = urlFilter ? `tabs.find(function (t) { return String(t.url || "").toLowerCase().indexOf(${JSON.stringify(urlFilter.toLowerCase())}) !== -1; })` : "(tabs.find(function (t) { return t.active; }) || tabs[0])";
|
|
3541
|
+
return `browser.tabs.query({}).then(function (tabs) {
|
|
3542
|
+
var tab = ${pick};
|
|
3543
|
+
if (!tab) return { error: "no matching tab" };
|
|
3544
|
+
return browser.tabs.executeScript(tab.id, { code: ${JSON.stringify(code)} }).then(
|
|
3545
|
+
function (results) { return { frames: results }; },
|
|
3546
|
+
function (err) { return { error: String((err && err.message) || err) }; }
|
|
3547
|
+
);
|
|
3548
|
+
})`;
|
|
3549
|
+
}
|
|
3550
|
+
async function collectGeckoDeepDom(args, browser, urlFilter, cap, result, notes) {
|
|
3551
|
+
const raw = await runActVerb([
|
|
3552
|
+
"eval",
|
|
3553
|
+
executeScriptExpression(urlFilter, closedShadowWalkerCode(cap)),
|
|
3554
|
+
args.projectPath,
|
|
3555
|
+
"--context",
|
|
3556
|
+
"background",
|
|
3557
|
+
"--browser",
|
|
3558
|
+
browser,
|
|
3559
|
+
...null != args.timeout ? [
|
|
3560
|
+
"--timeout",
|
|
3561
|
+
String(args.timeout)
|
|
3562
|
+
] : []
|
|
3563
|
+
], args.projectPath, args.timeout);
|
|
3564
|
+
let parsed;
|
|
3565
|
+
try {
|
|
3566
|
+
parsed = JSON.parse(raw);
|
|
3567
|
+
} catch {
|
|
3568
|
+
parsed = null;
|
|
3569
|
+
}
|
|
3570
|
+
const value = parsed?.ok === true ? parsed.value : null;
|
|
3571
|
+
const frame = Array.isArray(value?.frames) ? value.frames[0] : null;
|
|
3572
|
+
if (frame && Array.isArray(frame.closed)) {
|
|
3573
|
+
result.closedShadowRoots = frame.closed.map((c)=>({
|
|
3574
|
+
host: String(c.host ?? ""),
|
|
3575
|
+
type: "closed",
|
|
3576
|
+
html: String(c.html ?? "")
|
|
3577
|
+
}));
|
|
3578
|
+
result.deepDom = true;
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
const reason = value?.error ?? parsed?.error?.message ?? "the content-script walk returned nothing";
|
|
3582
|
+
notes.push(`deepDom failed on ${browser}: ${reason}. The walk runs via tabs.executeScript (MV2) and needs the extension to hold host permissions for the target url.`);
|
|
3583
|
+
}
|
|
3584
|
+
async function collectGeckoConsole(args, browser, urlFilter, result, notes) {
|
|
3585
|
+
const fallbackNote = `Console capture on ${browser} rides the RDP watcher replay and needs a session whose ready contract publishes rdpPort (extension.js 4.0.15+); extension_logs streams the extension's own console either way.`;
|
|
3586
|
+
const resolved = await resolveRdpPort(args.projectPath, browser, {
|
|
3587
|
+
waitMs: 5000
|
|
3588
|
+
});
|
|
3589
|
+
if (!resolved) return void notes.push(fallbackNote);
|
|
3590
|
+
try {
|
|
3591
|
+
const messages = await rdpCollectConsoleMessages(resolved.port, {
|
|
3592
|
+
urlFilter
|
|
3593
|
+
});
|
|
3594
|
+
result.console = summarizeConsoleMessages(messages);
|
|
3595
|
+
result.rdpPort = resolved.port;
|
|
3596
|
+
} catch (error) {
|
|
3597
|
+
notes.push(`Console capture over RDP failed: ${error.message}. ${fallbackNote}`);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
3601
|
+
const notes = [];
|
|
3602
|
+
if (args.url) {
|
|
3603
|
+
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
3604
|
+
if ("error" in listed) return listed.error;
|
|
3605
|
+
const already = listed.tabs.some((t)=>t.url.includes(args.url));
|
|
3606
|
+
if (!already) {
|
|
3607
|
+
const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout);
|
|
3608
|
+
try {
|
|
3609
|
+
if (JSON.parse(nav)?.ok !== true) return nav;
|
|
3610
|
+
} catch {
|
|
3611
|
+
return nav;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
const expression = buildBridgeInspectExpression({
|
|
3616
|
+
summary: include.has("summary"),
|
|
3617
|
+
meta: true,
|
|
3618
|
+
html: include.has("html"),
|
|
3619
|
+
domSnapshot: include.has("dom_snapshot"),
|
|
3620
|
+
extensionRoots: include.has("extension_roots"),
|
|
3621
|
+
probes: args.probe ?? [],
|
|
3622
|
+
maxBytes
|
|
3623
|
+
});
|
|
3624
|
+
let raw = await runActVerb([
|
|
3625
|
+
"eval",
|
|
3626
|
+
expression,
|
|
3627
|
+
args.projectPath,
|
|
3628
|
+
"--context",
|
|
3629
|
+
"page",
|
|
3630
|
+
...args.url ? [
|
|
3631
|
+
"--url",
|
|
3632
|
+
args.url
|
|
3633
|
+
] : [],
|
|
3634
|
+
"--browser",
|
|
3635
|
+
browser,
|
|
3636
|
+
...null != args.timeout ? [
|
|
3637
|
+
"--timeout",
|
|
3638
|
+
String(args.timeout)
|
|
3639
|
+
] : []
|
|
3640
|
+
], args.projectPath, args.timeout);
|
|
3641
|
+
let parsed;
|
|
3642
|
+
try {
|
|
3643
|
+
parsed = JSON.parse(raw);
|
|
3644
|
+
} catch {
|
|
3645
|
+
return raw;
|
|
3646
|
+
}
|
|
3647
|
+
let value = parsed?.ok === true ? parsed.value ?? {} : null;
|
|
3648
|
+
if (null === value && /scripting is not available/i.test(String(parsed?.error?.message ?? ""))) {
|
|
3649
|
+
raw = await runActVerb([
|
|
3650
|
+
"eval",
|
|
3651
|
+
executeScriptExpression(args.url, expression),
|
|
3652
|
+
args.projectPath,
|
|
3653
|
+
"--context",
|
|
3654
|
+
"background",
|
|
3655
|
+
"--browser",
|
|
3656
|
+
browser,
|
|
3657
|
+
...null != args.timeout ? [
|
|
3658
|
+
"--timeout",
|
|
3659
|
+
String(args.timeout)
|
|
3660
|
+
] : []
|
|
3661
|
+
], args.projectPath, args.timeout);
|
|
3662
|
+
try {
|
|
3663
|
+
parsed = JSON.parse(raw);
|
|
3664
|
+
} catch {
|
|
3665
|
+
return raw;
|
|
3666
|
+
}
|
|
3667
|
+
const frame = Array.isArray(parsed?.value?.frames) ? parsed.value.frames[0] : null;
|
|
3668
|
+
if (parsed?.ok === true && frame && "object" == typeof frame) value = frame;
|
|
3669
|
+
else if (parsed?.ok === true) return JSON.stringify({
|
|
3670
|
+
ok: false,
|
|
3671
|
+
error: {
|
|
3672
|
+
name: "InspectFailed",
|
|
3673
|
+
message: String(parsed?.value?.error ?? "the content-script inspect returned nothing")
|
|
3674
|
+
},
|
|
3675
|
+
hint: "The MV2 fallback inspects via tabs.executeScript, which needs the extension to hold host permissions for the target url."
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
if (null === value) return raw;
|
|
3679
|
+
const result = {
|
|
3680
|
+
browser,
|
|
3681
|
+
transport: "bridge"
|
|
3682
|
+
};
|
|
3683
|
+
if (value.meta) {
|
|
3684
|
+
result.target = {
|
|
3685
|
+
url: value.meta.url,
|
|
3686
|
+
title: value.meta.title
|
|
3687
|
+
};
|
|
3688
|
+
if (include.has("meta")) result.meta = value.meta;
|
|
3689
|
+
}
|
|
3690
|
+
if (include.has("summary") && value.summary) result.summary = value.summary;
|
|
3691
|
+
if (include.has("html") && "string" == typeof value.html) {
|
|
3692
|
+
result.html = value.html;
|
|
3693
|
+
if (value.htmlTruncated) result.htmlTruncated = true;
|
|
3694
|
+
}
|
|
3695
|
+
if (include.has("dom_snapshot") && value.domSnapshot) result.domSnapshot = value.domSnapshot;
|
|
3696
|
+
if (include.has("extension_roots") && void 0 !== value.extensionRoots) result.extensionRoots = value.extensionRoots;
|
|
3697
|
+
if (value.probes) {
|
|
3698
|
+
result.probes = value.probes;
|
|
3699
|
+
const jsLooking = (args.probe ?? []).filter((p)=>/^typeof\s|^(chrome|browser|window|document)\.|\(\)|=>|===/.test(p));
|
|
3700
|
+
if (jsLooking.length) result.probeWarning = `Probes are CSS selectors run through querySelectorAll against the live page, NOT JavaScript expressions. ${jsLooking.map((s)=>`"${s}"`).join(", ")} parsed as selectors and will match nothing. To evaluate JS, use extension_eval.`;
|
|
3701
|
+
}
|
|
3702
|
+
const urlFilter = args.url ?? ("string" == typeof value.meta?.url ? value.meta.url : void 0);
|
|
3703
|
+
if (include.has("console")) await collectGeckoConsole(args, browser, urlFilter, result, notes);
|
|
3704
|
+
if (args.deepDom) {
|
|
3705
|
+
const cap = maxBytes > 0 ? maxBytes : 65536;
|
|
3706
|
+
await collectGeckoDeepDom(args, browser, urlFilter, cap, result, notes);
|
|
3707
|
+
}
|
|
3708
|
+
if (notes.length) result.notes = notes;
|
|
3709
|
+
return JSON.stringify(result);
|
|
3710
|
+
}
|
|
3184
3711
|
const source_inspect_schema = {
|
|
3185
3712
|
name: "extension_source_inspect",
|
|
3186
|
-
description: "Inspect a running extension's live state: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Chromium sessions ride Chrome DevTools Protocol
|
|
3713
|
+
description: "Inspect a running extension's live state: full HTML (with shadow DOM), DOM structure, content script injection, console messages, and CSS selector queries. Chromium sessions ride Chrome DevTools Protocol. Firefox sessions are fully paired: summary/meta/html/dom_snapshot/extension_roots/probes ride the agent bridge (needs allowEval: true), console rides the RDP watcher replay (engine 4.0.15+), and deepDom walks closed shadow roots via a content-script eval (MV2 sessions with host permissions for the target url; Firefox MV3 background CSP blocks bridge evals entirely). Requires an active dev or start session.",
|
|
3187
3714
|
inputSchema: {
|
|
3188
3715
|
type: "object",
|
|
3189
3716
|
properties: {
|
|
@@ -3234,7 +3761,7 @@ const source_inspect_schema = {
|
|
|
3234
3761
|
deepDom: {
|
|
3235
3762
|
type: "boolean",
|
|
3236
3763
|
default: false,
|
|
3237
|
-
description: "Pierce CLOSED shadow roots
|
|
3764
|
+
description: "Pierce CLOSED shadow roots. The default path reads open shadow roots; closed ones need this escape hatch. Chromium: CDP DOM pierce. Firefox: a content-script walk via tabs.executeScript (MV2 sessions; the extension needs host permissions for the target url)."
|
|
3238
3765
|
}
|
|
3239
3766
|
},
|
|
3240
3767
|
required: [
|
|
@@ -3352,120 +3879,9 @@ async function source_inspect_handler(args) {
|
|
|
3352
3879
|
cdp.disconnect();
|
|
3353
3880
|
}
|
|
3354
3881
|
}
|
|
3355
|
-
function buildBridgeInspectExpression(opts) {
|
|
3356
|
-
const parts = [
|
|
3357
|
-
"const out = {};"
|
|
3358
|
-
];
|
|
3359
|
-
if (opts.meta) parts.push("try { out.meta = { url: location.href, title: document.title, readyState: document.readyState }; } catch (e) {}");
|
|
3360
|
-
if (opts.summary) parts.push(`try {
|
|
3361
|
-
const roots = document.querySelectorAll('#extension-root,[data-extension-root]:not([data-extension-root="extension-js-devtools"])');
|
|
3362
|
-
out.summary = {
|
|
3363
|
-
htmlLength: document.documentElement.outerHTML.length,
|
|
3364
|
-
scriptCount: document.querySelectorAll('script').length,
|
|
3365
|
-
styleCount: document.querySelectorAll('style').length,
|
|
3366
|
-
linkCount: document.querySelectorAll('link').length,
|
|
3367
|
-
extensionRootCount: roots.length,
|
|
3368
|
-
bodyChildCount: document.body ? document.body.children.length : 0
|
|
3369
|
-
};
|
|
3370
|
-
} catch (e) { out.summary = {}; }`);
|
|
3371
|
-
if (opts.html) parts.push(`try {
|
|
3372
|
-
const html = document.documentElement.outerHTML;
|
|
3373
|
-
const cap = ${JSON.stringify(opts.maxBytes)};
|
|
3374
|
-
out.htmlTruncated = cap > 0 && html.length > cap;
|
|
3375
|
-
out.html = out.htmlTruncated ? html.slice(0, cap) : html;
|
|
3376
|
-
} catch (e) {}`);
|
|
3377
|
-
if (opts.probes.length) parts.push(`out.probes = {};
|
|
3378
|
-
for (const sel of ${JSON.stringify(opts.probes)}) {
|
|
3379
|
-
try {
|
|
3380
|
-
const nodes = document.querySelectorAll(sel);
|
|
3381
|
-
const first = nodes[0];
|
|
3382
|
-
out.probes[sel] = { count: nodes.length, sample: first ? String(first.outerHTML || "").slice(0, 200) : null };
|
|
3383
|
-
} catch (e) { out.probes[sel] = { error: String((e && e.message) || e) }; }
|
|
3384
|
-
}`);
|
|
3385
|
-
parts.push("return out;");
|
|
3386
|
-
return `(() => { ${parts.join("\n")} })()`;
|
|
3387
|
-
}
|
|
3388
|
-
async function inspectViaBridge(args, browser, include, maxBytes) {
|
|
3389
|
-
const notes = [];
|
|
3390
|
-
const cdpOnly = [
|
|
3391
|
-
"dom_snapshot",
|
|
3392
|
-
"extension_roots"
|
|
3393
|
-
].filter((k)=>include.has(k));
|
|
3394
|
-
if (cdpOnly.length) notes.push(`${cdpOnly.join(" and ")} require CDP and are Chromium-only; on ${browser}, extension_dom_inspect's summary reports extension roots and open shadow roots.`);
|
|
3395
|
-
if (args.deepDom) notes.push("deepDom (closed shadow roots) requires CDP and is Chromium-only.");
|
|
3396
|
-
if (include.has("console")) notes.push(`Console capture rides CDP; on ${browser} read extension_logs, where the engine streams console output.`);
|
|
3397
|
-
if (args.url) {
|
|
3398
|
-
const listed = await listBridgeTabs(args.projectPath, browser, args.timeout);
|
|
3399
|
-
if ("error" in listed) return listed.error;
|
|
3400
|
-
const already = listed.tabs.some((t)=>t.url.includes(args.url));
|
|
3401
|
-
if (!already) {
|
|
3402
|
-
const nav = await navigateToUrlViaBridge(args.projectPath, browser, args.url, args.timeout);
|
|
3403
|
-
try {
|
|
3404
|
-
if (JSON.parse(nav)?.ok !== true) return nav;
|
|
3405
|
-
} catch {
|
|
3406
|
-
return nav;
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
}
|
|
3410
|
-
const expression = buildBridgeInspectExpression({
|
|
3411
|
-
summary: include.has("summary"),
|
|
3412
|
-
meta: true,
|
|
3413
|
-
html: include.has("html"),
|
|
3414
|
-
probes: args.probe ?? [],
|
|
3415
|
-
maxBytes
|
|
3416
|
-
});
|
|
3417
|
-
const raw = await runActVerb([
|
|
3418
|
-
"eval",
|
|
3419
|
-
expression,
|
|
3420
|
-
args.projectPath,
|
|
3421
|
-
"--context",
|
|
3422
|
-
"page",
|
|
3423
|
-
...args.url ? [
|
|
3424
|
-
"--url",
|
|
3425
|
-
args.url
|
|
3426
|
-
] : [],
|
|
3427
|
-
"--browser",
|
|
3428
|
-
browser,
|
|
3429
|
-
...null != args.timeout ? [
|
|
3430
|
-
"--timeout",
|
|
3431
|
-
String(args.timeout)
|
|
3432
|
-
] : []
|
|
3433
|
-
], args.projectPath, args.timeout);
|
|
3434
|
-
let parsed;
|
|
3435
|
-
try {
|
|
3436
|
-
parsed = JSON.parse(raw);
|
|
3437
|
-
} catch {
|
|
3438
|
-
return raw;
|
|
3439
|
-
}
|
|
3440
|
-
if (parsed?.ok !== true) return raw;
|
|
3441
|
-
const value = parsed.value ?? {};
|
|
3442
|
-
const result = {
|
|
3443
|
-
browser,
|
|
3444
|
-
transport: "bridge"
|
|
3445
|
-
};
|
|
3446
|
-
if (value.meta) {
|
|
3447
|
-
result.target = {
|
|
3448
|
-
url: value.meta.url,
|
|
3449
|
-
title: value.meta.title
|
|
3450
|
-
};
|
|
3451
|
-
if (include.has("meta")) result.meta = value.meta;
|
|
3452
|
-
}
|
|
3453
|
-
if (include.has("summary") && value.summary) result.summary = value.summary;
|
|
3454
|
-
if (include.has("html") && "string" == typeof value.html) {
|
|
3455
|
-
result.html = value.html;
|
|
3456
|
-
if (value.htmlTruncated) result.htmlTruncated = true;
|
|
3457
|
-
}
|
|
3458
|
-
if (value.probes) {
|
|
3459
|
-
result.probes = value.probes;
|
|
3460
|
-
const jsLooking = (args.probe ?? []).filter((p)=>/^typeof\s|^(chrome|browser|window|document)\.|\(\)|=>|===/.test(p));
|
|
3461
|
-
if (jsLooking.length) result.probeWarning = `Probes are CSS selectors run through querySelectorAll against the live page, NOT JavaScript expressions. ${jsLooking.map((s)=>`"${s}"`).join(", ")} parsed as selectors and will match nothing. To evaluate JS, use extension_eval.`;
|
|
3462
|
-
}
|
|
3463
|
-
if (notes.length) result.notes = notes;
|
|
3464
|
-
return JSON.stringify(result);
|
|
3465
|
-
}
|
|
3466
3882
|
const list_extensions_schema = {
|
|
3467
3883
|
name: "extension_list_extensions",
|
|
3468
|
-
description: "List the extensions
|
|
3884
|
+
description: "List the extensions in the running dev browser. Returns each extension's id, name, version, and (on Chromium) live contexts. The entry for THIS dev session's extension (the project being served) is flagged ownExtension: true, with name and version resolved from the session's ready contract even when the browser exposes no identity. On Chromium this rides the Chrome DevTools Protocol: entries are extensions with at least one live context (a dormant MV3 service worker with no open page may be absent until it wakes), and other extensions resolve via the read-only Extensions domain when available. On Firefox this rides the Remote Debugging Protocol root actor (listAddons, engine 4.0.15+): entries are INSTALLED add-ons regardless of live contexts, with temporarilyInstalled marking temporary loads, and carry no contexts. Either way other extensions' contexts are never attached to or evaluated in. Requires an active dev or start session.",
|
|
3469
3885
|
inputSchema: {
|
|
3470
3886
|
type: "object",
|
|
3471
3887
|
properties: {
|
|
@@ -3526,9 +3942,10 @@ function readOwnIdentity(projectPath, browser) {
|
|
|
3526
3942
|
const UNRESOLVED_NOTE = "Identity unresolved: the browser's Extensions CDP domain returned nothing for this id, and other extensions' contexts are never attached to or evaluated in to read a manifest.";
|
|
3527
3943
|
async function list_extensions_handler(args) {
|
|
3528
3944
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser, "chrome");
|
|
3945
|
+
if (isGeckoFamily(browser)) return listGeckoExtensions(args.projectPath, browser);
|
|
3529
3946
|
if (!isChromiumFamily(browser)) return JSON.stringify({
|
|
3530
|
-
error: `Listing extensions for ${browser}
|
|
3531
|
-
hint:
|
|
3947
|
+
error: `Listing extensions for ${browser} is not supported: no debugging-protocol pairing exists for this browser family.`,
|
|
3948
|
+
hint: "Target a Chromium-family (CDP) or Firefox-family (RDP) dev session."
|
|
3532
3949
|
});
|
|
3533
3950
|
const resolved = await resolveCdpPort(args.projectPath, browser);
|
|
3534
3951
|
if (!resolved) return JSON.stringify({
|
|
@@ -3621,6 +4038,70 @@ async function list_extensions_handler(args) {
|
|
|
3621
4038
|
cdp.disconnect();
|
|
3622
4039
|
}
|
|
3623
4040
|
}
|
|
4041
|
+
async function listGeckoExtensions(projectPath, browser) {
|
|
4042
|
+
const resolved = await resolveRdpPort(projectPath, browser);
|
|
4043
|
+
if (!resolved) return JSON.stringify({
|
|
4044
|
+
error: "No active dev session with a Firefox debugger server (RDP) found.",
|
|
4045
|
+
hint: `Start a dev session first with extension_dev, then use extension_wait to confirm it is ready. ${RDP_PORT_MISSING_HINT}`
|
|
4046
|
+
});
|
|
4047
|
+
const rdpPort = resolved.port;
|
|
4048
|
+
try {
|
|
4049
|
+
let addons = null;
|
|
4050
|
+
let lastError = null;
|
|
4051
|
+
for(let attempt = 0; attempt < 3; attempt++)try {
|
|
4052
|
+
addons = await rdpListAddons(rdpPort);
|
|
4053
|
+
break;
|
|
4054
|
+
} catch (error) {
|
|
4055
|
+
lastError = error;
|
|
4056
|
+
if (attempt < 2) await new Promise((resolve)=>setTimeout(resolve, 800));
|
|
4057
|
+
}
|
|
4058
|
+
if (!addons) throw lastError;
|
|
4059
|
+
const own = readOwnIdentity(projectPath, browser);
|
|
4060
|
+
const extensions = addons.filter((addon)=>true === addon.isWebExtension && true !== addon.isSystem && true !== addon.hidden).map((addon)=>{
|
|
4061
|
+
const entry = {
|
|
4062
|
+
id: String(addon.id ?? addon.actor ?? ""),
|
|
4063
|
+
contexts: [],
|
|
4064
|
+
source: "rdp-root"
|
|
4065
|
+
};
|
|
4066
|
+
if ("string" == typeof addon.name) entry.name = addon.name;
|
|
4067
|
+
if ("string" == typeof addon.version) entry.version = addon.version;
|
|
4068
|
+
if (true === addon.temporarilyInstalled) entry.temporarilyInstalled = true;
|
|
4069
|
+
return entry;
|
|
4070
|
+
});
|
|
4071
|
+
if (own?.name) {
|
|
4072
|
+
const byName = extensions.filter((e)=>e.name === own.name);
|
|
4073
|
+
if (1 === byName.length) byName[0].ownExtension = true;
|
|
4074
|
+
}
|
|
4075
|
+
if (!extensions.some((e)=>e.ownExtension)) {
|
|
4076
|
+
const temporary = extensions.filter((e)=>e.temporarilyInstalled);
|
|
4077
|
+
if (1 === temporary.length) {
|
|
4078
|
+
temporary[0].ownExtension = true;
|
|
4079
|
+
if (void 0 === temporary[0].name && own?.name !== void 0) {
|
|
4080
|
+
temporary[0].name = own.name;
|
|
4081
|
+
if (void 0 !== own.version) temporary[0].version = own.version;
|
|
4082
|
+
temporary[0].source = "session-contract";
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
extensions.sort((a, b)=>{
|
|
4087
|
+
if ((a.ownExtension ?? false) !== (b.ownExtension ?? false)) return a.ownExtension ? -1 : 1;
|
|
4088
|
+
return (a.name ?? a.id).localeCompare(b.name ?? b.id);
|
|
4089
|
+
});
|
|
4090
|
+
const ownEntry = extensions.find((e)=>e.ownExtension);
|
|
4091
|
+
return JSON.stringify({
|
|
4092
|
+
rdpPort,
|
|
4093
|
+
browser,
|
|
4094
|
+
count: extensions.length,
|
|
4095
|
+
ownExtensionId: ownEntry?.id ?? null,
|
|
4096
|
+
extensions,
|
|
4097
|
+
note: "Lists INSTALLED add-ons via the RDP root actor (listAddons), regardless of whether a context is currently live, so entries carry no contexts. temporarilyInstalled marks temporary loads; ownExtension marks the extension this dev session serves, matched from the session's ready contract. Add-ons are never attached to or evaluated in."
|
|
4098
|
+
});
|
|
4099
|
+
} catch (error) {
|
|
4100
|
+
return JSON.stringify({
|
|
4101
|
+
error: `Failed to list extensions over RDP: ${error.message}`
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
3624
4105
|
const CONTROL_WS_PATH = "/extjs-control";
|
|
3625
4106
|
const LEVEL_ORDER = [
|
|
3626
4107
|
"error",
|
|
@@ -4637,9 +5118,10 @@ function matchTargetsByUrl(targets, needle) {
|
|
|
4637
5118
|
if (byUrl.length > 0) return byUrl;
|
|
4638
5119
|
return targets.filter((t)=>t.title.toLowerCase().includes(wanted));
|
|
4639
5120
|
}
|
|
5121
|
+
const RDP_ACTOR_NOTE = "actor is an RDP tab descriptor actor id, NOT a chrome.tabs id: do not pass it as `tab`. Target a tab with `tabUrl` (URL substring) or `url`; if you need a numeric tab id, call extension_dom_inspect with listTabs: true.";
|
|
4640
5122
|
const dom_inspect_schema = {
|
|
4641
5123
|
name: "extension_dom_inspect",
|
|
4642
|
-
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Target a tab by `tabUrl` (case-insensitive URL substring resolved against the browser's live page targets; zero or several matches return the candidates instead of guessing), by `url`, or by numeric `tab`. Discover what is open with listTargets: true (CDP targetIds) or listTabs: true (numeric chrome.tabs ids). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
5124
|
+
description: "Inspect a page/content-script DOM via the agent bridge (CDP-free, localhost). Returns a structured snapshot (counts, extension roots, open shadow roots, optional capped HTML). Target a tab by `tabUrl` (case-insensitive URL substring resolved against the browser's live page targets; zero or several matches return the candidates instead of guessing), by `url`, or by numeric `tab`. Discover what is open with listTargets: true (Chromium CDP targetIds; Firefox RDP tab actors) or listTabs: true (numeric chrome.tabs ids). Requires the dev session to be started with allowControl: true (extension_dev). For closed shadow roots or deep CDP inspection use extension_source_inspect. Wraps `extension inspect`.",
|
|
4643
5125
|
inputSchema: {
|
|
4644
5126
|
type: "object",
|
|
4645
5127
|
properties: {
|
|
@@ -4662,7 +5144,7 @@ const dom_inspect_schema = {
|
|
|
4662
5144
|
listTargets: {
|
|
4663
5145
|
type: "boolean",
|
|
4664
5146
|
default: false,
|
|
4665
|
-
description: "Enumerate the browser's
|
|
5147
|
+
description: "Enumerate the browser's live page targets and return, ignoring the other args. The discovery path for `tabUrl`. Chromium: CDP page targets as {targetId,url,title,type}. Firefox: RDP tab descriptors as {actor,url,title,type} (engine 4.0.15+). Neither id is a numeric chrome.tabs id (for those use listTabs)."
|
|
4666
5148
|
},
|
|
4667
5149
|
listTabs: {
|
|
4668
5150
|
type: "boolean",
|
|
@@ -4752,6 +5234,43 @@ async function dom_inspect_handler(args) {
|
|
|
4752
5234
|
const withConsole = true === args.withConsole ? 50 : false === args.withConsole ? void 0 : args.withConsole;
|
|
4753
5235
|
if (args.listTargets) {
|
|
4754
5236
|
const { browser } = resolveSessionBrowser(args.projectPath, args.browser);
|
|
5237
|
+
if (isGeckoFamily(browser)) {
|
|
5238
|
+
const resolved = await resolveRdpPort(args.projectPath, browser);
|
|
5239
|
+
if (!resolved) return JSON.stringify({
|
|
5240
|
+
ok: false,
|
|
5241
|
+
error: {
|
|
5242
|
+
name: "NoSession",
|
|
5243
|
+
message: `No active dev session with a Firefox debugger server (RDP) for ${browser}, so listTargets has no browser to ask. Start extension_dev and extension_wait for ready. ${RDP_PORT_MISSING_HINT}`
|
|
5244
|
+
}
|
|
5245
|
+
});
|
|
5246
|
+
try {
|
|
5247
|
+
const tabs = await rdpListTabs(resolved.port);
|
|
5248
|
+
return JSON.stringify({
|
|
5249
|
+
ok: true,
|
|
5250
|
+
browser,
|
|
5251
|
+
transport: "rdp",
|
|
5252
|
+
targets: tabs.map((t)=>({
|
|
5253
|
+
actor: String(t.actor ?? ""),
|
|
5254
|
+
type: "tab",
|
|
5255
|
+
url: String(t.url ?? ""),
|
|
5256
|
+
title: String(t.title ?? ""),
|
|
5257
|
+
...true === t.selected ? {
|
|
5258
|
+
selected: true
|
|
5259
|
+
} : {}
|
|
5260
|
+
})),
|
|
5261
|
+
note: RDP_ACTOR_NOTE
|
|
5262
|
+
});
|
|
5263
|
+
} catch (e) {
|
|
5264
|
+
return JSON.stringify({
|
|
5265
|
+
ok: false,
|
|
5266
|
+
error: {
|
|
5267
|
+
name: "RdpError",
|
|
5268
|
+
message: `Could not list tab targets over RDP: ${e instanceof Error ? e.message : String(e)}`
|
|
5269
|
+
},
|
|
5270
|
+
hint: "Confirm the session is ready (extension_wait), then retry. listTabs: true is the bridge alternative (needs allowControl)."
|
|
5271
|
+
});
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
4755
5274
|
const cdp = await cdpPortOrError(args.projectPath, browser, "listTargets");
|
|
4756
5275
|
if ("error" in cdp) return cdp.error;
|
|
4757
5276
|
try {
|
|
@@ -5548,8 +6067,7 @@ const deploy_schema = {
|
|
|
5548
6067
|
enum: [
|
|
5549
6068
|
"chrome",
|
|
5550
6069
|
"firefox",
|
|
5551
|
-
"edge"
|
|
5552
|
-
"safari"
|
|
6070
|
+
"edge"
|
|
5553
6071
|
]
|
|
5554
6072
|
},
|
|
5555
6073
|
description: "Stores to submit to."
|
|
@@ -5718,12 +6236,11 @@ async function deploy_handler(args) {
|
|
|
5718
6236
|
const KNOWN_STORES = [
|
|
5719
6237
|
"chrome",
|
|
5720
6238
|
"firefox",
|
|
5721
|
-
"edge"
|
|
5722
|
-
"safari"
|
|
6239
|
+
"edge"
|
|
5723
6240
|
];
|
|
5724
6241
|
const store_status_schema = {
|
|
5725
6242
|
name: "extension_store_status",
|
|
5726
|
-
description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge
|
|
6243
|
+
description: "Report the project's browser-store state after an extension_deploy submission: per store (chrome, firefox, edge) whether it is configured, its latest credential health check, the last recorded submission (version, status, store URL, submitted-at), and the latest review status. Reads the project's public registry (registry.extension.land: stores/health.json, stores/status.json, stores/submissions.json) - read-only, dispatches nothing, no auth needed for public projects. Defaults to the logged-in project (extension_login); pass workspace + project to inspect another public project. Registry state can lag the store dashboards by up to a polling interval.",
|
|
5727
6244
|
inputSchema: {
|
|
5728
6245
|
type: "object",
|
|
5729
6246
|
properties: {
|
|
@@ -5,4 +5,12 @@ export declare function resolveCdpPort(projectPath: string, browser: string, opt
|
|
|
5
5
|
port: number;
|
|
6
6
|
source: "contract" | "default-probe";
|
|
7
7
|
} | null>;
|
|
8
|
+
export declare function resolveRdpPort(projectPath: string, browser: string, options?: {
|
|
9
|
+
waitMs?: number;
|
|
10
|
+
graceMs?: number;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
port: number;
|
|
13
|
+
source: "contract";
|
|
14
|
+
} | null>;
|
|
8
15
|
export declare const CDP_PORT_MISSING_HINT = "The session's ready contract has no CDP port (the browser may still be binding its debug port, or was launched without one). Confirm the session with extension_wait, give it a moment, and retry.";
|
|
16
|
+
export declare const RDP_PORT_MISSING_HINT = "The session's ready contract has no rdpPort. Firefox sessions publish one from extension.js 4.0.15 on; upgrade the project's extension dependency (or remove the local install so the MCP's pinned CLI drives the session) and restart the dev session.";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ConsoleMessage } from "./console-summary";
|
|
2
|
+
export interface RdpAddon {
|
|
3
|
+
id?: string;
|
|
4
|
+
actor?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
temporarilyInstalled?: boolean;
|
|
9
|
+
isWebExtension?: boolean;
|
|
10
|
+
isSystem?: boolean;
|
|
11
|
+
hidden?: boolean;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
}
|
|
14
|
+
export interface RdpTab {
|
|
15
|
+
actor?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
selected?: boolean;
|
|
19
|
+
browserId?: number;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
export declare function encodeRdpPacket(packet: Record<string, unknown>): Buffer;
|
|
23
|
+
export declare class RdpPacketDecoder {
|
|
24
|
+
private buffer;
|
|
25
|
+
push(chunk: Buffer): Array<Record<string, unknown>>;
|
|
26
|
+
}
|
|
27
|
+
type RdpPacket = Record<string, unknown>;
|
|
28
|
+
export declare class RdpSession {
|
|
29
|
+
private socket;
|
|
30
|
+
private waiters;
|
|
31
|
+
private taps;
|
|
32
|
+
private closed;
|
|
33
|
+
private constructor();
|
|
34
|
+
static connect(port: number, timeoutMs?: number): Promise<RdpSession>;
|
|
35
|
+
request(actor: string, packet: RdpPacket, timeoutMs?: number): Promise<RdpPacket>;
|
|
36
|
+
tap(handler: (p: RdpPacket) => void): () => void;
|
|
37
|
+
close(): void;
|
|
38
|
+
}
|
|
39
|
+
export declare function rdpListAddons(port: number, options?: {
|
|
40
|
+
timeoutMs?: number;
|
|
41
|
+
}): Promise<RdpAddon[]>;
|
|
42
|
+
export declare function rdpListTabs(port: number, options?: {
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
}): Promise<RdpTab[]>;
|
|
45
|
+
export declare function rdpCollectConsoleMessages(port: number, options?: {
|
|
46
|
+
urlFilter?: string;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
settleMs?: number;
|
|
49
|
+
}): Promise<ConsoleMessage[]>;
|
|
50
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@extension.dev/mcp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.6.1",
|
|
5
5
|
"description": "MCP server that lets AI agents (Claude Code, Claude Desktop, Cursor, Copilot, Codex) build, run, inspect, and publish browser extensions. 33 tools for scaffolding, live DOM inspection, log streaming, and store-ready builds across Chrome, Edge, Firefox, and every Chromium- or Gecko-based browser (Brave, Opera, Vivaldi, Yandex, Waterfox, LibreWolf), with Safari coming next. Powered by extension.dev and Extension.js.",
|
|
6
6
|
"mcpName": "io.github.extensiondev/mcp",
|
|
7
7
|
"license": "MIT",
|
|
@@ -88,9 +88,9 @@
|
|
|
88
88
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
89
89
|
"browser-extension-manifest-fields": "^2.2.9",
|
|
90
90
|
"cross-spawn": "^7.0.6",
|
|
91
|
-
"extension-create": "^4.0.
|
|
92
|
-
"extension-develop": "^4.0.
|
|
93
|
-
"extension-install": "^4.0.
|
|
91
|
+
"extension-create": "^4.0.15",
|
|
92
|
+
"extension-develop": "^4.0.15",
|
|
93
|
+
"extension-install": "^4.0.15",
|
|
94
94
|
"ws": "^8.20.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://extension.dev",
|
|
10
|
-
"version": "5.
|
|
10
|
+
"version": "5.6.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@extension.dev/mcp",
|
|
16
|
-
"version": "5.
|
|
16
|
+
"version": "5.6.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"transport": {
|
|
19
19
|
"type": "stdio"
|