@mozilla/firefox-devtools-mcp 0.9.15 → 0.10.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.
- package/README.md +75 -37
- package/dist/index.js +698 -386
- package/dist/snapshot.injected.global.js +1 -1
- package/package.json +5 -4
- package/scripts/build-mcpb.mjs +1 -1
package/dist/index.js
CHANGED
|
@@ -51,6 +51,13 @@ function previewExcerpt(text, preview) {
|
|
|
51
51
|
}
|
|
52
52
|
return truncateText(text, Math.min(preview, TOKEN_LIMITS.MAX_RESPONSE_CHARS), "...");
|
|
53
53
|
}
|
|
54
|
+
function truncationFooter(hiddenCount, unit, hints) {
|
|
55
|
+
if (hiddenCount <= 0) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
const head = `[+${hiddenCount} ${unit} hidden`;
|
|
59
|
+
return hints.length > 0 ? `${head}; ${hints.join(", ")}]` : `${head}]`;
|
|
60
|
+
}
|
|
54
61
|
function truncateHeaders(headers) {
|
|
55
62
|
if (!headers) {
|
|
56
63
|
return null;
|
|
@@ -124,21 +131,6 @@ var init_response_helpers = __esm({
|
|
|
124
131
|
}
|
|
125
132
|
});
|
|
126
133
|
|
|
127
|
-
// src/tools/module.ts
|
|
128
|
-
function defineModule(config2) {
|
|
129
|
-
return {
|
|
130
|
-
name: config2.name,
|
|
131
|
-
description: config2.description,
|
|
132
|
-
...config2.privileged ? { privileged: true } : {},
|
|
133
|
-
tools: config2.tools.map(([definition, handler]) => ({ definition, handler }))
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
var init_module = __esm({
|
|
137
|
-
"src/tools/module.ts"() {
|
|
138
|
-
"use strict";
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
134
|
// node_modules/zod/v4/core/core.js
|
|
143
135
|
// @__NO_SIDE_EFFECTS__
|
|
144
136
|
function $constructor(name, initializer3, params) {
|
|
@@ -14849,7 +14841,7 @@ var init_constants = __esm({
|
|
|
14849
14841
|
"src/config/constants.ts"() {
|
|
14850
14842
|
"use strict";
|
|
14851
14843
|
SERVER_NAME = true ? "@mozilla/firefox-devtools-mcp" : "firefox-devtools";
|
|
14852
|
-
SERVER_VERSION = true ? "0.
|
|
14844
|
+
SERVER_VERSION = true ? "0.10.0" : "dev";
|
|
14853
14845
|
}
|
|
14854
14846
|
});
|
|
14855
14847
|
|
|
@@ -14970,7 +14962,16 @@ var init_profile = __esm({
|
|
|
14970
14962
|
// src/firefox/core.ts
|
|
14971
14963
|
import { Builder, Browser, Capabilities } from "selenium-webdriver";
|
|
14972
14964
|
import firefox from "selenium-webdriver/firefox.js";
|
|
14973
|
-
import {
|
|
14965
|
+
import {
|
|
14966
|
+
mkdirSync as mkdirSync2,
|
|
14967
|
+
openSync,
|
|
14968
|
+
closeSync,
|
|
14969
|
+
existsSync as existsSync2,
|
|
14970
|
+
readdirSync,
|
|
14971
|
+
statSync,
|
|
14972
|
+
readFileSync
|
|
14973
|
+
} from "fs";
|
|
14974
|
+
import { connect as netConnect } from "net";
|
|
14974
14975
|
import { homedir } from "os";
|
|
14975
14976
|
import { join as join2, delimiter } from "path";
|
|
14976
14977
|
function findGeckodriverInPath(binaryName) {
|
|
@@ -15016,6 +15017,57 @@ async function findGeckodriverInNpmPackage() {
|
|
|
15016
15017
|
return null;
|
|
15017
15018
|
}
|
|
15018
15019
|
}
|
|
15020
|
+
function lookupMarionettePort() {
|
|
15021
|
+
const instancesDir = join2(homedir(), ".firefox-devtools-mcp", "instances");
|
|
15022
|
+
if (!existsSync2(instancesDir)) {
|
|
15023
|
+
logDebug(`Failed to lookup Marionette port: ${instancesDir} doesn't exist.`);
|
|
15024
|
+
return;
|
|
15025
|
+
}
|
|
15026
|
+
const files = readdirSync(instancesDir);
|
|
15027
|
+
const portFiles = files.filter((f) => /^\d+\.port$/.test(f));
|
|
15028
|
+
const mostRecent = portFiles.map((f) => ({
|
|
15029
|
+
name: f,
|
|
15030
|
+
path: join2(instancesDir, f),
|
|
15031
|
+
mtime: statSync(join2(instancesDir, f)).mtimeMs
|
|
15032
|
+
})).sort((a, b) => b.mtime - a.mtime)[0];
|
|
15033
|
+
if (mostRecent) {
|
|
15034
|
+
const pid = Number(mostRecent.name.substring(0, mostRecent.name.length - 5));
|
|
15035
|
+
if (!checkProcess(pid)) {
|
|
15036
|
+
logDebug(`Failed to lookup Marionette port: No process with PID ${pid} is running.`);
|
|
15037
|
+
return;
|
|
15038
|
+
}
|
|
15039
|
+
logDebug(`Reading Marionette port from ${mostRecent.path}`);
|
|
15040
|
+
const content = readFileSync(mostRecent.path, "utf-8").trim();
|
|
15041
|
+
if (/^\d+$/.test(content)) {
|
|
15042
|
+
return Number(content);
|
|
15043
|
+
} else {
|
|
15044
|
+
logDebug(`Failed to lookup Marionette port: "${content}" is not a number.`);
|
|
15045
|
+
}
|
|
15046
|
+
} else {
|
|
15047
|
+
logDebug(`Failed to lookup Marionette port: No port file found in ${instancesDir}.`);
|
|
15048
|
+
}
|
|
15049
|
+
}
|
|
15050
|
+
function checkProcess(pid) {
|
|
15051
|
+
try {
|
|
15052
|
+
process.kill(pid, 0);
|
|
15053
|
+
return true;
|
|
15054
|
+
} catch {
|
|
15055
|
+
return false;
|
|
15056
|
+
}
|
|
15057
|
+
}
|
|
15058
|
+
function checkPort(port, timeoutMs = 1e3) {
|
|
15059
|
+
return new Promise((resolve4) => {
|
|
15060
|
+
const socket = netConnect({ host: "127.0.0.1", port });
|
|
15061
|
+
const done = (reason) => {
|
|
15062
|
+
socket.destroy();
|
|
15063
|
+
resolve4(reason);
|
|
15064
|
+
};
|
|
15065
|
+
socket.setTimeout(timeoutMs);
|
|
15066
|
+
socket.once("connect", () => done(null));
|
|
15067
|
+
socket.once("timeout", () => done(`connection to 127.0.0.1:${port} timed out`));
|
|
15068
|
+
socket.once("error", (error2) => done(error2.message));
|
|
15069
|
+
});
|
|
15070
|
+
}
|
|
15019
15071
|
async function findGeckodriver() {
|
|
15020
15072
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
15021
15073
|
const binaryName = `geckodriver${ext}`;
|
|
@@ -15047,8 +15099,15 @@ var init_core3 = __esm({
|
|
|
15047
15099
|
*/
|
|
15048
15100
|
async connect() {
|
|
15049
15101
|
const isAndroid = this.options.androidDevice !== void 0;
|
|
15102
|
+
const androidPackage = this.options.androidPackage ?? "org.mozilla.firefox";
|
|
15103
|
+
if (isAndroid && !this.options.androidWipeAppData) {
|
|
15104
|
+
throw new Error(
|
|
15105
|
+
`Firefox for Android mode wipes all data of ${androidPackage} on the device: tabs, history, bookmarks, passwords, cookies and settings are all lost, because geckodriver clears the app data before every session and cannot be configured to skip it. Pass --android-wipe-app-data (or ANDROID_WIPE_APP_DATA=true) to confirm. Prefer a build dedicated to automation, for instance --android-package org.mozilla.fenix for Nightly.`
|
|
15106
|
+
);
|
|
15107
|
+
}
|
|
15050
15108
|
if (isAndroid) {
|
|
15051
15109
|
log("Launching Firefox for Android via ADB...");
|
|
15110
|
+
log(`Wiping all data of ${androidPackage} on the device`);
|
|
15052
15111
|
} else if (this.options.connectExisting) {
|
|
15053
15112
|
log("Connecting to existing Firefox via Marionette...");
|
|
15054
15113
|
} else {
|
|
@@ -15057,8 +15116,7 @@ var init_core3 = __esm({
|
|
|
15057
15116
|
if (isAndroid) {
|
|
15058
15117
|
const geckodriverPath = await findGeckodriver();
|
|
15059
15118
|
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
15060
|
-
const
|
|
15061
|
-
const mozOptions = { androidPackage: pkg };
|
|
15119
|
+
const mozOptions = { androidPackage };
|
|
15062
15120
|
const deviceSerial = this.options.androidDevice;
|
|
15063
15121
|
if (deviceSerial && deviceSerial !== "auto") {
|
|
15064
15122
|
mozOptions.androidDeviceSerial = deviceSerial;
|
|
@@ -15075,7 +15133,31 @@ var init_core3 = __esm({
|
|
|
15075
15133
|
const serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
|
|
15076
15134
|
this.driver = firefox.Driver.createSession(caps, serviceBuilder.build());
|
|
15077
15135
|
} else if (this.options.connectExisting) {
|
|
15078
|
-
|
|
15136
|
+
let port = this.options.marionettePort ?? 2828;
|
|
15137
|
+
if (this.options.lookupMarionettePort) {
|
|
15138
|
+
logDebug("Looking up Marionette port");
|
|
15139
|
+
const lookedUpPort = lookupMarionettePort();
|
|
15140
|
+
if (lookedUpPort !== void 0) {
|
|
15141
|
+
port = lookedUpPort;
|
|
15142
|
+
} else {
|
|
15143
|
+
throw new Error(
|
|
15144
|
+
"Marionette port not found: please enable Firefox remote control for AI tooling using the AI assistant companion button."
|
|
15145
|
+
);
|
|
15146
|
+
}
|
|
15147
|
+
}
|
|
15148
|
+
logDebug(`Using Marionette port ${port}`);
|
|
15149
|
+
const failure = await checkPort(port);
|
|
15150
|
+
if (failure) {
|
|
15151
|
+
if (this.options.lookupMarionettePort) {
|
|
15152
|
+
throw new Error(
|
|
15153
|
+
`No Marionette listener on 127.0.0.1:${port} (${failure}). Please enable Firefox remote control for AI tooling using the AI assistant companion button.`
|
|
15154
|
+
);
|
|
15155
|
+
} else {
|
|
15156
|
+
throw new Error(
|
|
15157
|
+
`No Marionette listener on 127.0.0.1:${port} (${failure}). Start Firefox with both flags: firefox --marionette --remote-debugging-port, or pass the port it is actually using via --marionette-port.`
|
|
15158
|
+
);
|
|
15159
|
+
}
|
|
15160
|
+
}
|
|
15079
15161
|
const geckodriverPath = await findGeckodriver();
|
|
15080
15162
|
logDebug(`Using geckodriver: ${geckodriverPath}`);
|
|
15081
15163
|
const serviceBuilder = new firefox.ServiceBuilder(geckodriverPath);
|
|
@@ -15397,6 +15479,67 @@ var init_core3 = __esm({
|
|
|
15397
15479
|
}
|
|
15398
15480
|
});
|
|
15399
15481
|
|
|
15482
|
+
// src/utils/remote-value.ts
|
|
15483
|
+
function remoteValueToNative(rv) {
|
|
15484
|
+
if (!rv || typeof rv !== "object") {
|
|
15485
|
+
return rv;
|
|
15486
|
+
}
|
|
15487
|
+
const { type, value } = rv;
|
|
15488
|
+
switch (type) {
|
|
15489
|
+
case "undefined":
|
|
15490
|
+
return void 0;
|
|
15491
|
+
case "null":
|
|
15492
|
+
return null;
|
|
15493
|
+
case "string":
|
|
15494
|
+
case "boolean":
|
|
15495
|
+
return value;
|
|
15496
|
+
case "number":
|
|
15497
|
+
if (value === "NaN") {
|
|
15498
|
+
return "NaN";
|
|
15499
|
+
}
|
|
15500
|
+
if (value === "Infinity") {
|
|
15501
|
+
return "Infinity";
|
|
15502
|
+
}
|
|
15503
|
+
if (value === "-Infinity") {
|
|
15504
|
+
return "-Infinity";
|
|
15505
|
+
}
|
|
15506
|
+
if (value === "-0") {
|
|
15507
|
+
return "-0";
|
|
15508
|
+
}
|
|
15509
|
+
return value;
|
|
15510
|
+
case "bigint":
|
|
15511
|
+
return `${value}n`;
|
|
15512
|
+
case "array":
|
|
15513
|
+
return value.map(remoteValueToNative);
|
|
15514
|
+
case "object":
|
|
15515
|
+
return Object.fromEntries(
|
|
15516
|
+
value.map(([k, v]) => [k, remoteValueToNative(v)])
|
|
15517
|
+
);
|
|
15518
|
+
case "map":
|
|
15519
|
+
return Object.fromEntries(
|
|
15520
|
+
value.map(([k, v]) => [
|
|
15521
|
+
typeof k === "object" ? JSON.stringify(remoteValueToNative(k)) : String(k),
|
|
15522
|
+
remoteValueToNative(v)
|
|
15523
|
+
])
|
|
15524
|
+
);
|
|
15525
|
+
case "set":
|
|
15526
|
+
return value.map(remoteValueToNative);
|
|
15527
|
+
case "regexp": {
|
|
15528
|
+
const { pattern, flags } = value;
|
|
15529
|
+
return `/${pattern}/${flags ?? ""}`;
|
|
15530
|
+
}
|
|
15531
|
+
case "date":
|
|
15532
|
+
return value;
|
|
15533
|
+
default:
|
|
15534
|
+
return `[${type}]`;
|
|
15535
|
+
}
|
|
15536
|
+
}
|
|
15537
|
+
var init_remote_value = __esm({
|
|
15538
|
+
"src/utils/remote-value.ts"() {
|
|
15539
|
+
"use strict";
|
|
15540
|
+
}
|
|
15541
|
+
});
|
|
15542
|
+
|
|
15400
15543
|
// src/firefox/events/console.ts
|
|
15401
15544
|
var MAX_CONSOLE_MESSAGES, CONSOLE_TTL_MS, ConsoleEvents;
|
|
15402
15545
|
var init_console = __esm({
|
|
@@ -15454,9 +15597,6 @@ var init_console = __esm({
|
|
|
15454
15597
|
if (this.options.autoClearOnNavigate) {
|
|
15455
15598
|
this.clearMessages();
|
|
15456
15599
|
}
|
|
15457
|
-
if (this.options.onNavigate) {
|
|
15458
|
-
this.options.onNavigate();
|
|
15459
|
-
}
|
|
15460
15600
|
}
|
|
15461
15601
|
} catch {
|
|
15462
15602
|
}
|
|
@@ -15499,16 +15639,18 @@ var init_console = __esm({
|
|
|
15499
15639
|
});
|
|
15500
15640
|
|
|
15501
15641
|
// src/firefox/events/network.ts
|
|
15502
|
-
var MAX_NETWORK_REQUESTS, NETWORK_TTL_MS, NetworkEvents;
|
|
15642
|
+
var MAX_NETWORK_REQUESTS, NETWORK_TTL_MS, MAX_ENCODED_DATA_SIZE, NetworkEvents;
|
|
15503
15643
|
var init_network = __esm({
|
|
15504
15644
|
"src/firefox/events/network.ts"() {
|
|
15505
15645
|
"use strict";
|
|
15506
15646
|
init_logger();
|
|
15507
15647
|
MAX_NETWORK_REQUESTS = 1e3;
|
|
15508
15648
|
NETWORK_TTL_MS = 5 * 60 * 1e3;
|
|
15649
|
+
MAX_ENCODED_DATA_SIZE = 10 * 1e3 * 1e3;
|
|
15509
15650
|
NetworkEvents = class {
|
|
15510
|
-
constructor(driver, options = {}) {
|
|
15651
|
+
constructor(driver, options = {}, sendCommand) {
|
|
15511
15652
|
this.driver = driver;
|
|
15653
|
+
this.sendCommand = sendCommand;
|
|
15512
15654
|
this.options = {
|
|
15513
15655
|
autoClearOnNavigate: true,
|
|
15514
15656
|
...options
|
|
@@ -15519,6 +15661,7 @@ var init_network = __esm({
|
|
|
15519
15661
|
enabled = false;
|
|
15520
15662
|
requestStartTimes = /* @__PURE__ */ new Map();
|
|
15521
15663
|
options;
|
|
15664
|
+
collectorId = null;
|
|
15522
15665
|
/**
|
|
15523
15666
|
* Subscribe to BiDi network events and navigation lifecycle
|
|
15524
15667
|
* Enables monitoring by default (always-on capture)
|
|
@@ -15547,9 +15690,6 @@ var init_network = __esm({
|
|
|
15547
15690
|
if (this.enabled && this.options.autoClearOnNavigate) {
|
|
15548
15691
|
this.clearRequests();
|
|
15549
15692
|
}
|
|
15550
|
-
if (this.options.onNavigate) {
|
|
15551
|
-
this.options.onNavigate();
|
|
15552
|
-
}
|
|
15553
15693
|
return;
|
|
15554
15694
|
}
|
|
15555
15695
|
if (!this.enabled) {
|
|
@@ -15611,10 +15751,77 @@ var init_network = __esm({
|
|
|
15611
15751
|
} catch {
|
|
15612
15752
|
}
|
|
15613
15753
|
});
|
|
15754
|
+
await this.registerDataCollector();
|
|
15614
15755
|
this.subscribed = true;
|
|
15615
15756
|
this.enabled = true;
|
|
15616
15757
|
logDebug("Network listener ready with lifecycle hooks (monitoring enabled by default)");
|
|
15617
15758
|
}
|
|
15759
|
+
/**
|
|
15760
|
+
* Register a BiDi network data collector for request and response bodies.
|
|
15761
|
+
* Failures are non-fatal and simply leave body capture disabled.
|
|
15762
|
+
*/
|
|
15763
|
+
async registerDataCollector() {
|
|
15764
|
+
if (this.options.captureBodies === false) {
|
|
15765
|
+
logDebug("Network body capture disabled, skipping data collector registration");
|
|
15766
|
+
return;
|
|
15767
|
+
}
|
|
15768
|
+
if (!this.sendCommand) {
|
|
15769
|
+
return;
|
|
15770
|
+
}
|
|
15771
|
+
try {
|
|
15772
|
+
const result = await this.sendCommand("network.addDataCollector", {
|
|
15773
|
+
dataTypes: ["request", "response"],
|
|
15774
|
+
maxEncodedDataSize: MAX_ENCODED_DATA_SIZE
|
|
15775
|
+
});
|
|
15776
|
+
this.collectorId = result?.collector ?? null;
|
|
15777
|
+
if (this.collectorId) {
|
|
15778
|
+
logDebug(`Network data collector registered (${this.collectorId})`);
|
|
15779
|
+
}
|
|
15780
|
+
} catch (error2) {
|
|
15781
|
+
logDebug(
|
|
15782
|
+
`Network data collector unavailable, response bodies will not be captured: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
15783
|
+
);
|
|
15784
|
+
}
|
|
15785
|
+
}
|
|
15786
|
+
/**
|
|
15787
|
+
* Fetch a captured request or response body via network.getData.
|
|
15788
|
+
* Returns a structured result so callers can render an appropriate marker
|
|
15789
|
+
* when the body was never collected, evicted, or the browser lacks support.
|
|
15790
|
+
*/
|
|
15791
|
+
async fetchBody(requestId, dataType) {
|
|
15792
|
+
if (!this.sendCommand || !this.collectorId) {
|
|
15793
|
+
return { ok: false, reason: "unsupported" };
|
|
15794
|
+
}
|
|
15795
|
+
try {
|
|
15796
|
+
const result = await this.sendCommand("network.getData", {
|
|
15797
|
+
request: requestId,
|
|
15798
|
+
dataType
|
|
15799
|
+
});
|
|
15800
|
+
const bytes = result?.bytes;
|
|
15801
|
+
if (!bytes || typeof bytes.value !== "string") {
|
|
15802
|
+
return { ok: false, reason: "not-collected" };
|
|
15803
|
+
}
|
|
15804
|
+
return {
|
|
15805
|
+
ok: true,
|
|
15806
|
+
type: bytes.type === "base64" ? "base64" : "string",
|
|
15807
|
+
value: bytes.value
|
|
15808
|
+
};
|
|
15809
|
+
} catch (error2) {
|
|
15810
|
+
const message = (error2 instanceof Error ? error2.message : String(error2)).toLowerCase();
|
|
15811
|
+
if (message.includes("no such network data")) {
|
|
15812
|
+
return { ok: false, reason: "not-collected" };
|
|
15813
|
+
}
|
|
15814
|
+
if (message.includes("unavailable network data")) {
|
|
15815
|
+
if (message.includes("evicted")) {
|
|
15816
|
+
return { ok: false, reason: "evicted" };
|
|
15817
|
+
}
|
|
15818
|
+
if (message.includes("aborted")) {
|
|
15819
|
+
return { ok: false, reason: "aborted" };
|
|
15820
|
+
}
|
|
15821
|
+
}
|
|
15822
|
+
return { ok: false, reason: "error" };
|
|
15823
|
+
}
|
|
15824
|
+
}
|
|
15618
15825
|
/**
|
|
15619
15826
|
* Start collecting network requests
|
|
15620
15827
|
*/
|
|
@@ -16033,19 +16240,6 @@ var init_dom = __esm({
|
|
|
16033
16240
|
this.driver = driver;
|
|
16034
16241
|
this.resolveUid = resolveUid;
|
|
16035
16242
|
}
|
|
16036
|
-
/**
|
|
16037
|
-
* Evaluate JavaScript - direct passthrough to executeScript
|
|
16038
|
-
*/
|
|
16039
|
-
async evaluate(script) {
|
|
16040
|
-
return await this.driver.executeScript(script);
|
|
16041
|
-
}
|
|
16042
|
-
/**
|
|
16043
|
-
* Get page HTML content
|
|
16044
|
-
*/
|
|
16045
|
-
async getContent() {
|
|
16046
|
-
const html = await this.evaluate("return document.documentElement.outerHTML");
|
|
16047
|
-
return String(html);
|
|
16048
|
-
}
|
|
16049
16243
|
// ============================================================================
|
|
16050
16244
|
// Element polling helpers
|
|
16051
16245
|
// ============================================================================
|
|
@@ -16509,7 +16703,12 @@ __export(formatter_exports, {
|
|
|
16509
16703
|
formatSnapshotTree: () => formatSnapshotTree
|
|
16510
16704
|
});
|
|
16511
16705
|
function formatSnapshotTree(node, depth = 0, options = {}) {
|
|
16512
|
-
const {
|
|
16706
|
+
const {
|
|
16707
|
+
includeAttributes = true,
|
|
16708
|
+
includeText = true,
|
|
16709
|
+
maxDepth,
|
|
16710
|
+
maxAttrLength = MAX_ATTR_LENGTH
|
|
16711
|
+
} = options;
|
|
16513
16712
|
if (maxDepth !== void 0 && depth >= maxDepth) {
|
|
16514
16713
|
return "";
|
|
16515
16714
|
}
|
|
@@ -16519,22 +16718,22 @@ function formatSnapshotTree(node, depth = 0, options = {}) {
|
|
|
16519
16718
|
const role = node.role || node.tag;
|
|
16520
16719
|
attrs.push(role);
|
|
16521
16720
|
if (node.name) {
|
|
16522
|
-
attrs.push(`"${truncate(node.name,
|
|
16721
|
+
attrs.push(`"${truncate(node.name, maxAttrLength)}"`);
|
|
16523
16722
|
}
|
|
16524
16723
|
if (node.role && node.role !== node.tag) {
|
|
16525
16724
|
attrs.push(`tag=${node.tag}`);
|
|
16526
16725
|
}
|
|
16527
16726
|
if (node.value) {
|
|
16528
|
-
attrs.push(`value="${truncate(node.value,
|
|
16727
|
+
attrs.push(`value="${truncate(node.value, maxAttrLength)}"`);
|
|
16529
16728
|
}
|
|
16530
16729
|
if (node.href) {
|
|
16531
|
-
attrs.push(`href="${truncate(node.href,
|
|
16730
|
+
attrs.push(`href="${truncate(node.href, maxAttrLength)}"`);
|
|
16532
16731
|
}
|
|
16533
16732
|
if (node.src) {
|
|
16534
|
-
attrs.push(`src="${truncate(node.src,
|
|
16733
|
+
attrs.push(`src="${truncate(node.src, maxAttrLength)}"`);
|
|
16535
16734
|
}
|
|
16536
16735
|
if (includeText && node.text) {
|
|
16537
|
-
attrs.push(`text="${truncate(node.text,
|
|
16736
|
+
attrs.push(`text="${truncate(node.text, maxAttrLength)}"`);
|
|
16538
16737
|
}
|
|
16539
16738
|
if (includeAttributes && node.aria) {
|
|
16540
16739
|
if (node.aria.disabled) {
|
|
@@ -16593,7 +16792,7 @@ function formatSnapshotTree(node, depth = 0, options = {}) {
|
|
|
16593
16792
|
if (node.isIframe) {
|
|
16594
16793
|
attrs.push("[iframe");
|
|
16595
16794
|
if (node.frameSrc) {
|
|
16596
|
-
attrs.push(`src="${truncate(node.frameSrc,
|
|
16795
|
+
attrs.push(`src="${truncate(node.frameSrc, maxAttrLength)}"`);
|
|
16597
16796
|
}
|
|
16598
16797
|
if (node.crossOrigin) {
|
|
16599
16798
|
attrs.push("cross-origin");
|
|
@@ -16621,140 +16820,60 @@ var init_formatter = __esm({
|
|
|
16621
16820
|
});
|
|
16622
16821
|
|
|
16623
16822
|
// src/firefox/snapshot/resolver.ts
|
|
16624
|
-
|
|
16625
|
-
|
|
16823
|
+
function notFoundMessage(uid) {
|
|
16824
|
+
return `UID not found: ${uid}. The element is gone from the page, or the page was reloaded. Take a fresh snapshot first.`;
|
|
16825
|
+
}
|
|
16826
|
+
var RESOLVE_SCRIPT, SELECTOR_SCRIPT, CLEAR_SCRIPT, UidResolver;
|
|
16626
16827
|
var init_resolver = __esm({
|
|
16627
16828
|
"src/firefox/snapshot/resolver.ts"() {
|
|
16628
16829
|
"use strict";
|
|
16629
16830
|
init_logger();
|
|
16831
|
+
RESOLVE_SCRIPT = "return window.__resolveUid ? window.__resolveUid(arguments[0]) : null;";
|
|
16832
|
+
SELECTOR_SCRIPT = "return window.__uidToSelector ? window.__uidToSelector(arguments[0]) : null;";
|
|
16833
|
+
CLEAR_SCRIPT = "if (window.__clearUidRegistry) { window.__clearUidRegistry(); }";
|
|
16630
16834
|
UidResolver = class {
|
|
16631
16835
|
constructor(driver) {
|
|
16632
16836
|
this.driver = driver;
|
|
16633
16837
|
}
|
|
16634
|
-
uidToEntry = /* @__PURE__ */ new Map();
|
|
16635
|
-
elementCache = /* @__PURE__ */ new Map();
|
|
16636
|
-
currentSnapshotId = 0;
|
|
16637
|
-
/**
|
|
16638
|
-
* Update current snapshot ID
|
|
16639
|
-
*/
|
|
16640
|
-
setSnapshotId(snapshotId) {
|
|
16641
|
-
this.currentSnapshotId = snapshotId;
|
|
16642
|
-
}
|
|
16643
|
-
/**
|
|
16644
|
-
* Get current snapshot ID
|
|
16645
|
-
*/
|
|
16646
|
-
getSnapshotId() {
|
|
16647
|
-
return this.currentSnapshotId;
|
|
16648
|
-
}
|
|
16649
|
-
/**
|
|
16650
|
-
* Store UID mappings from snapshot result
|
|
16651
|
-
*/
|
|
16652
|
-
storeUidMappings(uidMap) {
|
|
16653
|
-
this.uidToEntry.clear();
|
|
16654
|
-
for (const entry of uidMap) {
|
|
16655
|
-
this.uidToEntry.set(entry.uid, entry);
|
|
16656
|
-
}
|
|
16657
|
-
}
|
|
16658
|
-
/**
|
|
16659
|
-
* Clear all UID mappings and cache
|
|
16660
|
-
*/
|
|
16661
|
-
clear() {
|
|
16662
|
-
this.uidToEntry.clear();
|
|
16663
|
-
this.elementCache.clear();
|
|
16664
|
-
logDebug("Snapshot UIDs cleared");
|
|
16665
|
-
}
|
|
16666
16838
|
/**
|
|
16667
|
-
*
|
|
16839
|
+
* Forget all UID associations in the page, making existing UIDs unresolvable.
|
|
16840
|
+
* Best effort: the registry dies with the page anyway.
|
|
16668
16841
|
*/
|
|
16669
|
-
|
|
16670
|
-
|
|
16671
|
-
|
|
16672
|
-
|
|
16673
|
-
}
|
|
16674
|
-
|
|
16675
|
-
if (isNaN(uidSnapshotId)) {
|
|
16676
|
-
throw new Error(`Invalid UID format: ${uid}`);
|
|
16677
|
-
}
|
|
16678
|
-
if (uidSnapshotId !== this.currentSnapshotId) {
|
|
16679
|
-
throw new Error(
|
|
16680
|
-
`This uid is from a stale snapshot (snapshot ${uidSnapshotId}, current ${this.currentSnapshotId}). Take a fresh snapshot.`
|
|
16681
|
-
);
|
|
16842
|
+
async clear() {
|
|
16843
|
+
try {
|
|
16844
|
+
await this.driver.executeScript(CLEAR_SCRIPT);
|
|
16845
|
+
logDebug("Snapshot UIDs cleared");
|
|
16846
|
+
} catch {
|
|
16847
|
+
logDebug("Unable to clear snapshot UIDs (page may be navigating)");
|
|
16682
16848
|
}
|
|
16683
16849
|
}
|
|
16684
16850
|
/**
|
|
16685
|
-
* Resolve UID to CSS selector
|
|
16851
|
+
* Resolve UID to a CSS selector, generated on demand from the element it points at
|
|
16686
16852
|
*/
|
|
16687
|
-
resolveUidToSelector(uid) {
|
|
16688
|
-
this.
|
|
16689
|
-
|
|
16690
|
-
|
|
16691
|
-
throw new Error(`UID not found: ${uid}. Take a fresh snapshot first.`);
|
|
16853
|
+
async resolveUidToSelector(uid) {
|
|
16854
|
+
const selector = await this.driver.executeScript(SELECTOR_SCRIPT, uid);
|
|
16855
|
+
if (!selector) {
|
|
16856
|
+
throw new Error(notFoundMessage(uid));
|
|
16692
16857
|
}
|
|
16693
|
-
return
|
|
16858
|
+
return selector;
|
|
16694
16859
|
}
|
|
16695
16860
|
/**
|
|
16696
|
-
* Resolve UID to element
|
|
16697
|
-
* Tries CSS first, falls back to XPath
|
|
16861
|
+
* Resolve UID to the element it was assigned to during the snapshot
|
|
16698
16862
|
*/
|
|
16699
16863
|
async resolveUidToElement(uid) {
|
|
16700
|
-
this.
|
|
16701
|
-
|
|
16702
|
-
|
|
16703
|
-
throw new Error(`UID not found: ${uid}. Take a fresh snapshot first.`);
|
|
16704
|
-
}
|
|
16705
|
-
const cached2 = this.elementCache.get(uid);
|
|
16706
|
-
if (cached2?.cachedElement) {
|
|
16707
|
-
try {
|
|
16708
|
-
await cached2.cachedElement.isDisplayed();
|
|
16709
|
-
logDebug(`Using cached element for UID: ${uid}`);
|
|
16710
|
-
return cached2.cachedElement;
|
|
16711
|
-
} catch {
|
|
16712
|
-
logDebug(`Cached element stale for UID: ${uid}, re-finding...`);
|
|
16713
|
-
}
|
|
16714
|
-
}
|
|
16715
|
-
try {
|
|
16716
|
-
const element = await this.driver.findElement(By2.css(entry.css));
|
|
16717
|
-
this.elementCache.set(uid, {
|
|
16718
|
-
selector: entry.css,
|
|
16719
|
-
...entry.xpath && { xpath: entry.xpath },
|
|
16720
|
-
cachedElement: element,
|
|
16721
|
-
snapshotId: this.currentSnapshotId,
|
|
16722
|
-
timestamp: Date.now()
|
|
16723
|
-
});
|
|
16724
|
-
logDebug(`Found element by CSS for UID: ${uid}`);
|
|
16725
|
-
return element;
|
|
16726
|
-
} catch {
|
|
16727
|
-
logDebug(`CSS selector failed for UID: ${uid}, trying XPath fallback...`);
|
|
16728
|
-
const xpathSelector = entry.xpath;
|
|
16729
|
-
if (xpathSelector) {
|
|
16730
|
-
try {
|
|
16731
|
-
const element = await this.driver.findElement(By2.xpath(xpathSelector));
|
|
16732
|
-
this.elementCache.set(uid, {
|
|
16733
|
-
selector: entry.css,
|
|
16734
|
-
...xpathSelector && { xpath: xpathSelector },
|
|
16735
|
-
cachedElement: element,
|
|
16736
|
-
snapshotId: this.currentSnapshotId,
|
|
16737
|
-
timestamp: Date.now()
|
|
16738
|
-
});
|
|
16739
|
-
logDebug(`Found element by XPath for UID: ${uid}`);
|
|
16740
|
-
return element;
|
|
16741
|
-
} catch {
|
|
16742
|
-
throw new Error(
|
|
16743
|
-
`Element not found for UID: ${uid}. The element may have changed. Take a fresh snapshot.`
|
|
16744
|
-
);
|
|
16745
|
-
}
|
|
16746
|
-
}
|
|
16747
|
-
throw new Error(
|
|
16748
|
-
`Element not found for UID: ${uid}. The element may have changed. Take a fresh snapshot.`
|
|
16749
|
-
);
|
|
16864
|
+
const element = await this.driver.executeScript(RESOLVE_SCRIPT, uid);
|
|
16865
|
+
if (!element) {
|
|
16866
|
+
throw new Error(notFoundMessage(uid));
|
|
16750
16867
|
}
|
|
16868
|
+
logDebug(`Resolved element for UID: ${uid}`);
|
|
16869
|
+
return element;
|
|
16751
16870
|
}
|
|
16752
16871
|
};
|
|
16753
16872
|
}
|
|
16754
16873
|
});
|
|
16755
16874
|
|
|
16756
16875
|
// src/firefox/snapshot/manager.ts
|
|
16757
|
-
import { readFileSync } from "fs";
|
|
16876
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
16758
16877
|
import { dirname, resolve } from "path";
|
|
16759
16878
|
import { fileURLToPath } from "url";
|
|
16760
16879
|
var SnapshotManager;
|
|
@@ -16768,7 +16887,8 @@ var init_manager = __esm({
|
|
|
16768
16887
|
driver;
|
|
16769
16888
|
resolver;
|
|
16770
16889
|
injectedScript = null;
|
|
16771
|
-
|
|
16890
|
+
/** Counter handed to the injected script so UIDs stay unique across snapshots */
|
|
16891
|
+
nextElementId = 0;
|
|
16772
16892
|
constructor(driver) {
|
|
16773
16893
|
this.driver = driver;
|
|
16774
16894
|
this.resolver = new UidResolver(driver);
|
|
@@ -16796,7 +16916,7 @@ var init_manager = __esm({
|
|
|
16796
16916
|
for (const path of possiblePaths) {
|
|
16797
16917
|
attemptedPaths.push(path);
|
|
16798
16918
|
try {
|
|
16799
|
-
this.injectedScript =
|
|
16919
|
+
this.injectedScript = readFileSync2(path, "utf-8");
|
|
16800
16920
|
const sizeKB = (this.injectedScript.length / 1024).toFixed(1);
|
|
16801
16921
|
logDebug(`\u2713 Loaded snapshot bundle: ${path.split("/").pop()} (${sizeKB} KB)`);
|
|
16802
16922
|
return this.injectedScript;
|
|
@@ -16815,14 +16935,25 @@ ${attemptedPaths.map((p) => ` - ${p}`).join("\n")}`
|
|
|
16815
16935
|
}
|
|
16816
16936
|
/**
|
|
16817
16937
|
* Take a snapshot of the current page
|
|
16818
|
-
* Returns text and JSON
|
|
16938
|
+
* Returns text and JSON, no DOM mutations
|
|
16819
16939
|
*/
|
|
16820
16940
|
async takeSnapshot(options) {
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16941
|
+
if (options?.selector || options?.includeAll) {
|
|
16942
|
+
const optionsOutput = [];
|
|
16943
|
+
if (options.selector) {
|
|
16944
|
+
optionsOutput.push(`selector: ${options.selector}`);
|
|
16945
|
+
}
|
|
16946
|
+
if (options.includeAll) {
|
|
16947
|
+
optionsOutput.push("include all");
|
|
16948
|
+
}
|
|
16949
|
+
logDebug(`Taking snapshot (${optionsOutput.join(", ")})...`);
|
|
16950
|
+
} else {
|
|
16951
|
+
logDebug("Taking snapshot...");
|
|
16952
|
+
}
|
|
16953
|
+
const result = await this.executeInjectedScript(this.nextElementId, options);
|
|
16954
|
+
if (typeof result?.nextElementId === "number") {
|
|
16955
|
+
this.nextElementId = result.nextElementId;
|
|
16956
|
+
}
|
|
16826
16957
|
logDebug(
|
|
16827
16958
|
`Snapshot executeScript result: hasResult=${!!result}, hasTree=${!!result?.tree}, truncated=${result?.truncated || false}`
|
|
16828
16959
|
);
|
|
@@ -16844,60 +16975,60 @@ ${attemptedPaths.map((p) => ` - ${p}`).join("\n")}`
|
|
|
16844
16975
|
logDebug(`Snapshot generation failed: ${errorMsg}`);
|
|
16845
16976
|
throw new Error(`Failed to generate snapshot: ${errorMsg}`);
|
|
16846
16977
|
}
|
|
16847
|
-
this.resolver.storeUidMappings(result.uidMap);
|
|
16848
16978
|
const snapshotJson = {
|
|
16849
16979
|
root: result.tree,
|
|
16850
|
-
snapshotId,
|
|
16851
16980
|
timestamp: Date.now(),
|
|
16852
|
-
truncated: result.truncated || false
|
|
16853
|
-
uidMap: result.uidMap
|
|
16981
|
+
truncated: result.truncated || false
|
|
16854
16982
|
};
|
|
16855
16983
|
const snapshot = {
|
|
16856
16984
|
text: formatSnapshotTree(result.tree),
|
|
16857
16985
|
json: snapshotJson
|
|
16858
16986
|
};
|
|
16859
16987
|
logDebug(
|
|
16860
|
-
`Snapshot created: ${result.
|
|
16988
|
+
`Snapshot created: ${result.nodeCount} elements with UIDs${result.truncated ? " (truncated)" : ""}`
|
|
16861
16989
|
);
|
|
16862
16990
|
return snapshot;
|
|
16863
16991
|
}
|
|
16864
16992
|
/**
|
|
16865
|
-
* Resolve UID to CSS selector
|
|
16993
|
+
* Resolve UID to a CSS selector generated on demand
|
|
16866
16994
|
*/
|
|
16867
|
-
resolveUidToSelector(uid) {
|
|
16868
|
-
return this.resolver.resolveUidToSelector(uid);
|
|
16995
|
+
async resolveUidToSelector(uid) {
|
|
16996
|
+
return await this.resolver.resolveUidToSelector(uid);
|
|
16869
16997
|
}
|
|
16870
16998
|
/**
|
|
16871
|
-
* Resolve UID to WebElement
|
|
16999
|
+
* Resolve UID to the WebElement it was assigned to
|
|
16872
17000
|
*/
|
|
16873
17001
|
async resolveUidToElement(uid) {
|
|
16874
17002
|
return await this.resolver.resolveUidToElement(uid);
|
|
16875
17003
|
}
|
|
16876
17004
|
/**
|
|
16877
|
-
* Clear snapshot
|
|
17005
|
+
* Clear snapshot UIDs
|
|
16878
17006
|
*/
|
|
16879
|
-
clear() {
|
|
16880
|
-
this.resolver.clear();
|
|
17007
|
+
async clear() {
|
|
17008
|
+
await this.resolver.clear();
|
|
16881
17009
|
}
|
|
16882
17010
|
/**
|
|
16883
17011
|
* Execute bundled injected snapshot script
|
|
16884
17012
|
*/
|
|
16885
|
-
async executeInjectedScript(
|
|
17013
|
+
async executeInjectedScript(nextElementId, options) {
|
|
16886
17014
|
const scriptSource = this.getInjectedScript();
|
|
16887
17015
|
const result = await this.driver.executeScript(
|
|
16888
17016
|
`
|
|
16889
17017
|
// Only inject the bundle if not already present
|
|
16890
17018
|
if (typeof window.__createSnapshot === 'undefined') {
|
|
16891
17019
|
${scriptSource}
|
|
16892
|
-
// Register the
|
|
17020
|
+
// Register the snapshot and UID resolution functions globally
|
|
16893
17021
|
if (typeof __SnapshotInjected !== 'undefined' && __SnapshotInjected.createSnapshot) {
|
|
16894
17022
|
window.__createSnapshot = __SnapshotInjected.createSnapshot;
|
|
17023
|
+
window.__resolveUid = __SnapshotInjected.resolveUid;
|
|
17024
|
+
window.__uidToSelector = __SnapshotInjected.uidToSelector;
|
|
17025
|
+
window.__clearUidRegistry = __SnapshotInjected.clearUidRegistry;
|
|
16895
17026
|
}
|
|
16896
17027
|
}
|
|
16897
17028
|
// Call it with options
|
|
16898
17029
|
return window.__createSnapshot(arguments[0], arguments[1]);
|
|
16899
17030
|
`,
|
|
16900
|
-
|
|
17031
|
+
nextElementId,
|
|
16901
17032
|
options || {}
|
|
16902
17033
|
);
|
|
16903
17034
|
return result;
|
|
@@ -16922,6 +17053,7 @@ var init_firefox = __esm({
|
|
|
16922
17053
|
"use strict";
|
|
16923
17054
|
init_core3();
|
|
16924
17055
|
init_logger();
|
|
17056
|
+
init_remote_value();
|
|
16925
17057
|
init_events();
|
|
16926
17058
|
init_dom();
|
|
16927
17059
|
init_pages();
|
|
@@ -16945,21 +17077,19 @@ var init_firefox = __esm({
|
|
|
16945
17077
|
await this.core.connect();
|
|
16946
17078
|
const driver = this.core.getDriver();
|
|
16947
17079
|
this.snapshot = new SnapshotManager(driver);
|
|
16948
|
-
const onNavigate = () => {
|
|
16949
|
-
if (this.snapshot) {
|
|
16950
|
-
this.snapshot.clear();
|
|
16951
|
-
}
|
|
16952
|
-
};
|
|
16953
17080
|
const hasBidi = "getBidi" in driver && typeof driver.getBidi === "function";
|
|
16954
17081
|
if (hasBidi) {
|
|
16955
17082
|
this.consoleEvents = new ConsoleEvents(driver, {
|
|
16956
|
-
onNavigate,
|
|
16957
|
-
autoClearOnNavigate: false
|
|
16958
|
-
});
|
|
16959
|
-
this.networkEvents = new NetworkEvents(driver, {
|
|
16960
|
-
onNavigate,
|
|
16961
17083
|
autoClearOnNavigate: false
|
|
16962
17084
|
});
|
|
17085
|
+
this.networkEvents = new NetworkEvents(
|
|
17086
|
+
driver,
|
|
17087
|
+
{
|
|
17088
|
+
autoClearOnNavigate: false,
|
|
17089
|
+
captureBodies: this.core.getOptions().captureNetworkBodies !== false
|
|
17090
|
+
},
|
|
17091
|
+
(method, params) => this.core.sendBiDiCommand(method, params ?? {})
|
|
17092
|
+
);
|
|
16963
17093
|
this.debuggingEvents = new DebuggingEvents(
|
|
16964
17094
|
driver,
|
|
16965
17095
|
(method, params) => this.core.sendBiDiCommand(method, params)
|
|
@@ -17012,17 +17142,24 @@ var init_firefox = __esm({
|
|
|
17012
17142
|
// ============================================================================
|
|
17013
17143
|
// DOM / Evaluate
|
|
17014
17144
|
// ============================================================================
|
|
17015
|
-
|
|
17016
|
-
|
|
17017
|
-
|
|
17018
|
-
|
|
17019
|
-
|
|
17020
|
-
|
|
17021
|
-
async
|
|
17022
|
-
|
|
17023
|
-
|
|
17145
|
+
/**
|
|
17146
|
+
* Evaluate a JavaScript expression in the current browsing context over
|
|
17147
|
+
* WebDriver BiDi (script.evaluate), so reads target the BiDi-tracked context
|
|
17148
|
+
* rather than Selenium's classic window handle. Returns the result as a
|
|
17149
|
+
* native value; throws on a script exception.
|
|
17150
|
+
*/
|
|
17151
|
+
async evaluate(expression) {
|
|
17152
|
+
const result = await this.core.sendBiDiCommand("script.evaluate", {
|
|
17153
|
+
expression,
|
|
17154
|
+
awaitPromise: true,
|
|
17155
|
+
target: { context: this.core.getCurrentContextId() }
|
|
17156
|
+
});
|
|
17157
|
+
if (result.type === "success") {
|
|
17158
|
+
return remoteValueToNative(result.result);
|
|
17024
17159
|
}
|
|
17025
|
-
|
|
17160
|
+
throw new Error(
|
|
17161
|
+
`Script evaluation failed: ${result.exceptionDetails?.text ?? "unknown error"}`
|
|
17162
|
+
);
|
|
17026
17163
|
}
|
|
17027
17164
|
async clickBySelector(selector) {
|
|
17028
17165
|
if (!this.dom) {
|
|
@@ -17118,7 +17255,6 @@ var init_firefox = __esm({
|
|
|
17118
17255
|
throw new Error("Not connected");
|
|
17119
17256
|
}
|
|
17120
17257
|
await this.pages.navigate(url);
|
|
17121
|
-
this.clearSnapshot();
|
|
17122
17258
|
}
|
|
17123
17259
|
async navigateBack() {
|
|
17124
17260
|
if (!this.pages) {
|
|
@@ -17221,6 +17357,18 @@ var init_firefox = __esm({
|
|
|
17221
17357
|
}
|
|
17222
17358
|
this.networkEvents.clearRequests();
|
|
17223
17359
|
}
|
|
17360
|
+
/**
|
|
17361
|
+
* Fetch a captured request or response body for a given request id.
|
|
17362
|
+
* Returns a structured result describing the body or why it is unavailable.
|
|
17363
|
+
*/
|
|
17364
|
+
async getNetworkRequestBody(requestId, dataType) {
|
|
17365
|
+
if (!this.networkEvents) {
|
|
17366
|
+
throw new Error(
|
|
17367
|
+
"Network events not available (Firefox Remote Agent not running \u2014 start Firefox with --remote-debugging-port to enable BiDi)"
|
|
17368
|
+
);
|
|
17369
|
+
}
|
|
17370
|
+
return this.networkEvents.fetchBody(requestId, dataType);
|
|
17371
|
+
}
|
|
17224
17372
|
// ============================================================================
|
|
17225
17373
|
// Downloads
|
|
17226
17374
|
// ============================================================================
|
|
@@ -17257,11 +17405,11 @@ var init_firefox = __esm({
|
|
|
17257
17405
|
}
|
|
17258
17406
|
return await this.snapshot.takeSnapshot(options);
|
|
17259
17407
|
}
|
|
17260
|
-
resolveUidToSelector(uid) {
|
|
17408
|
+
async resolveUidToSelector(uid) {
|
|
17261
17409
|
if (!this.snapshot) {
|
|
17262
17410
|
throw new Error("Not connected");
|
|
17263
17411
|
}
|
|
17264
|
-
return this.snapshot.resolveUidToSelector(uid);
|
|
17412
|
+
return await this.snapshot.resolveUidToSelector(uid);
|
|
17265
17413
|
}
|
|
17266
17414
|
async resolveUidToElement(uid) {
|
|
17267
17415
|
if (!this.snapshot) {
|
|
@@ -17269,11 +17417,11 @@ var init_firefox = __esm({
|
|
|
17269
17417
|
}
|
|
17270
17418
|
return await this.snapshot.resolveUidToElement(uid);
|
|
17271
17419
|
}
|
|
17272
|
-
clearSnapshot() {
|
|
17420
|
+
async clearSnapshot() {
|
|
17273
17421
|
if (!this.snapshot) {
|
|
17274
17422
|
throw new Error("Not connected");
|
|
17275
17423
|
}
|
|
17276
|
-
this.snapshot.clear();
|
|
17424
|
+
await this.snapshot.clear();
|
|
17277
17425
|
}
|
|
17278
17426
|
// ============================================================================
|
|
17279
17427
|
// Screenshot
|
|
@@ -17579,11 +17727,14 @@ async function getFirefox() {
|
|
|
17579
17727
|
acceptInsecureCerts: args.acceptInsecureCerts,
|
|
17580
17728
|
connectExisting: args.connectExisting,
|
|
17581
17729
|
marionettePort: args.marionettePort,
|
|
17730
|
+
lookupMarionettePort: args.lookupMarionettePort,
|
|
17582
17731
|
env: envVars,
|
|
17583
17732
|
logFile: args.outputFile ?? void 0,
|
|
17584
17733
|
prefs,
|
|
17585
17734
|
androidDevice: args.androidDevice ?? void 0,
|
|
17586
|
-
androidPackage: args.androidPackage ?? void 0
|
|
17735
|
+
androidPackage: args.androidPackage ?? void 0,
|
|
17736
|
+
androidWipeAppData: args.androidWipeAppData,
|
|
17737
|
+
captureNetworkBodies: !args.disableNetworkBodyCollection
|
|
17587
17738
|
};
|
|
17588
17739
|
}
|
|
17589
17740
|
firefox2 = new FirefoxClient(options);
|
|
@@ -17649,7 +17800,6 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
|
|
|
17649
17800
|
},
|
|
17650
17801
|
{
|
|
17651
17802
|
capabilities: {
|
|
17652
|
-
resources: {},
|
|
17653
17803
|
tools: {}
|
|
17654
17804
|
}
|
|
17655
17805
|
}
|
|
@@ -17683,12 +17833,6 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
|
|
|
17683
17833
|
throw error2;
|
|
17684
17834
|
}
|
|
17685
17835
|
});
|
|
17686
|
-
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
17687
|
-
return { resources: [] };
|
|
17688
|
-
});
|
|
17689
|
-
server.setRequestHandler(ReadResourceRequestSchema, async () => {
|
|
17690
|
-
throw new Error("Resource reading not implemented");
|
|
17691
|
-
});
|
|
17692
17836
|
const transport = new StdioServerTransport();
|
|
17693
17837
|
await server.connect(transport);
|
|
17694
17838
|
log("Firefox DevTools MCP server running on stdio");
|
|
@@ -17731,26 +17875,101 @@ var init_src = __esm({
|
|
|
17731
17875
|
}
|
|
17732
17876
|
});
|
|
17733
17877
|
|
|
17734
|
-
// src/
|
|
17735
|
-
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
const idx = tabs.indexOf(tab);
|
|
17742
|
-
const marker = idx === selectedIdx ? ">" : " ";
|
|
17743
|
-
const title = (tab.title || "Untitled").substring(0, 40);
|
|
17744
|
-
lines.push(`${marker}[${idx}] ${title}`);
|
|
17745
|
-
}
|
|
17746
|
-
return lines.join("\n");
|
|
17878
|
+
// src/utils/save-output.ts
|
|
17879
|
+
import { mkdir, rename, stat, unlink, writeFile } from "fs/promises";
|
|
17880
|
+
import { randomBytes } from "crypto";
|
|
17881
|
+
import { homedir as homedir2 } from "os";
|
|
17882
|
+
import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve3, sep } from "path";
|
|
17883
|
+
function homeRoot() {
|
|
17884
|
+
return join3(homedir2(), ".firefox-devtools-mcp");
|
|
17747
17885
|
}
|
|
17748
|
-
|
|
17886
|
+
function generatedName(baseName, extension) {
|
|
17887
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
17888
|
+
return `${baseName}-${timestamp}.${extension}`;
|
|
17889
|
+
}
|
|
17890
|
+
async function isDirectory(path) {
|
|
17749
17891
|
try {
|
|
17750
|
-
|
|
17751
|
-
|
|
17752
|
-
|
|
17753
|
-
|
|
17892
|
+
return (await stat(path)).isDirectory();
|
|
17893
|
+
} catch {
|
|
17894
|
+
return false;
|
|
17895
|
+
}
|
|
17896
|
+
}
|
|
17897
|
+
async function assertAllowedPath(saveTo, resolvedPath) {
|
|
17898
|
+
const { args: args2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
17899
|
+
if (args2?.unrestrictedSavePaths) {
|
|
17900
|
+
return;
|
|
17901
|
+
}
|
|
17902
|
+
const root = isAbsolute(saveTo) ? homeRoot() : process.cwd();
|
|
17903
|
+
if (resolvedPath !== root && !resolvedPath.startsWith(root + sep)) {
|
|
17904
|
+
throw new Error(
|
|
17905
|
+
`saveTo "${saveTo}" resolves outside the allowed location (${resolvedPath}). Relative paths must stay within the current working directory and absolute paths within ${homeRoot()}. Start the server with --unrestricted-save-paths to write to arbitrary locations.`
|
|
17906
|
+
);
|
|
17907
|
+
}
|
|
17908
|
+
}
|
|
17909
|
+
async function saveOutput(content, saveTo, baseName, extension = "json") {
|
|
17910
|
+
let resolvedPath;
|
|
17911
|
+
if (saveTo) {
|
|
17912
|
+
resolvedPath = resolve3(saveTo);
|
|
17913
|
+
if (await isDirectory(resolvedPath)) {
|
|
17914
|
+
resolvedPath = join3(resolvedPath, generatedName(baseName, extension));
|
|
17915
|
+
}
|
|
17916
|
+
await assertAllowedPath(saveTo, resolvedPath);
|
|
17917
|
+
} else {
|
|
17918
|
+
resolvedPath = join3(homeRoot(), "output", generatedName(baseName, extension));
|
|
17919
|
+
}
|
|
17920
|
+
await mkdir(dirname2(resolvedPath), { recursive: true });
|
|
17921
|
+
const tmpPath = `${resolvedPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
17922
|
+
try {
|
|
17923
|
+
await writeFile(tmpPath, content);
|
|
17924
|
+
await rename(tmpPath, resolvedPath);
|
|
17925
|
+
} catch (error2) {
|
|
17926
|
+
await unlink(tmpPath).catch(() => void 0);
|
|
17927
|
+
throw error2;
|
|
17928
|
+
}
|
|
17929
|
+
return { path: resolvedPath, bytes: Buffer.byteLength(content) };
|
|
17930
|
+
}
|
|
17931
|
+
var init_save_output = __esm({
|
|
17932
|
+
"src/utils/save-output.ts"() {
|
|
17933
|
+
"use strict";
|
|
17934
|
+
}
|
|
17935
|
+
});
|
|
17936
|
+
|
|
17937
|
+
// src/tools/module.ts
|
|
17938
|
+
function defineModule(config2) {
|
|
17939
|
+
return {
|
|
17940
|
+
name: config2.name,
|
|
17941
|
+
description: config2.description,
|
|
17942
|
+
...config2.privileged ? { privileged: true } : {},
|
|
17943
|
+
tools: config2.tools.map(([definition, handler]) => ({ definition, handler }))
|
|
17944
|
+
};
|
|
17945
|
+
}
|
|
17946
|
+
var init_module = __esm({
|
|
17947
|
+
"src/tools/module.ts"() {
|
|
17948
|
+
"use strict";
|
|
17949
|
+
}
|
|
17950
|
+
});
|
|
17951
|
+
|
|
17952
|
+
// src/tools/pages.ts
|
|
17953
|
+
function formatPageList(tabs, selectedIdx) {
|
|
17954
|
+
if (tabs.length === 0) {
|
|
17955
|
+
return "No pages";
|
|
17956
|
+
}
|
|
17957
|
+
const lines = [`${tabs.length} pages (selected: ${selectedIdx})`];
|
|
17958
|
+
for (const tab of tabs) {
|
|
17959
|
+
const idx = tabs.indexOf(tab);
|
|
17960
|
+
const marker = idx === selectedIdx ? ">" : " ";
|
|
17961
|
+
const title = (tab.title || "Untitled").substring(0, 40);
|
|
17962
|
+
const url = (tab.url || "URL unavailable").substring(0, 200);
|
|
17963
|
+
lines.push(`${marker}[${idx}] ${title} (${url})`);
|
|
17964
|
+
}
|
|
17965
|
+
return lines.join("\n");
|
|
17966
|
+
}
|
|
17967
|
+
async function handleListPages(_args) {
|
|
17968
|
+
try {
|
|
17969
|
+
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
17970
|
+
const firefox3 = await getFirefox2();
|
|
17971
|
+
await firefox3.refreshTabs();
|
|
17972
|
+
const tabs = firefox3.getTabs();
|
|
17754
17973
|
const selectedIdx = firefox3.getSelectedTabIdx();
|
|
17755
17974
|
return successResponse(formatPageList(tabs, selectedIdx));
|
|
17756
17975
|
} catch (error2) {
|
|
@@ -17848,12 +18067,57 @@ async function handleClosePage(args2) {
|
|
|
17848
18067
|
return errorResponse(error2);
|
|
17849
18068
|
}
|
|
17850
18069
|
}
|
|
17851
|
-
|
|
18070
|
+
async function respondWithContent(content, args2, baseName, extension) {
|
|
18071
|
+
const {
|
|
18072
|
+
maxLength = DEFAULT_MAX_CONTENT_CHARS,
|
|
18073
|
+
saveTo,
|
|
18074
|
+
preview
|
|
18075
|
+
} = args2 || {};
|
|
18076
|
+
if (saveTo) {
|
|
18077
|
+
const saved = await saveOutput(
|
|
18078
|
+
content,
|
|
18079
|
+
saveTo === true ? void 0 : saveTo,
|
|
18080
|
+
baseName,
|
|
18081
|
+
extension
|
|
18082
|
+
);
|
|
18083
|
+
let output = `${baseName} saved to: ${saved.path} (${(saved.bytes / 1024).toFixed(1)}KB)`;
|
|
18084
|
+
const excerpt = previewExcerpt(content, preview);
|
|
18085
|
+
if (excerpt) {
|
|
18086
|
+
output += "\nPreview:\n" + excerpt;
|
|
18087
|
+
}
|
|
18088
|
+
return successResponse(output);
|
|
18089
|
+
}
|
|
18090
|
+
if (content.length <= maxLength) {
|
|
18091
|
+
return successResponse(content + `
|
|
18092
|
+
|
|
18093
|
+
[full content, ${content.length} chars]`);
|
|
18094
|
+
}
|
|
18095
|
+
const footer = truncationFooter(content.length - maxLength, "chars", [
|
|
18096
|
+
"maxLength to show more",
|
|
18097
|
+
"saveTo to save the full content to a file"
|
|
18098
|
+
]);
|
|
18099
|
+
return successResponse(content.slice(0, maxLength) + "\n\n" + footer);
|
|
18100
|
+
}
|
|
18101
|
+
async function handleGetPageText(args2) {
|
|
18102
|
+
try {
|
|
18103
|
+
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
18104
|
+
const firefox3 = await getFirefox2();
|
|
18105
|
+
const text = await firefox3.evaluate(
|
|
18106
|
+
"document.body ? document.body.innerText : document.documentElement.innerText"
|
|
18107
|
+
);
|
|
18108
|
+
return respondWithContent(text ?? "", args2, "page-text", "txt");
|
|
18109
|
+
} catch (error2) {
|
|
18110
|
+
return errorResponse(error2);
|
|
18111
|
+
}
|
|
18112
|
+
}
|
|
18113
|
+
var DEFAULT_MAX_CONTENT_CHARS, listPagesTool, newPageTool, navigatePageTool, selectPageTool, closePageTool, getPageTextTool, module;
|
|
17852
18114
|
var init_pages2 = __esm({
|
|
17853
18115
|
"src/tools/pages.ts"() {
|
|
17854
18116
|
"use strict";
|
|
17855
18117
|
init_response_helpers();
|
|
18118
|
+
init_save_output();
|
|
17856
18119
|
init_module();
|
|
18120
|
+
DEFAULT_MAX_CONTENT_CHARS = 2e4;
|
|
17857
18121
|
listPagesTool = {
|
|
17858
18122
|
name: "list_pages",
|
|
17859
18123
|
description: "List open tabs (index, title, URL). Selected tab is marked.",
|
|
@@ -17941,6 +18205,30 @@ var init_pages2 = __esm({
|
|
|
17941
18205
|
required: ["pageIdx"]
|
|
17942
18206
|
}
|
|
17943
18207
|
};
|
|
18208
|
+
getPageTextTool = {
|
|
18209
|
+
name: "get_page_text",
|
|
18210
|
+
description: "Get the visible text of the page (document.body.innerText). Caps at maxLength (default 20000 chars); saveTo saves the full text to a file.",
|
|
18211
|
+
annotations: {
|
|
18212
|
+
readOnlyHint: true
|
|
18213
|
+
},
|
|
18214
|
+
inputSchema: {
|
|
18215
|
+
type: "object",
|
|
18216
|
+
properties: {
|
|
18217
|
+
maxLength: {
|
|
18218
|
+
type: "number",
|
|
18219
|
+
description: "Max characters to return inline (default: 20000). Ignored when saveTo is used."
|
|
18220
|
+
},
|
|
18221
|
+
saveTo: {
|
|
18222
|
+
type: ["boolean", "string"],
|
|
18223
|
+
description: "Save the full untruncated text to a file instead of returning it inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
18224
|
+
},
|
|
18225
|
+
preview: {
|
|
18226
|
+
type: "number",
|
|
18227
|
+
description: "Number of characters of the saved text to return inline as a preview when saveTo is used. Omit for no preview."
|
|
18228
|
+
}
|
|
18229
|
+
}
|
|
18230
|
+
}
|
|
18231
|
+
};
|
|
17944
18232
|
module = defineModule({
|
|
17945
18233
|
name: "pages",
|
|
17946
18234
|
description: "Open, navigate, select, and close pages.",
|
|
@@ -17949,7 +18237,8 @@ var init_pages2 = __esm({
|
|
|
17949
18237
|
[newPageTool, handleNewPage],
|
|
17950
18238
|
[navigatePageTool, handleNavigatePage],
|
|
17951
18239
|
[selectPageTool, handleSelectPage],
|
|
17952
|
-
[closePageTool, handleClosePage]
|
|
18240
|
+
[closePageTool, handleClosePage],
|
|
18241
|
+
[getPageTextTool, handleGetPageText]
|
|
17953
18242
|
]
|
|
17954
18243
|
});
|
|
17955
18244
|
}
|
|
@@ -17969,65 +18258,6 @@ var init_uid_helpers = __esm({
|
|
|
17969
18258
|
}
|
|
17970
18259
|
});
|
|
17971
18260
|
|
|
17972
|
-
// src/utils/save-output.ts
|
|
17973
|
-
import { mkdir, rename, stat, unlink, writeFile } from "fs/promises";
|
|
17974
|
-
import { randomBytes } from "crypto";
|
|
17975
|
-
import { homedir as homedir2 } from "os";
|
|
17976
|
-
import { dirname as dirname2, isAbsolute, join as join3, resolve as resolve3, sep } from "path";
|
|
17977
|
-
function homeRoot() {
|
|
17978
|
-
return join3(homedir2(), ".firefox-devtools-mcp");
|
|
17979
|
-
}
|
|
17980
|
-
function generatedName(baseName, extension) {
|
|
17981
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
17982
|
-
return `${baseName}-${timestamp}.${extension}`;
|
|
17983
|
-
}
|
|
17984
|
-
async function isDirectory(path) {
|
|
17985
|
-
try {
|
|
17986
|
-
return (await stat(path)).isDirectory();
|
|
17987
|
-
} catch {
|
|
17988
|
-
return false;
|
|
17989
|
-
}
|
|
17990
|
-
}
|
|
17991
|
-
async function assertAllowedPath(saveTo, resolvedPath) {
|
|
17992
|
-
const { args: args2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
17993
|
-
if (args2?.unrestrictedSavePaths) {
|
|
17994
|
-
return;
|
|
17995
|
-
}
|
|
17996
|
-
const root = isAbsolute(saveTo) ? homeRoot() : process.cwd();
|
|
17997
|
-
if (resolvedPath !== root && !resolvedPath.startsWith(root + sep)) {
|
|
17998
|
-
throw new Error(
|
|
17999
|
-
`saveTo "${saveTo}" resolves outside the allowed location (${resolvedPath}). Relative paths must stay within the current working directory and absolute paths within ${homeRoot()}. Start the server with --unrestricted-save-paths to write to arbitrary locations.`
|
|
18000
|
-
);
|
|
18001
|
-
}
|
|
18002
|
-
}
|
|
18003
|
-
async function saveOutput(content, saveTo, baseName, extension = "json") {
|
|
18004
|
-
let resolvedPath;
|
|
18005
|
-
if (saveTo) {
|
|
18006
|
-
resolvedPath = resolve3(saveTo);
|
|
18007
|
-
if (await isDirectory(resolvedPath)) {
|
|
18008
|
-
resolvedPath = join3(resolvedPath, generatedName(baseName, extension));
|
|
18009
|
-
}
|
|
18010
|
-
await assertAllowedPath(saveTo, resolvedPath);
|
|
18011
|
-
} else {
|
|
18012
|
-
resolvedPath = join3(homeRoot(), "output", generatedName(baseName, extension));
|
|
18013
|
-
}
|
|
18014
|
-
await mkdir(dirname2(resolvedPath), { recursive: true });
|
|
18015
|
-
const tmpPath = `${resolvedPath}.${randomBytes(6).toString("hex")}.tmp`;
|
|
18016
|
-
try {
|
|
18017
|
-
await writeFile(tmpPath, content);
|
|
18018
|
-
await rename(tmpPath, resolvedPath);
|
|
18019
|
-
} catch (error2) {
|
|
18020
|
-
await unlink(tmpPath).catch(() => void 0);
|
|
18021
|
-
throw error2;
|
|
18022
|
-
}
|
|
18023
|
-
return { path: resolvedPath, bytes: Buffer.byteLength(content) };
|
|
18024
|
-
}
|
|
18025
|
-
var init_save_output = __esm({
|
|
18026
|
-
"src/utils/save-output.ts"() {
|
|
18027
|
-
"use strict";
|
|
18028
|
-
}
|
|
18029
|
-
});
|
|
18030
|
-
|
|
18031
18261
|
// src/tools/snapshot.ts
|
|
18032
18262
|
async function handleTakeSnapshot(args2) {
|
|
18033
18263
|
try {
|
|
@@ -18063,6 +18293,9 @@ async function handleTakeSnapshot(args2) {
|
|
|
18063
18293
|
if (maxDepth !== void 0) {
|
|
18064
18294
|
options.maxDepth = maxDepth;
|
|
18065
18295
|
}
|
|
18296
|
+
if (saveTo) {
|
|
18297
|
+
options.maxAttrLength = Infinity;
|
|
18298
|
+
}
|
|
18066
18299
|
const formattedText = formatSnapshotTree2(snapshot.json.root, 0, options);
|
|
18067
18300
|
if (saveTo) {
|
|
18068
18301
|
const saved = await saveOutput(
|
|
@@ -18071,7 +18304,7 @@ async function handleTakeSnapshot(args2) {
|
|
|
18071
18304
|
"snapshot",
|
|
18072
18305
|
"txt"
|
|
18073
18306
|
);
|
|
18074
|
-
let output2 = `Snapshot
|
|
18307
|
+
let output2 = `Snapshot saved to: ${saved.path} (${(saved.bytes / 1024).toFixed(1)}KB)`;
|
|
18075
18308
|
if (snapshot.json.truncated) {
|
|
18076
18309
|
output2 += " [DOM truncated]";
|
|
18077
18310
|
}
|
|
@@ -18084,7 +18317,7 @@ async function handleTakeSnapshot(args2) {
|
|
|
18084
18317
|
const lines = formattedText.split("\n");
|
|
18085
18318
|
const truncated = lines.length > maxLines;
|
|
18086
18319
|
const displayLines = truncated ? lines.slice(0, maxLines) : lines;
|
|
18087
|
-
let output =
|
|
18320
|
+
let output = "Snapshot";
|
|
18088
18321
|
if (selector) {
|
|
18089
18322
|
output += ` [selector: ${selector}]`;
|
|
18090
18323
|
}
|
|
@@ -18100,9 +18333,11 @@ async function handleTakeSnapshot(args2) {
|
|
|
18100
18333
|
output += "\n\n";
|
|
18101
18334
|
output += displayLines.join("\n");
|
|
18102
18335
|
if (truncated) {
|
|
18103
|
-
output +=
|
|
18104
|
-
|
|
18105
|
-
|
|
18336
|
+
output += "\n\n" + truncationFooter(lines.length - maxLines, "lines", [
|
|
18337
|
+
"maxLines to show more",
|
|
18338
|
+
"selector to scope",
|
|
18339
|
+
"saveTo to dump the full tree to a file"
|
|
18340
|
+
]);
|
|
18106
18341
|
}
|
|
18107
18342
|
return successResponse(output);
|
|
18108
18343
|
} catch (error2) {
|
|
@@ -18124,7 +18359,7 @@ async function handleResolveUidToSelector(args2) {
|
|
|
18124
18359
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
18125
18360
|
const firefox3 = await getFirefox2();
|
|
18126
18361
|
try {
|
|
18127
|
-
const selector = firefox3.resolveUidToSelector(uid);
|
|
18362
|
+
const selector = await firefox3.resolveUidToSelector(uid);
|
|
18128
18363
|
return successResponse(`${uid} \u2192 ${selector}`);
|
|
18129
18364
|
} catch (error2) {
|
|
18130
18365
|
throw handleUidError(error2, uid);
|
|
@@ -18137,7 +18372,7 @@ async function handleClearSnapshot(_args) {
|
|
|
18137
18372
|
try {
|
|
18138
18373
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
18139
18374
|
const firefox3 = await getFirefox2();
|
|
18140
|
-
firefox3.clearSnapshot();
|
|
18375
|
+
await firefox3.clearSnapshot();
|
|
18141
18376
|
return successResponse("\u{1F9F9} Snapshot cleared");
|
|
18142
18377
|
} catch (error2) {
|
|
18143
18378
|
return errorResponse(error2);
|
|
@@ -18154,7 +18389,7 @@ var init_snapshot2 = __esm({
|
|
|
18154
18389
|
DEFAULT_SNAPSHOT_LINES = 100;
|
|
18155
18390
|
takeSnapshotTool = {
|
|
18156
18391
|
name: "take_snapshot",
|
|
18157
|
-
description: "Capture DOM snapshot with stable UIDs.
|
|
18392
|
+
description: "Capture DOM snapshot with stable UIDs. A UID stays valid across snapshots until its element is removed or the page navigates. Output caps at maxLines (default 100); scope with selector or dump the full tree with saveTo.",
|
|
18158
18393
|
annotations: {
|
|
18159
18394
|
readOnlyHint: true
|
|
18160
18395
|
},
|
|
@@ -18198,7 +18433,7 @@ var init_snapshot2 = __esm({
|
|
|
18198
18433
|
};
|
|
18199
18434
|
resolveUidToSelectorTool = {
|
|
18200
18435
|
name: "resolve_uid_to_selector",
|
|
18201
|
-
description: "Resolve UID to CSS selector. Fails if
|
|
18436
|
+
description: "Resolve UID to CSS selector. Fails if the element is gone.",
|
|
18202
18437
|
annotations: {
|
|
18203
18438
|
readOnlyHint: true
|
|
18204
18439
|
},
|
|
@@ -18215,7 +18450,7 @@ var init_snapshot2 = __esm({
|
|
|
18215
18450
|
};
|
|
18216
18451
|
clearSnapshotTool = {
|
|
18217
18452
|
name: "clear_snapshot",
|
|
18218
|
-
description: "Clear snapshot
|
|
18453
|
+
description: "Clear snapshot UIDs. Usually not needed.",
|
|
18219
18454
|
annotations: {
|
|
18220
18455
|
readOnlyHint: false
|
|
18221
18456
|
},
|
|
@@ -18535,6 +18770,55 @@ var init_input = __esm({
|
|
|
18535
18770
|
});
|
|
18536
18771
|
|
|
18537
18772
|
// src/tools/network.ts
|
|
18773
|
+
async function safeFetchBody(firefox3, id, dataType) {
|
|
18774
|
+
if (typeof firefox3.getNetworkRequestBody !== "function") {
|
|
18775
|
+
return { ok: false, reason: "unsupported" };
|
|
18776
|
+
}
|
|
18777
|
+
try {
|
|
18778
|
+
return await firefox3.getNetworkRequestBody(id, dataType);
|
|
18779
|
+
} catch {
|
|
18780
|
+
return { ok: false, reason: "error" };
|
|
18781
|
+
}
|
|
18782
|
+
}
|
|
18783
|
+
function bodyUnavailableMarker(reason) {
|
|
18784
|
+
switch (reason) {
|
|
18785
|
+
case "unsupported":
|
|
18786
|
+
return "<not captured: body collection not supported by this Firefox>";
|
|
18787
|
+
case "not-collected":
|
|
18788
|
+
return "<not captured>";
|
|
18789
|
+
case "evicted":
|
|
18790
|
+
return "<not available: evicted from the capture buffer>";
|
|
18791
|
+
case "aborted":
|
|
18792
|
+
return "<not available: collection aborted>";
|
|
18793
|
+
default:
|
|
18794
|
+
return "<not available>";
|
|
18795
|
+
}
|
|
18796
|
+
}
|
|
18797
|
+
function hasRequestBody(result) {
|
|
18798
|
+
return result.ok || result.reason === "evicted" || result.reason === "aborted";
|
|
18799
|
+
}
|
|
18800
|
+
function renderBodyInline(result) {
|
|
18801
|
+
if (!result.ok) {
|
|
18802
|
+
return { body: bodyUnavailableMarker(result.reason) };
|
|
18803
|
+
}
|
|
18804
|
+
if (result.type === "base64") {
|
|
18805
|
+
const approxKb = (result.value.length * 3 / 4 / 1024).toFixed(1);
|
|
18806
|
+
return {
|
|
18807
|
+
body: `<binary data (~${approxKb}KB); use saveTo to retrieve the full body>`,
|
|
18808
|
+
encoding: "base64"
|
|
18809
|
+
};
|
|
18810
|
+
}
|
|
18811
|
+
return { body: truncateText(result.value, TOKEN_LIMITS.MAX_RESPONSE_CHARS) };
|
|
18812
|
+
}
|
|
18813
|
+
function renderBodyForFile(result) {
|
|
18814
|
+
if (!result.ok) {
|
|
18815
|
+
return { body: null, encoding: null, unavailable: result.reason };
|
|
18816
|
+
}
|
|
18817
|
+
return {
|
|
18818
|
+
body: result.value,
|
|
18819
|
+
encoding: result.type === "base64" ? "base64" : "utf-8"
|
|
18820
|
+
};
|
|
18821
|
+
}
|
|
18538
18822
|
async function handleListNetworkRequests(args2) {
|
|
18539
18823
|
try {
|
|
18540
18824
|
const {
|
|
@@ -18643,6 +18927,11 @@ async function handleListNetworkRequests(args2) {
|
|
|
18643
18927
|
const effectiveDetail = detail ?? "summary";
|
|
18644
18928
|
const limitedRequests = requests.slice(0, effectiveLimit);
|
|
18645
18929
|
const hasMore = requests.length > effectiveLimit;
|
|
18930
|
+
const moreFooter = hasMore ? "\n" + truncationFooter(requests.length - limitedRequests.length, "requests", [
|
|
18931
|
+
"limit to show more",
|
|
18932
|
+
"urlContains/method/status to filter",
|
|
18933
|
+
"saveTo to save all to a file"
|
|
18934
|
+
]) : "";
|
|
18646
18935
|
if (format === "json") {
|
|
18647
18936
|
const responseData = {
|
|
18648
18937
|
total: requests.length,
|
|
@@ -18684,7 +18973,7 @@ async function handleListNetworkRequests(args2) {
|
|
|
18684
18973
|
});
|
|
18685
18974
|
const header = `[network] ${requests.length} requests${hasMore ? ` (limit ${effectiveLimit})` : ""}
|
|
18686
18975
|
`;
|
|
18687
|
-
return successResponse(header + formattedRequests.join("\n"));
|
|
18976
|
+
return successResponse(header + formattedRequests.join("\n") + moreFooter);
|
|
18688
18977
|
} else if (effectiveDetail === "min") {
|
|
18689
18978
|
const minData = limitedRequests.map((req) => ({
|
|
18690
18979
|
id: req.id,
|
|
@@ -18698,7 +18987,7 @@ async function handleListNetworkRequests(args2) {
|
|
|
18698
18987
|
}));
|
|
18699
18988
|
return successResponse(
|
|
18700
18989
|
`[network] ${requests.length} requests${hasMore ? ` (limit ${effectiveLimit})` : ""}
|
|
18701
|
-
` + JSON.stringify(minData, null, 2)
|
|
18990
|
+
` + JSON.stringify(minData, null, 2) + moreFooter
|
|
18702
18991
|
);
|
|
18703
18992
|
} else {
|
|
18704
18993
|
const fullData = limitedRequests.map((req) => ({
|
|
@@ -18715,7 +19004,7 @@ async function handleListNetworkRequests(args2) {
|
|
|
18715
19004
|
}));
|
|
18716
19005
|
return successResponse(
|
|
18717
19006
|
`[network] ${requests.length} requests${hasMore ? ` (limit ${effectiveLimit})` : ""}
|
|
18718
|
-
` + JSON.stringify(fullData, null, 2)
|
|
19007
|
+
` + JSON.stringify(fullData, null, 2) + moreFooter
|
|
18719
19008
|
);
|
|
18720
19009
|
}
|
|
18721
19010
|
} catch (error2) {
|
|
@@ -18764,8 +19053,29 @@ async function handleGetNetworkRequest(args2) {
|
|
|
18764
19053
|
requestHeaders: request.requestHeaders ?? null,
|
|
18765
19054
|
responseHeaders: request.responseHeaders ?? null
|
|
18766
19055
|
};
|
|
19056
|
+
const [responseBodyResult, requestBodyResult] = await Promise.all([
|
|
19057
|
+
safeFetchBody(firefox3, request.id, "response"),
|
|
19058
|
+
safeFetchBody(firefox3, request.id, "request")
|
|
19059
|
+
]);
|
|
18767
19060
|
if (saveTo) {
|
|
18768
|
-
const
|
|
19061
|
+
const responseFile = renderBodyForFile(responseBodyResult);
|
|
19062
|
+
const fileObject = {
|
|
19063
|
+
...fullDetails,
|
|
19064
|
+
responseBody: responseFile.body,
|
|
19065
|
+
responseBodyEncoding: responseFile.encoding
|
|
19066
|
+
};
|
|
19067
|
+
if (responseFile.unavailable) {
|
|
19068
|
+
fileObject.responseBodyUnavailable = responseFile.unavailable;
|
|
19069
|
+
}
|
|
19070
|
+
if (hasRequestBody(requestBodyResult)) {
|
|
19071
|
+
const requestFile = renderBodyForFile(requestBodyResult);
|
|
19072
|
+
fileObject.requestBody = requestFile.body;
|
|
19073
|
+
fileObject.requestBodyEncoding = requestFile.encoding;
|
|
19074
|
+
if (requestFile.unavailable) {
|
|
19075
|
+
fileObject.requestBodyUnavailable = requestFile.unavailable;
|
|
19076
|
+
}
|
|
19077
|
+
}
|
|
19078
|
+
const fileBody = JSON.stringify(fileObject, null, 2);
|
|
18769
19079
|
const saved = await saveOutput(
|
|
18770
19080
|
fileBody,
|
|
18771
19081
|
saveTo === true ? void 0 : saveTo,
|
|
@@ -18778,11 +19088,23 @@ async function handleGetNetworkRequest(args2) {
|
|
|
18778
19088
|
}
|
|
18779
19089
|
return successResponse(output);
|
|
18780
19090
|
}
|
|
19091
|
+
const responseInline = renderBodyInline(responseBodyResult);
|
|
18781
19092
|
const details = {
|
|
18782
19093
|
...fullDetails,
|
|
18783
19094
|
requestHeaders: truncateHeaders(request.requestHeaders),
|
|
18784
|
-
responseHeaders: truncateHeaders(request.responseHeaders)
|
|
19095
|
+
responseHeaders: truncateHeaders(request.responseHeaders),
|
|
19096
|
+
responseBody: responseInline.body
|
|
18785
19097
|
};
|
|
19098
|
+
if (responseInline.encoding) {
|
|
19099
|
+
details.responseBodyEncoding = responseInline.encoding;
|
|
19100
|
+
}
|
|
19101
|
+
if (hasRequestBody(requestBodyResult)) {
|
|
19102
|
+
const requestInline = renderBodyInline(requestBodyResult);
|
|
19103
|
+
details.requestBody = requestInline.body;
|
|
19104
|
+
if (requestInline.encoding) {
|
|
19105
|
+
details.requestBodyEncoding = requestInline.encoding;
|
|
19106
|
+
}
|
|
19107
|
+
}
|
|
18786
19108
|
if (format === "json") {
|
|
18787
19109
|
return jsonResponse(details);
|
|
18788
19110
|
}
|
|
@@ -18800,7 +19122,7 @@ var init_network2 = __esm({
|
|
|
18800
19122
|
init_module();
|
|
18801
19123
|
listNetworkRequestsTool = {
|
|
18802
19124
|
name: "list_network_requests",
|
|
18803
|
-
description: "List network requests
|
|
19125
|
+
description: "List network requests, returning IDs for get_network_request. Filter by url/method/status; caps at limit (default 50); saveTo saves all matches to a file.",
|
|
18804
19126
|
annotations: {
|
|
18805
19127
|
readOnlyHint: true
|
|
18806
19128
|
},
|
|
@@ -18871,7 +19193,7 @@ var init_network2 = __esm({
|
|
|
18871
19193
|
};
|
|
18872
19194
|
getNetworkRequestTool = {
|
|
18873
19195
|
name: "get_network_request",
|
|
18874
|
-
description: "Get request details by ID. URL lookup as fallback.",
|
|
19196
|
+
description: "Get request details by ID, including the response body (and request body when present). Large text bodies are truncated inline; binary bodies are summarized. URL lookup as fallback.",
|
|
18875
19197
|
annotations: {
|
|
18876
19198
|
readOnlyHint: true
|
|
18877
19199
|
},
|
|
@@ -18893,7 +19215,7 @@ var init_network2 = __esm({
|
|
|
18893
19215
|
},
|
|
18894
19216
|
saveTo: {
|
|
18895
19217
|
type: ["boolean", "string"],
|
|
18896
|
-
description: "Save the request details with full untruncated headers to a file as JSON instead of returning them inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
19218
|
+
description: "Save the request details with full untruncated headers and bodies to a file as JSON instead of returning them inline (binary bodies are stored base64-encoded). Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
18897
19219
|
},
|
|
18898
19220
|
preview: {
|
|
18899
19221
|
type: "number",
|
|
@@ -19072,8 +19394,11 @@ Total messages: ${totalCount}${filterInfo.length > 0 ? `, Filters: ${filterInfo.
|
|
|
19072
19394
|
`;
|
|
19073
19395
|
}
|
|
19074
19396
|
if (truncated) {
|
|
19075
|
-
output +=
|
|
19076
|
-
|
|
19397
|
+
output += "\n" + truncationFooter(filteredCount - messages.length, "messages", [
|
|
19398
|
+
"limit to show more",
|
|
19399
|
+
"level/textContains/source to filter",
|
|
19400
|
+
"saveTo to save all to a file"
|
|
19401
|
+
]);
|
|
19077
19402
|
}
|
|
19078
19403
|
return successResponse(output);
|
|
19079
19404
|
} catch (error2) {
|
|
@@ -19100,7 +19425,7 @@ var init_console2 = __esm({
|
|
|
19100
19425
|
init_module();
|
|
19101
19426
|
listConsoleMessagesTool = {
|
|
19102
19427
|
name: "list_console_messages",
|
|
19103
|
-
description: "List console messages
|
|
19428
|
+
description: "List console messages, filterable by level, time, text, source. Caps at limit (default 50); saveTo saves all matches to a file.",
|
|
19104
19429
|
annotations: {
|
|
19105
19430
|
readOnlyHint: true
|
|
19106
19431
|
},
|
|
@@ -19570,7 +19895,7 @@ var init_utilities = __esm({
|
|
|
19570
19895
|
});
|
|
19571
19896
|
|
|
19572
19897
|
// src/tools/firefox-management.ts
|
|
19573
|
-
import { readFileSync as
|
|
19898
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
19574
19899
|
async function handleGetFirefoxLogs(input) {
|
|
19575
19900
|
try {
|
|
19576
19901
|
const {
|
|
@@ -19598,7 +19923,7 @@ async function handleGetFirefoxLogs(input) {
|
|
|
19598
19923
|
);
|
|
19599
19924
|
}
|
|
19600
19925
|
}
|
|
19601
|
-
const content =
|
|
19926
|
+
const content = readFileSync3(logFilePath, "utf-8");
|
|
19602
19927
|
let allLines = content.split("\n").filter((line) => line.trim().length > 0);
|
|
19603
19928
|
if (grep) {
|
|
19604
19929
|
const grepLower = grep.toLowerCase();
|
|
@@ -20488,67 +20813,6 @@ var init_screencast = __esm({
|
|
|
20488
20813
|
}
|
|
20489
20814
|
});
|
|
20490
20815
|
|
|
20491
|
-
// src/utils/remote-value.ts
|
|
20492
|
-
function remoteValueToNative(rv) {
|
|
20493
|
-
if (!rv || typeof rv !== "object") {
|
|
20494
|
-
return rv;
|
|
20495
|
-
}
|
|
20496
|
-
const { type, value } = rv;
|
|
20497
|
-
switch (type) {
|
|
20498
|
-
case "undefined":
|
|
20499
|
-
return void 0;
|
|
20500
|
-
case "null":
|
|
20501
|
-
return null;
|
|
20502
|
-
case "string":
|
|
20503
|
-
case "boolean":
|
|
20504
|
-
return value;
|
|
20505
|
-
case "number":
|
|
20506
|
-
if (value === "NaN") {
|
|
20507
|
-
return "NaN";
|
|
20508
|
-
}
|
|
20509
|
-
if (value === "Infinity") {
|
|
20510
|
-
return "Infinity";
|
|
20511
|
-
}
|
|
20512
|
-
if (value === "-Infinity") {
|
|
20513
|
-
return "-Infinity";
|
|
20514
|
-
}
|
|
20515
|
-
if (value === "-0") {
|
|
20516
|
-
return "-0";
|
|
20517
|
-
}
|
|
20518
|
-
return value;
|
|
20519
|
-
case "bigint":
|
|
20520
|
-
return `${value}n`;
|
|
20521
|
-
case "array":
|
|
20522
|
-
return value.map(remoteValueToNative);
|
|
20523
|
-
case "object":
|
|
20524
|
-
return Object.fromEntries(
|
|
20525
|
-
value.map(([k, v]) => [k, remoteValueToNative(v)])
|
|
20526
|
-
);
|
|
20527
|
-
case "map":
|
|
20528
|
-
return Object.fromEntries(
|
|
20529
|
-
value.map(([k, v]) => [
|
|
20530
|
-
typeof k === "object" ? JSON.stringify(remoteValueToNative(k)) : String(k),
|
|
20531
|
-
remoteValueToNative(v)
|
|
20532
|
-
])
|
|
20533
|
-
);
|
|
20534
|
-
case "set":
|
|
20535
|
-
return value.map(remoteValueToNative);
|
|
20536
|
-
case "regexp": {
|
|
20537
|
-
const { pattern, flags } = value;
|
|
20538
|
-
return `/${pattern}/${flags ?? ""}`;
|
|
20539
|
-
}
|
|
20540
|
-
case "date":
|
|
20541
|
-
return value;
|
|
20542
|
-
default:
|
|
20543
|
-
return `[${type}]`;
|
|
20544
|
-
}
|
|
20545
|
-
}
|
|
20546
|
-
var init_remote_value = __esm({
|
|
20547
|
-
"src/utils/remote-value.ts"() {
|
|
20548
|
-
"use strict";
|
|
20549
|
-
}
|
|
20550
|
-
});
|
|
20551
|
-
|
|
20552
20816
|
// src/utils/js-validation.ts
|
|
20553
20817
|
function validateFunction(fnString) {
|
|
20554
20818
|
if (!fnString || typeof fnString !== "string") {
|
|
@@ -20588,6 +20852,7 @@ async function handleEvaluateScript(args2) {
|
|
|
20588
20852
|
function: fnString,
|
|
20589
20853
|
args: fnArgs,
|
|
20590
20854
|
timeout,
|
|
20855
|
+
sandbox,
|
|
20591
20856
|
saveTo,
|
|
20592
20857
|
preview
|
|
20593
20858
|
} = args2;
|
|
@@ -20619,7 +20884,7 @@ Please call take_snapshot to get fresh UIDs and try again.`
|
|
|
20619
20884
|
functionDeclaration: fnString,
|
|
20620
20885
|
awaitPromise: true,
|
|
20621
20886
|
arguments: resolvedArgs,
|
|
20622
|
-
target: { context: firefox3.getCurrentContextId() }
|
|
20887
|
+
target: { context: firefox3.getCurrentContextId(), ...sandbox !== void 0 && { sandbox } }
|
|
20623
20888
|
});
|
|
20624
20889
|
const result = await Promise.race([
|
|
20625
20890
|
new Promise((r) => setTimeout(() => r(TIMEOUT), scriptTimeout)),
|
|
@@ -20678,7 +20943,7 @@ var init_script = __esm({
|
|
|
20678
20943
|
init_module();
|
|
20679
20944
|
evaluateScriptTool = {
|
|
20680
20945
|
name: "evaluate_script",
|
|
20681
|
-
description: "
|
|
20946
|
+
description: "Run a JS function in the page and return its result. Prefer this for targeted reads (a value, text, computed style, whether an element exists) instead of a full take_snapshot. Use the UID interaction tools for clicking, typing, and filling.",
|
|
20682
20947
|
annotations: {
|
|
20683
20948
|
readOnlyHint: false
|
|
20684
20949
|
},
|
|
@@ -20707,6 +20972,10 @@ var init_script = __esm({
|
|
|
20707
20972
|
type: "number",
|
|
20708
20973
|
description: "Timeout in ms (default: 5000)"
|
|
20709
20974
|
},
|
|
20975
|
+
sandbox: {
|
|
20976
|
+
type: "string",
|
|
20977
|
+
description: "Evaluate in an isolated sandbox realm with this name instead of the page realm. The sandbox shares the page DOM and keeps the native built-ins even where the page overrode them. Page-defined globals and expandos are invisible from the sandbox, and vice-versa. The same name reuses the same sandbox across calls; omit to evaluate in the page realm."
|
|
20978
|
+
},
|
|
20710
20979
|
saveTo: {
|
|
20711
20980
|
type: ["boolean", "string"],
|
|
20712
20981
|
description: "Save the result to a file as JSON instead of returning it inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
@@ -21182,6 +21451,25 @@ function formatContextList(contexts) {
|
|
|
21182
21451
|
}
|
|
21183
21452
|
return lines.join("\n");
|
|
21184
21453
|
}
|
|
21454
|
+
async function assertPrivilegedContext(firefox3, contextId) {
|
|
21455
|
+
let result;
|
|
21456
|
+
try {
|
|
21457
|
+
result = await firefox3.sendBiDiCommand("browsingContext.getTree", {
|
|
21458
|
+
"moz:scope": "chrome"
|
|
21459
|
+
});
|
|
21460
|
+
} catch (error2) {
|
|
21461
|
+
if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
|
|
21462
|
+
throw new Error(SYSTEM_ACCESS_ERROR);
|
|
21463
|
+
}
|
|
21464
|
+
throw error2;
|
|
21465
|
+
}
|
|
21466
|
+
const contexts = result.contexts || [];
|
|
21467
|
+
if (!contexts.some((ctx) => ctx.context === contextId)) {
|
|
21468
|
+
throw new Error(
|
|
21469
|
+
`${contextId} is not a privileged context. Use list_privileged_contexts to see valid ids.`
|
|
21470
|
+
);
|
|
21471
|
+
}
|
|
21472
|
+
}
|
|
21185
21473
|
async function handleListPrivilegedContexts(_args) {
|
|
21186
21474
|
try {
|
|
21187
21475
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
@@ -21193,11 +21481,7 @@ async function handleListPrivilegedContexts(_args) {
|
|
|
21193
21481
|
return successResponse(formatContextList(contexts));
|
|
21194
21482
|
} catch (error2) {
|
|
21195
21483
|
if (error2 instanceof Error && error2.message.includes("UnsupportedOperationError")) {
|
|
21196
|
-
return errorResponse(
|
|
21197
|
-
new Error(
|
|
21198
|
-
"Privileged context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox."
|
|
21199
|
-
)
|
|
21200
|
-
);
|
|
21484
|
+
return errorResponse(new Error(SYSTEM_ACCESS_ERROR));
|
|
21201
21485
|
}
|
|
21202
21486
|
return errorResponse(error2);
|
|
21203
21487
|
}
|
|
@@ -21210,6 +21494,7 @@ async function handleSelectPrivilegedContext(args2) {
|
|
|
21210
21494
|
}
|
|
21211
21495
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
21212
21496
|
const firefox3 = await getFirefox2();
|
|
21497
|
+
await assertPrivilegedContext(firefox3, contextId);
|
|
21213
21498
|
const driver = firefox3.getDriver();
|
|
21214
21499
|
await driver.switchTo().window(contextId);
|
|
21215
21500
|
try {
|
|
@@ -21233,17 +21518,22 @@ async function handleEvaluatePrivilegedScript(args2) {
|
|
|
21233
21518
|
try {
|
|
21234
21519
|
const {
|
|
21235
21520
|
function: fnString,
|
|
21521
|
+
context,
|
|
21236
21522
|
saveTo,
|
|
21237
21523
|
preview
|
|
21238
21524
|
} = args2;
|
|
21239
21525
|
validateFunction(fnString);
|
|
21526
|
+
if (!context || typeof context !== "string") {
|
|
21527
|
+
throw new Error("context parameter is required and must be a string");
|
|
21528
|
+
}
|
|
21240
21529
|
const { getFirefox: getFirefox2 } = await Promise.resolve().then(() => (init_src(), src_exports));
|
|
21241
21530
|
const firefox3 = await getFirefox2();
|
|
21531
|
+
await assertPrivilegedContext(firefox3, context);
|
|
21242
21532
|
const result = await firefox3.sendBiDiCommand("script.callFunction", {
|
|
21243
21533
|
functionDeclaration: fnString,
|
|
21244
21534
|
awaitPromise: true,
|
|
21245
21535
|
arguments: [],
|
|
21246
|
-
target: { context
|
|
21536
|
+
target: { context }
|
|
21247
21537
|
});
|
|
21248
21538
|
if (result.type === EvaluateResultType2.Success) {
|
|
21249
21539
|
const json = JSON.stringify(remoteValueToNative(result.result), null, 2) ?? "undefined";
|
|
@@ -21280,7 +21570,7 @@ async function handleEvaluatePrivilegedScript(args2) {
|
|
|
21280
21570
|
return errorResponse(error2);
|
|
21281
21571
|
}
|
|
21282
21572
|
}
|
|
21283
|
-
var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool, EvaluateResultType2, module16;
|
|
21573
|
+
var listPrivilegedContextsTool, selectPrivilegedContextTool, evaluatePrivilegedScriptTool, SYSTEM_ACCESS_ERROR, EvaluateResultType2, module16;
|
|
21284
21574
|
var init_privileged_context = __esm({
|
|
21285
21575
|
"src/tools/privileged-context.ts"() {
|
|
21286
21576
|
"use strict";
|
|
@@ -21320,7 +21610,7 @@ var init_privileged_context = __esm({
|
|
|
21320
21610
|
};
|
|
21321
21611
|
evaluatePrivilegedScriptTool = {
|
|
21322
21612
|
name: "evaluate_privileged_script",
|
|
21323
|
-
description: "Execute JS function in
|
|
21613
|
+
description: "Execute JS function in a privileged (chrome) browsing context. Requires MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 env var. Get context ids from list_privileged_contexts.",
|
|
21324
21614
|
annotations: {
|
|
21325
21615
|
readOnlyHint: false
|
|
21326
21616
|
},
|
|
@@ -21331,6 +21621,10 @@ var init_privileged_context = __esm({
|
|
|
21331
21621
|
type: "string",
|
|
21332
21622
|
description: 'JS function string, e.g. () => Services.prefs.getBoolPref("foo")'
|
|
21333
21623
|
},
|
|
21624
|
+
context: {
|
|
21625
|
+
type: "string",
|
|
21626
|
+
description: "Privileged browsing context ID from list_privileged_contexts"
|
|
21627
|
+
},
|
|
21334
21628
|
saveTo: {
|
|
21335
21629
|
type: ["boolean", "string"],
|
|
21336
21630
|
description: "Save the result to a file as JSON instead of returning it inline. Pass a file path, an existing directory (generated file inside), or true (generated file under ~/.firefox-devtools-mcp/output/). Relative paths resolve against the current working directory."
|
|
@@ -21340,9 +21634,10 @@ var init_privileged_context = __esm({
|
|
|
21340
21634
|
description: "Number of characters of the saved result to return inline as a preview when saveTo is used. Omit for no preview."
|
|
21341
21635
|
}
|
|
21342
21636
|
},
|
|
21343
|
-
required: ["function"]
|
|
21637
|
+
required: ["function", "context"]
|
|
21344
21638
|
}
|
|
21345
21639
|
};
|
|
21640
|
+
SYSTEM_ACCESS_ERROR = "Privileged context access not enabled. Set MOZ_REMOTE_ALLOW_SYSTEM_ACCESS=1 environment variable and restart Firefox.";
|
|
21346
21641
|
EvaluateResultType2 = {
|
|
21347
21642
|
Exception: "exception",
|
|
21348
21643
|
Success: "success"
|
|
@@ -21547,6 +21842,12 @@ var init_cli = __esm({
|
|
|
21547
21842
|
description: "Marionette port to connect to when using --connect-existing (default: 2828)",
|
|
21548
21843
|
default: Number(process.env.MARIONETTE_PORT ?? "2828")
|
|
21549
21844
|
},
|
|
21845
|
+
lookupMarionettePort: {
|
|
21846
|
+
type: "boolean",
|
|
21847
|
+
hidden: true,
|
|
21848
|
+
description: "Lookup the port of a Marionette instance started by Firefox's AI assistant companion, instead of using --marionette-port. Only applies when --connect-existing is set.",
|
|
21849
|
+
default: (process.env.LOOKUP_MARIONETTE_PORT ?? "false") === "true"
|
|
21850
|
+
},
|
|
21550
21851
|
env: {
|
|
21551
21852
|
type: "array",
|
|
21552
21853
|
description: "Environment variables for Firefox in KEY=VALUE format. Can be specified multiple times. Example: --env MOZ_LOG=HTMLMediaElement:4"
|
|
@@ -21570,6 +21871,11 @@ var init_cli = __esm({
|
|
|
21570
21871
|
description: "Android app package name (default: org.mozilla.firefox). Use org.mozilla.fenix for Nightly.",
|
|
21571
21872
|
default: process.env.ANDROID_PACKAGE ?? "org.mozilla.firefox"
|
|
21572
21873
|
},
|
|
21874
|
+
androidWipeAppData: {
|
|
21875
|
+
type: "boolean",
|
|
21876
|
+
description: "Confirm that connecting to Firefox for Android wipes all data of the target app (tabs, history, bookmarks, passwords, settings). Required with --android-device.",
|
|
21877
|
+
default: (process.env.ANDROID_WIPE_APP_DATA ?? "false") === "true"
|
|
21878
|
+
},
|
|
21573
21879
|
logFile: {
|
|
21574
21880
|
type: "string",
|
|
21575
21881
|
description: "Path to a file where MCP server logs will be written. Set DEBUG=* to also enable verbose debug logs."
|
|
@@ -21604,6 +21910,12 @@ var init_cli = __esm({
|
|
|
21604
21910
|
type: "boolean",
|
|
21605
21911
|
description: "Allow tools that save files (e.g. screenshots, snapshots, network/console output) to write to arbitrary locations. By default, relative save paths resolve against the current working directory and absolute save paths are restricted to ~/.firefox-devtools-mcp; this flag lifts both restrictions. Use with caution.",
|
|
21606
21912
|
default: (process.env.UNRESTRICTED_SAVE_PATHS ?? "false") === "true"
|
|
21913
|
+
},
|
|
21914
|
+
disableNetworkBodyCollection: {
|
|
21915
|
+
type: "boolean",
|
|
21916
|
+
hidden: true,
|
|
21917
|
+
description: "Disable capturing network request/response bodies via BiDi data collectors. get_network_request will still return metadata and headers but no bodies. Reduces browser memory and stream-cloning overhead.",
|
|
21918
|
+
default: (process.env.DISABLE_NETWORK_BODY_COLLECTION ?? "false") === "true"
|
|
21607
21919
|
}
|
|
21608
21920
|
};
|
|
21609
21921
|
}
|