@mozilla/firefox-devtools-mcp 0.10.0 → 0.10.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.
Files changed (2) hide show
  1. package/dist/index.js +353 -328
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14841,7 +14841,7 @@ var init_constants = __esm({
14841
14841
  "src/config/constants.ts"() {
14842
14842
  "use strict";
14843
14843
  SERVER_NAME = true ? "@mozilla/firefox-devtools-mcp" : "firefox-devtools";
14844
- SERVER_VERSION = true ? "0.10.0" : "dev";
14844
+ SERVER_VERSION = true ? "0.10.1" : "dev";
14845
14845
  }
14846
14846
  });
14847
14847
 
@@ -15348,68 +15348,6 @@ var init_core3 = __esm({
15348
15348
  getOptions() {
15349
15349
  return this.options;
15350
15350
  }
15351
- /**
15352
- * Wait for WebSocket to be in OPEN state
15353
- */
15354
- async waitForWebSocketOpen(ws, timeout = 5e3) {
15355
- if (ws.readyState === 1) {
15356
- return;
15357
- }
15358
- if (ws.readyState === 0) {
15359
- return new Promise((resolve4, reject) => {
15360
- const timeoutId = setTimeout(() => {
15361
- ws.off("open", onOpen);
15362
- reject(new Error("Timeout waiting for WebSocket to open"));
15363
- }, timeout);
15364
- const onOpen = () => {
15365
- clearTimeout(timeoutId);
15366
- ws.off("open", onOpen);
15367
- resolve4();
15368
- };
15369
- ws.on("open", onOpen);
15370
- });
15371
- }
15372
- throw new Error(`WebSocket is not open: readyState ${ws.readyState}`);
15373
- }
15374
- /**
15375
- * Send raw BiDi command and get response
15376
- */
15377
- async sendBiDiCommand(method, params = {}) {
15378
- if (!this.driver) {
15379
- throw new Error("Driver not connected");
15380
- }
15381
- const bidi = await this.driver.getBidi();
15382
- const ws = bidi.socket;
15383
- await this.waitForWebSocketOpen(ws);
15384
- const id = Math.floor(Math.random() * 1e6);
15385
- return new Promise((resolve4, reject) => {
15386
- const messageHandler = (data) => {
15387
- try {
15388
- const payload = JSON.parse(data.toString());
15389
- if (payload.id === id) {
15390
- ws.off("message", messageHandler);
15391
- if (payload.error) {
15392
- reject(new Error(`BiDi error: ${JSON.stringify(payload.error)}`));
15393
- } else {
15394
- resolve4(payload.result);
15395
- }
15396
- }
15397
- } catch {
15398
- }
15399
- };
15400
- ws.on("message", messageHandler);
15401
- const command = {
15402
- id,
15403
- method,
15404
- params
15405
- };
15406
- ws.send(JSON.stringify(command));
15407
- setTimeout(() => {
15408
- ws.off("message", messageHandler);
15409
- reject(new Error(`BiDi command timeout: ${method}`));
15410
- }, 1e4);
15411
- });
15412
- }
15413
15351
  /**
15414
15352
  * Close driver and cleanup.
15415
15353
  * - Tries graceful quit() with a timeout; on timeout, force-kills via onQuit_().
@@ -15479,6 +15417,107 @@ var init_core3 = __esm({
15479
15417
  }
15480
15418
  });
15481
15419
 
15420
+ // src/firefox/bidi.ts
15421
+ import EventEmitter from "events";
15422
+ var BiDiFacade;
15423
+ var init_bidi = __esm({
15424
+ "src/firefox/bidi.ts"() {
15425
+ "use strict";
15426
+ init_logger();
15427
+ BiDiFacade = class extends EventEmitter {
15428
+ constructor(driver) {
15429
+ super();
15430
+ this.driver = driver;
15431
+ }
15432
+ listening = false;
15433
+ nextCommandId = 1;
15434
+ async subscribe(events) {
15435
+ const bidi = await this.driver.getBidi();
15436
+ if (!this.listening) {
15437
+ this.listenForEvents(bidi.socket);
15438
+ this.listening = true;
15439
+ }
15440
+ await bidi.subscribe(events);
15441
+ }
15442
+ async sendCommand(method, params = {}) {
15443
+ const bidi = await this.driver.getBidi();
15444
+ const ws = bidi.socket;
15445
+ await this.waitForWebSocketOpen(ws);
15446
+ const id = this.nextCommandId++;
15447
+ return new Promise((resolve4, reject) => {
15448
+ const messageHandler = (data) => {
15449
+ try {
15450
+ const payload = JSON.parse(data.toString());
15451
+ if (payload.id === id) {
15452
+ ws.off("message", messageHandler);
15453
+ if (payload.error) {
15454
+ reject(new Error(`BiDi error: ${JSON.stringify(payload.error)}`));
15455
+ } else {
15456
+ resolve4(payload.result);
15457
+ }
15458
+ }
15459
+ } catch {
15460
+ }
15461
+ };
15462
+ ws.on("message", messageHandler);
15463
+ const command = {
15464
+ id,
15465
+ method,
15466
+ params
15467
+ };
15468
+ ws.send(JSON.stringify(command));
15469
+ setTimeout(() => {
15470
+ ws.off("message", messageHandler);
15471
+ reject(new Error(`BiDi command timeout: ${method}`));
15472
+ }, 1e4);
15473
+ });
15474
+ }
15475
+ listenForEvents(ws) {
15476
+ ws.on("message", (data) => {
15477
+ let payload;
15478
+ try {
15479
+ payload = JSON.parse(data.toString());
15480
+ } catch {
15481
+ return;
15482
+ }
15483
+ if (payload?.type === "event" && payload.method) {
15484
+ try {
15485
+ this.emit(payload.method, payload.params);
15486
+ } catch (error2) {
15487
+ logDebug(
15488
+ `Error emitting ${payload.method} event: ${error2 instanceof Error ? error2.message : String(error2)}`
15489
+ );
15490
+ }
15491
+ }
15492
+ });
15493
+ }
15494
+ /**
15495
+ * Wait for WebSocket to be in OPEN state
15496
+ */
15497
+ async waitForWebSocketOpen(ws, timeout = 5e3) {
15498
+ if (ws.readyState === 1) {
15499
+ return;
15500
+ }
15501
+ if (ws.readyState === 0) {
15502
+ return new Promise((resolve4, reject) => {
15503
+ const timeoutId = setTimeout(() => {
15504
+ ws.off("open", onOpen);
15505
+ reject(new Error("Timeout waiting for WebSocket to open"));
15506
+ }, timeout);
15507
+ const onOpen = () => {
15508
+ clearTimeout(timeoutId);
15509
+ ws.off("open", onOpen);
15510
+ resolve4();
15511
+ };
15512
+ ws.on("open", onOpen);
15513
+ });
15514
+ }
15515
+ throw new Error(`WebSocket is not open: readyState ${ws.readyState}`);
15516
+ }
15517
+ };
15518
+ }
15519
+ });
15520
+
15482
15521
  // src/utils/remote-value.ts
15483
15522
  function remoteValueToNative(rv) {
15484
15523
  if (!rv || typeof rv !== "object") {
@@ -15549,8 +15588,8 @@ var init_console = __esm({
15549
15588
  MAX_CONSOLE_MESSAGES = 1e3;
15550
15589
  CONSOLE_TTL_MS = 5 * 60 * 1e3;
15551
15590
  ConsoleEvents = class {
15552
- constructor(driver, options = {}) {
15553
- this.driver = driver;
15591
+ constructor(bidi, options = {}) {
15592
+ this.bidi = bidi;
15554
15593
  this.options = {
15555
15594
  autoClearOnNavigate: false,
15556
15595
  // Changed default to false to preserve logs across tabs
@@ -15563,44 +15602,27 @@ var init_console = __esm({
15563
15602
  /**
15564
15603
  * Subscribe to BiDi console events and navigation lifecycle
15565
15604
  */
15566
- async subscribe(contextId) {
15605
+ async subscribe() {
15567
15606
  if (this.subscribed) {
15568
15607
  return;
15569
15608
  }
15570
- const bidi = await this.driver.getBidi();
15571
- await bidi.subscribe("log.entryAdded", contextId ? [contextId] : void 0);
15572
- try {
15573
- await bidi.subscribe("browsingContext.load", contextId ? [contextId] : void 0);
15574
- await bidi.subscribe("browsingContext.domContentLoaded", contextId ? [contextId] : void 0);
15575
- } catch {
15576
- logDebug(
15577
- "Navigation events subscription skipped (may not be available in this Firefox version)"
15578
- );
15579
- }
15580
- const ws = bidi.socket;
15581
- ws.on("message", (data) => {
15582
- try {
15583
- const payload = JSON.parse(data.toString());
15584
- if (payload?.method === "log.entryAdded") {
15585
- const entry = payload.params;
15586
- const message = {
15587
- level: entry.level || "info",
15588
- text: entry.text || (entry.args ? JSON.stringify(entry.args) : ""),
15589
- timestamp: entry.timestamp || Date.now(),
15590
- source: entry.source?.realm,
15591
- args: entry.args
15592
- };
15593
- this.consoleMessages.push(message);
15594
- logDebug(`Console [${message.level}]: ${message.text}`);
15595
- }
15596
- if (payload?.method === "browsingContext.load" || payload?.method === "browsingContext.domContentLoaded") {
15597
- if (this.options.autoClearOnNavigate) {
15598
- this.clearMessages();
15599
- }
15600
- }
15601
- } catch {
15602
- }
15609
+ await this.bidi.subscribe("log.entryAdded");
15610
+ await this.bidi.subscribe(["browsingContext.load", "browsingContext.domContentLoaded"]);
15611
+ this.bidi.on("log.entryAdded", (entry) => {
15612
+ const message = {
15613
+ level: entry.level || "info",
15614
+ text: entry.text || (entry.args ? JSON.stringify(entry.args) : ""),
15615
+ timestamp: entry.timestamp || Date.now(),
15616
+ source: entry.source?.realm,
15617
+ args: entry.args
15618
+ };
15619
+ this.consoleMessages.push(message);
15620
+ logDebug(`Console [${message.level}]: ${message.text}`);
15603
15621
  });
15622
+ if (this.options.autoClearOnNavigate) {
15623
+ this.bidi.on("browsingContext.load", () => this.clearMessages());
15624
+ this.bidi.on("browsingContext.domContentLoaded", () => this.clearMessages());
15625
+ }
15604
15626
  this.subscribed = true;
15605
15627
  logDebug("Console listener active with lifecycle hooks");
15606
15628
  }
@@ -15648,9 +15670,8 @@ var init_network = __esm({
15648
15670
  NETWORK_TTL_MS = 5 * 60 * 1e3;
15649
15671
  MAX_ENCODED_DATA_SIZE = 10 * 1e3 * 1e3;
15650
15672
  NetworkEvents = class {
15651
- constructor(driver, options = {}, sendCommand) {
15652
- this.driver = driver;
15653
- this.sendCommand = sendCommand;
15673
+ constructor(bidi, options = {}) {
15674
+ this.bidi = bidi;
15654
15675
  this.options = {
15655
15676
  autoClearOnNavigate: true,
15656
15677
  ...options
@@ -15666,90 +15687,81 @@ var init_network = __esm({
15666
15687
  * Subscribe to BiDi network events and navigation lifecycle
15667
15688
  * Enables monitoring by default (always-on capture)
15668
15689
  */
15669
- async subscribe(contextId) {
15690
+ async subscribe() {
15670
15691
  if (this.subscribed) {
15671
15692
  return;
15672
15693
  }
15673
- const bidi = await this.driver.getBidi();
15674
- await bidi.subscribe("network.beforeRequestSent", contextId ? [contextId] : void 0);
15675
- await bidi.subscribe("network.responseStarted", contextId ? [contextId] : void 0);
15676
- await bidi.subscribe("network.responseCompleted", contextId ? [contextId] : void 0);
15677
- try {
15678
- await bidi.subscribe("browsingContext.load", contextId ? [contextId] : void 0);
15679
- await bidi.subscribe("browsingContext.domContentLoaded", contextId ? [contextId] : void 0);
15680
- } catch {
15681
- logDebug(
15682
- "Navigation events subscription skipped (may not be available in this Firefox version)"
15683
- );
15684
- }
15685
- const ws = bidi.socket;
15686
- ws.on("message", (data) => {
15687
- try {
15688
- const payload = JSON.parse(data.toString());
15689
- if (payload?.method === "browsingContext.load" || payload?.method === "browsingContext.domContentLoaded") {
15690
- if (this.enabled && this.options.autoClearOnNavigate) {
15691
- this.clearRequests();
15692
- }
15693
- return;
15694
- }
15695
- if (!this.enabled) {
15696
- return;
15697
- }
15698
- if (payload?.method === "network.beforeRequestSent") {
15699
- const req = payload.params;
15700
- const requestId = req.request?.request || req.requestId;
15701
- if (!requestId) {
15702
- return;
15703
- }
15704
- this.requestStartTimes.set(requestId, Date.now());
15705
- const record2 = {
15706
- id: requestId,
15707
- url: req.request?.url || "",
15708
- method: req.request?.method || "GET",
15709
- timestamp: Date.now(),
15710
- resourceType: this.guessResourceType(req.request?.url || ""),
15711
- isXHR: req.initiator?.type === "xmlhttprequest" || req.initiator?.type === "fetch",
15712
- requestHeaders: this.parseHeaders(req.request?.headers || []),
15713
- timings: {
15714
- requestTime: Date.now()
15715
- }
15716
- };
15717
- this.networkRecords.set(requestId, record2);
15718
- logDebug(`Network request [${record2.method}]: ${record2.url}`);
15719
- }
15720
- if (payload?.method === "network.responseStarted") {
15721
- const resp = payload.params;
15722
- const requestId = resp.request?.request || resp.requestId;
15723
- if (!requestId) {
15724
- return;
15725
- }
15726
- const existing = this.networkRecords.get(requestId);
15727
- if (existing) {
15728
- existing.status = resp.response?.status;
15729
- existing.statusText = resp.response?.statusText || "";
15730
- existing.responseHeaders = this.parseHeaders(resp.response?.headers || []);
15731
- }
15694
+ await this.bidi.subscribe([
15695
+ "network.beforeRequestSent",
15696
+ "network.responseStarted",
15697
+ "network.responseCompleted"
15698
+ ]);
15699
+ await this.bidi.subscribe(["browsingContext.load", "browsingContext.domContentLoaded"]);
15700
+ const onLoadEvent = () => {
15701
+ if (this.enabled && this.options.autoClearOnNavigate) {
15702
+ this.clearRequests();
15703
+ }
15704
+ };
15705
+ this.bidi.on("browsingContext.domContentLoaded", onLoadEvent);
15706
+ this.bidi.on("browsingContext.load", onLoadEvent);
15707
+ this.bidi.on("network.beforeRequestSent", (req) => {
15708
+ if (!this.enabled) {
15709
+ return;
15710
+ }
15711
+ const requestId = req.request?.request || req.requestId;
15712
+ if (!requestId) {
15713
+ return;
15714
+ }
15715
+ this.requestStartTimes.set(requestId, Date.now());
15716
+ const record2 = {
15717
+ id: requestId,
15718
+ url: req.request?.url || "",
15719
+ method: req.request?.method || "GET",
15720
+ timestamp: Date.now(),
15721
+ resourceType: this.guessResourceType(req.request?.url || ""),
15722
+ isXHR: req.initiator?.type === "xmlhttprequest" || req.initiator?.type === "fetch",
15723
+ requestHeaders: this.parseHeaders(req.request?.headers || []),
15724
+ timings: {
15725
+ requestTime: Date.now()
15732
15726
  }
15733
- if (payload?.method === "network.responseCompleted") {
15734
- const resp = payload.params;
15735
- const requestId = resp.request?.request || resp.requestId;
15736
- if (!requestId) {
15737
- return;
15738
- }
15739
- const existing = this.networkRecords.get(requestId);
15740
- const startTime = this.requestStartTimes.get(requestId);
15741
- if (existing && startTime) {
15742
- existing.timings.responseTime = Date.now();
15743
- existing.timings.duration = Date.now() - startTime;
15744
- if (!existing.status && resp.response?.status) {
15745
- existing.status = resp.response.status;
15746
- existing.statusText = resp.response.statusText || "";
15747
- }
15748
- }
15749
- this.requestStartTimes.delete(requestId);
15727
+ };
15728
+ this.networkRecords.set(requestId, record2);
15729
+ logDebug(`Network request [${record2.method}]: ${record2.url}`);
15730
+ });
15731
+ this.bidi.on("network.responseStarted", (resp) => {
15732
+ if (!this.enabled) {
15733
+ return;
15734
+ }
15735
+ const requestId = resp.request?.request || resp.requestId;
15736
+ if (!requestId) {
15737
+ return;
15738
+ }
15739
+ const existing = this.networkRecords.get(requestId);
15740
+ if (existing) {
15741
+ existing.status = resp.response?.status;
15742
+ existing.statusText = resp.response?.statusText || "";
15743
+ existing.responseHeaders = this.parseHeaders(resp.response?.headers || []);
15744
+ }
15745
+ });
15746
+ this.bidi.on("network.responseCompleted", (resp) => {
15747
+ if (!this.enabled) {
15748
+ return;
15749
+ }
15750
+ const requestId = resp.request?.request || resp.requestId;
15751
+ if (!requestId) {
15752
+ return;
15753
+ }
15754
+ const existing = this.networkRecords.get(requestId);
15755
+ const startTime = this.requestStartTimes.get(requestId);
15756
+ if (existing && startTime) {
15757
+ existing.timings.responseTime = Date.now();
15758
+ existing.timings.duration = Date.now() - startTime;
15759
+ if (!existing.status && resp.response?.status) {
15760
+ existing.status = resp.response.status;
15761
+ existing.statusText = resp.response.statusText || "";
15750
15762
  }
15751
- } catch {
15752
15763
  }
15764
+ this.requestStartTimes.delete(requestId);
15753
15765
  });
15754
15766
  await this.registerDataCollector();
15755
15767
  this.subscribed = true;
@@ -15765,11 +15777,8 @@ var init_network = __esm({
15765
15777
  logDebug("Network body capture disabled, skipping data collector registration");
15766
15778
  return;
15767
15779
  }
15768
- if (!this.sendCommand) {
15769
- return;
15770
- }
15771
15780
  try {
15772
- const result = await this.sendCommand("network.addDataCollector", {
15781
+ const result = await this.bidi.sendCommand("network.addDataCollector", {
15773
15782
  dataTypes: ["request", "response"],
15774
15783
  maxEncodedDataSize: MAX_ENCODED_DATA_SIZE
15775
15784
  });
@@ -15789,11 +15798,11 @@ var init_network = __esm({
15789
15798
  * when the body was never collected, evicted, or the browser lacks support.
15790
15799
  */
15791
15800
  async fetchBody(requestId, dataType) {
15792
- if (!this.sendCommand || !this.collectorId) {
15801
+ if (!this.collectorId) {
15793
15802
  return { ok: false, reason: "unsupported" };
15794
15803
  }
15795
15804
  try {
15796
- const result = await this.sendCommand("network.getData", {
15805
+ const result = await this.bidi.sendCommand("network.getData", {
15797
15806
  request: requestId,
15798
15807
  dataType
15799
15808
  });
@@ -15975,46 +15984,35 @@ var init_debugging = __esm({
15975
15984
  init_logger();
15976
15985
  MAX_LOGPOINT_RESULTS = 100;
15977
15986
  DebuggingEvents = class {
15978
- constructor(driver, sendBiDiCommand) {
15979
- this.driver = driver;
15980
- this.sendBiDiCommand = sendBiDiCommand;
15987
+ constructor(bidi) {
15988
+ this.bidi = bidi;
15981
15989
  }
15982
15990
  logpoints = /* @__PURE__ */ new Map();
15983
15991
  subscribed = false;
15984
15992
  /**
15985
15993
  * Subscribe to moz:debugging events
15986
15994
  */
15987
- async subscribe(contextId) {
15995
+ async subscribe() {
15988
15996
  if (this.subscribed) {
15989
15997
  return;
15990
15998
  }
15991
- const bidi = await this.driver.getBidi();
15992
15999
  try {
15993
- await bidi.subscribe("moz:debugging.paused", contextId ? [contextId] : void 0);
15994
- await bidi.subscribe("moz:debugging.resumed", contextId ? [contextId] : void 0);
16000
+ await this.bidi.subscribe(["moz:debugging.paused", "moz:debugging.resumed"]);
15995
16001
  } catch {
15996
16002
  logDebug(
15997
16003
  "Debugging events subscription skipped (may not be available in this Firefox version)"
15998
16004
  );
15999
16005
  }
16000
- const ws = bidi.socket;
16001
- ws.on("message", (data) => {
16002
- try {
16003
- const payload = JSON.parse(data.toString());
16004
- if (payload?.method === "moz:debugging.paused") {
16005
- const { context, url, line, column } = payload.params;
16006
- const logpointId = this.findLogpointByLocation(url, line);
16007
- if (logpointId) {
16008
- void this.handleLogpointPause(context, logpointId);
16009
- return;
16010
- }
16011
- logDebug(`moz:Debugging paused in context: ${context} at ${url}:${line}:${column}`);
16012
- }
16013
- if (payload?.method === "moz:debugging.resumed") {
16014
- logDebug(`moz:Debugging resumed in context: ${payload.params.context}`);
16015
- }
16016
- } catch {
16006
+ this.bidi.on("moz:debugging.paused", ({ context, url, line, column }) => {
16007
+ const logpointId = this.findLogpointByLocation(url, line);
16008
+ if (logpointId) {
16009
+ void this.handleLogpointPause(context, logpointId);
16010
+ return;
16017
16011
  }
16012
+ logDebug(`moz:Debugging paused in context: ${context} at ${url}:${line}:${column}`);
16013
+ });
16014
+ this.bidi.on("moz:debugging.resumed", (entry) => {
16015
+ logDebug(`moz:Debugging resumed in context: ${entry.context}`);
16018
16016
  });
16019
16017
  this.subscribed = true;
16020
16018
  logDebug("moz:debugging listener active");
@@ -16048,7 +16046,7 @@ var init_debugging = __esm({
16048
16046
  }
16049
16047
  logDebug(`Logpoint hit: ${logpointId} in context ${contextId}`);
16050
16048
  try {
16051
- const result = await this.sendBiDiCommand("script.evaluate", {
16049
+ const result = await this.bidi.sendCommand("script.evaluate", {
16052
16050
  expression: entry.expression,
16053
16051
  target: { context: contextId },
16054
16052
  awaitPromise: false
@@ -16080,7 +16078,7 @@ var init_debugging = __esm({
16080
16078
  logDebug(`Logpoint ${logpointId}: result buffer capped at ${MAX_LOGPOINT_RESULTS}`);
16081
16079
  }
16082
16080
  }
16083
- await this.sendBiDiCommand("moz:debugging.resume", { context: contextId }).catch((err) => {
16081
+ await this.bidi.sendCommand("moz:debugging.resume", { context: contextId }).catch((err) => {
16084
16082
  logDebug(`Failed to resume after logpoint: ${String(err)}`);
16085
16083
  });
16086
16084
  }
@@ -16098,8 +16096,8 @@ var init_downloads = __esm({
16098
16096
  MAX_DOWNLOADS = 500;
16099
16097
  DOWNLOAD_TTL_MS = 30 * 60 * 1e3;
16100
16098
  DownloadEvents = class {
16101
- constructor(driver) {
16102
- this.driver = driver;
16099
+ constructor(bidi) {
16100
+ this.bidi = bidi;
16103
16101
  }
16104
16102
  downloads = /* @__PURE__ */ new Map();
16105
16103
  subscribed = false;
@@ -16111,65 +16109,53 @@ var init_downloads = __esm({
16111
16109
  /**
16112
16110
  * Subscribe to BiDi download events.
16113
16111
  */
16114
- async subscribe(contextId) {
16112
+ async subscribe() {
16115
16113
  if (this.subscribed) {
16116
16114
  return;
16117
16115
  }
16118
- const bidi = await this.driver.getBidi();
16119
- const contexts = contextId ? [contextId] : void 0;
16120
- await bidi.subscribe("browsingContext.downloadWillBegin", contexts);
16121
- await bidi.subscribe("browsingContext.downloadEnd", contexts);
16122
- const ws = bidi.socket;
16123
- ws.on("message", (data) => {
16124
- try {
16125
- const payload = JSON.parse(data.toString());
16126
- if (payload?.method === "browsingContext.downloadWillBegin") {
16127
- const p = payload.params;
16128
- let key = p.download ?? p.navigation;
16129
- if (!key) {
16130
- key = `download-${this.fallbackCounter++}`;
16131
- this.pendingFallbackKey = key;
16132
- }
16133
- this.downloads.set(key, {
16134
- id: key,
16135
- context: p.context,
16136
- navigation: p.navigation ?? null,
16137
- url: p.url || "",
16138
- suggestedFilename: p.suggestedFilename || "",
16139
- status: "in_progress",
16140
- startTimestamp: p.timestamp ?? Date.now()
16141
- });
16142
- logDebug(`Download started: filename=${p.suggestedFilename}, url=${p.url}, id=${key}`);
16143
- }
16144
- if (payload?.method === "browsingContext.downloadEnd") {
16145
- const p = payload.params;
16146
- let key = p?.download ?? p?.navigation;
16147
- if (!key) {
16148
- key = this.pendingFallbackKey ?? `download-${this.fallbackCounter++}`;
16149
- this.pendingFallbackKey = null;
16150
- }
16151
- const existing = this.downloads.get(key) ?? {
16152
- id: key,
16153
- context: p.context,
16154
- navigation: p.navigation ?? null,
16155
- url: p.url || "",
16156
- suggestedFilename: "",
16157
- status: "in_progress",
16158
- startTimestamp: p.timestamp ?? Date.now()
16159
- };
16160
- existing.status = p.status;
16161
- existing.endTimestamp = p.timestamp ?? Date.now();
16162
- existing.durationMs = existing.endTimestamp - existing.startTimestamp;
16163
- if (p.status === "complete" && p.filepath) {
16164
- existing.filepath = p.filepath;
16165
- }
16166
- this.downloads.set(key, existing);
16167
- logDebug(
16168
- `Download ${p.status}: filepath=${existing.filepath}, url=${existing.url}, id=${key}`
16169
- );
16170
- }
16171
- } catch {
16116
+ await this.bidi.subscribe(["browsingContext.downloadWillBegin", "browsingContext.downloadEnd"]);
16117
+ this.bidi.on("browsingContext.downloadWillBegin", (p) => {
16118
+ let key = p.download ?? p.navigation;
16119
+ if (!key) {
16120
+ key = `download-${this.fallbackCounter++}`;
16121
+ this.pendingFallbackKey = key;
16122
+ }
16123
+ this.downloads.set(key, {
16124
+ id: key,
16125
+ context: p.context,
16126
+ navigation: p.navigation ?? null,
16127
+ url: p.url || "",
16128
+ suggestedFilename: p.suggestedFilename || "",
16129
+ status: "in_progress",
16130
+ startTimestamp: p.timestamp ?? Date.now()
16131
+ });
16132
+ logDebug(`Download started: filename=${p.suggestedFilename}, url=${p.url}, id=${key}`);
16133
+ });
16134
+ this.bidi.on("browsingContext.downloadEnd", (p) => {
16135
+ let key = p?.download ?? p?.navigation;
16136
+ if (!key) {
16137
+ key = this.pendingFallbackKey ?? `download-${this.fallbackCounter++}`;
16138
+ this.pendingFallbackKey = null;
16139
+ }
16140
+ const existing = this.downloads.get(key) ?? {
16141
+ id: key,
16142
+ context: p.context,
16143
+ navigation: p.navigation ?? null,
16144
+ url: p.url || "",
16145
+ suggestedFilename: "",
16146
+ status: "in_progress",
16147
+ startTimestamp: p.timestamp ?? Date.now()
16148
+ };
16149
+ existing.status = p.status;
16150
+ existing.endTimestamp = p.timestamp ?? Date.now();
16151
+ existing.durationMs = existing.endTimestamp - existing.startTimestamp;
16152
+ if (p.status === "complete" && p.filepath) {
16153
+ existing.filepath = p.filepath;
16172
16154
  }
16155
+ this.downloads.set(key, existing);
16156
+ logDebug(
16157
+ `Download ${p.status}: filepath=${existing.filepath}, url=${existing.url}, id=${key}`
16158
+ );
16173
16159
  });
16174
16160
  this.subscribed = true;
16175
16161
  logDebug("Download listener ready");
@@ -17052,6 +17038,7 @@ var init_firefox = __esm({
17052
17038
  "src/firefox/index.ts"() {
17053
17039
  "use strict";
17054
17040
  init_core3();
17041
+ init_bidi();
17055
17042
  init_logger();
17056
17043
  init_remote_value();
17057
17044
  init_events();
@@ -17060,6 +17047,7 @@ var init_firefox = __esm({
17060
17047
  init_snapshot();
17061
17048
  FirefoxClient = class {
17062
17049
  core;
17050
+ bidi = null;
17063
17051
  consoleEvents = null;
17064
17052
  networkEvents = null;
17065
17053
  debuggingEvents = null;
@@ -17070,31 +17058,52 @@ var init_firefox = __esm({
17070
17058
  constructor(options) {
17071
17059
  this.core = new FirefoxCore(options);
17072
17060
  }
17061
+ getBidi() {
17062
+ if (!this.bidi) {
17063
+ throw new Error("Not connected");
17064
+ }
17065
+ return this.bidi;
17066
+ }
17073
17067
  /**
17074
17068
  * Connect and initialize all modules
17075
17069
  */
17076
17070
  async connect() {
17077
17071
  await this.core.connect();
17078
17072
  const driver = this.core.getDriver();
17073
+ this.bidi = new BiDiFacade(driver);
17079
17074
  this.snapshot = new SnapshotManager(driver);
17080
- const hasBidi = "getBidi" in driver && typeof driver.getBidi === "function";
17081
- if (hasBidi) {
17082
- this.consoleEvents = new ConsoleEvents(driver, {
17083
- autoClearOnNavigate: false
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
- );
17093
- this.debuggingEvents = new DebuggingEvents(
17094
- driver,
17095
- (method, params) => this.core.sendBiDiCommand(method, params)
17096
- );
17097
- this.downloadEvents = new DownloadEvents(driver);
17075
+ this.consoleEvents = new ConsoleEvents(this.bidi, {
17076
+ autoClearOnNavigate: false
17077
+ });
17078
+ try {
17079
+ await this.consoleEvents.subscribe();
17080
+ } catch {
17081
+ logDebug("Unable to subscribe to console events");
17082
+ this.consoleEvents = null;
17083
+ }
17084
+ this.networkEvents = new NetworkEvents(this.bidi, {
17085
+ autoClearOnNavigate: false,
17086
+ captureBodies: this.core.getOptions().captureNetworkBodies !== false
17087
+ });
17088
+ try {
17089
+ await this.networkEvents.subscribe();
17090
+ } catch {
17091
+ logDebug("Unable to subscribe to network events");
17092
+ this.networkEvents = null;
17093
+ }
17094
+ this.debuggingEvents = new DebuggingEvents(this.bidi);
17095
+ try {
17096
+ await this.debuggingEvents.subscribe();
17097
+ } catch {
17098
+ logDebug("Unable to subscribe to debugging events");
17099
+ this.debuggingEvents = null;
17100
+ }
17101
+ this.downloadEvents = new DownloadEvents(this.bidi);
17102
+ try {
17103
+ await this.downloadEvents.subscribe();
17104
+ } catch {
17105
+ logDebug("Unable to subscribe to download events");
17106
+ this.downloadEvents = null;
17098
17107
  }
17099
17108
  this.dom = new DomInteractions(
17100
17109
  driver,
@@ -17104,40 +17113,8 @@ var init_firefox = __esm({
17104
17113
  driver,
17105
17114
  () => this.core.getCurrentContextId(),
17106
17115
  (id) => this.core.setCurrentContextId(id),
17107
- (method, params) => this.core.sendBiDiCommand(method, params)
17116
+ (method, params) => this.getBidi().sendCommand(method, params)
17108
17117
  );
17109
- if (this.consoleEvents) {
17110
- try {
17111
- await this.consoleEvents.subscribe(void 0);
17112
- } catch {
17113
- logDebug("Unable to subscribe to console events");
17114
- this.consoleEvents = null;
17115
- }
17116
- }
17117
- if (this.networkEvents) {
17118
- try {
17119
- await this.networkEvents.subscribe(void 0);
17120
- } catch {
17121
- logDebug("Unable to subscribe to network events");
17122
- this.networkEvents = null;
17123
- }
17124
- }
17125
- if (this.debuggingEvents) {
17126
- try {
17127
- await this.debuggingEvents.subscribe();
17128
- } catch {
17129
- logDebug("Unable to subscribe to debugging events");
17130
- this.debuggingEvents = null;
17131
- }
17132
- }
17133
- if (this.downloadEvents) {
17134
- try {
17135
- await this.downloadEvents.subscribe(void 0);
17136
- } catch {
17137
- logDebug("Unable to subscribe to download events");
17138
- this.downloadEvents = null;
17139
- }
17140
- }
17141
17118
  }
17142
17119
  // ============================================================================
17143
17120
  // DOM / Evaluate
@@ -17149,7 +17126,7 @@ var init_firefox = __esm({
17149
17126
  * native value; throws on a script exception.
17150
17127
  */
17151
17128
  async evaluate(expression) {
17152
- const result = await this.core.sendBiDiCommand("script.evaluate", {
17129
+ const result = await this.getBidi().sendCommand("script.evaluate", {
17153
17130
  expression,
17154
17131
  awaitPromise: true,
17155
17132
  target: { context: this.core.getCurrentContextId() }
@@ -17394,7 +17371,7 @@ var init_firefox = __esm({
17394
17371
  */
17395
17372
  async setDownloadBehavior(behavior) {
17396
17373
  const downloadBehavior = behavior === "default" ? null : behavior === "allowed" ? { type: "allowed" } : { type: "denied" };
17397
- await this.core.sendBiDiCommand("browser.setDownloadBehavior", { downloadBehavior });
17374
+ await this.getBidi().sendCommand("browser.setDownloadBehavior", { downloadBehavior });
17398
17375
  }
17399
17376
  // ============================================================================
17400
17377
  // Snapshot
@@ -17446,7 +17423,7 @@ var init_firefox = __esm({
17446
17423
  * @internal
17447
17424
  */
17448
17425
  async sendBiDiCommand(method, params = {}) {
17449
- return await this.core.sendBiDiCommand(method, params);
17426
+ return await this.getBidi().sendCommand(method, params);
17450
17427
  }
17451
17428
  /**
17452
17429
  * Get WebDriver instance (for advanced operations)
@@ -17491,7 +17468,7 @@ var init_firefox = __esm({
17491
17468
  if (!this.debuggingEvents) {
17492
17469
  throw new Error("Debugging events not available");
17493
17470
  }
17494
- const result = await this.core.sendBiDiCommand("moz:debugging.setBreakpoint", {
17471
+ const result = await this.getBidi().sendCommand("moz:debugging.setBreakpoint", {
17495
17472
  location: { url, line }
17496
17473
  });
17497
17474
  const logpointId = result.breakpoint;
@@ -17505,7 +17482,7 @@ var init_firefox = __esm({
17505
17482
  if (!this.debuggingEvents) {
17506
17483
  throw new Error("Debugging events not available");
17507
17484
  }
17508
- await this.core.sendBiDiCommand("moz:debugging.removeBreakpoint", {
17485
+ await this.getBidi().sendCommand("moz:debugging.removeBreakpoint", {
17509
17486
  breakpoint: logpointId
17510
17487
  });
17511
17488
  this.debuggingEvents.removeLogpoint(logpointId);
@@ -17559,11 +17536,56 @@ var init_firefox = __esm({
17559
17536
  }
17560
17537
  });
17561
17538
 
17539
+ // src/tools/instructions.ts
17540
+ function buildInstructions(moduleNames, toolNames) {
17541
+ const byName = new Map(MODULES.map((m) => [m.name, m]));
17542
+ const capabilities = moduleNames.flatMap((name) => {
17543
+ const module17 = byName.get(name);
17544
+ return module17 ? [`- ${module17.name}: ${module17.description}`] : [];
17545
+ });
17546
+ const sections = [INSTRUCTIONS_INTRO, ["Enabled capabilities:", ...capabilities].join("\n")];
17547
+ const core = CORE_TOOLS.filter((name) => toolNames.has(name));
17548
+ if (core.length > 0) {
17549
+ const groups = TASK_TOOLS.flatMap(({ purpose, tools }) => {
17550
+ const available = tools.filter((name) => toolNames.has(name));
17551
+ return available.length > 0 ? [`${available.join(" / ")} for ${purpose}`] : [];
17552
+ });
17553
+ const extra = groups.length > 0 ? ` Add task-specific tools to the same search: ${groups.join(", ")}.` : "";
17554
+ sections.push(
17555
+ `When these tools are deferred, load the core set in one search rather than one at a time: ${core.join(", ")}.${extra}`
17556
+ );
17557
+ }
17558
+ return sections.join("\n\n");
17559
+ }
17560
+ var INSTRUCTIONS_INTRO, CORE_TOOLS, TASK_TOOLS;
17561
+ var init_instructions = __esm({
17562
+ "src/tools/instructions.ts"() {
17563
+ "use strict";
17564
+ init_tools();
17565
+ INSTRUCTIONS_INTRO = [
17566
+ "Browse and debug the web with Firefox.",
17567
+ "Use this for any browser or web page task: opening a URL, automating a web flow,",
17568
+ "or debugging a page (console errors, network requests, DOM state).",
17569
+ "The task does not need to name Firefox, reach for these tools unless another",
17570
+ "browser is explicitly requested.",
17571
+ "Chrome-only browser tools do not control Firefox, do not assume they cover this."
17572
+ ].join(" ");
17573
+ CORE_TOOLS = ["list_pages", "new_page", "navigate_page", "take_snapshot", "get_page_text"];
17574
+ TASK_TOOLS = [
17575
+ { purpose: "interaction", tools: ["click_by_uid", "fill_by_uid", "hover_by_uid"] },
17576
+ { purpose: "debugging", tools: ["list_console_messages", "list_network_requests"] },
17577
+ { purpose: "visuals", tools: ["screenshot_page", "screenshot_by_uid"] },
17578
+ { purpose: "one-off JS", tools: ["evaluate_script"] }
17579
+ ];
17580
+ }
17581
+ });
17582
+
17562
17583
  // src/tools/registry.ts
17563
17584
  function buildToolset(options) {
17564
17585
  const { moduleNames, warnings } = selectModules(options);
17565
17586
  const { toolDefinitions, handlers } = collectTools(moduleNames);
17566
- return { moduleNames, warnings, toolDefinitions, handlers };
17587
+ const instructions = buildInstructions(moduleNames, new Set(handlers.keys()));
17588
+ return { moduleNames, warnings, toolDefinitions, handlers, instructions };
17567
17589
  }
17568
17590
  function selectModules(options) {
17569
17591
  const { tools: requested, preset, enableScript, enablePrivilegedContext } = options;
@@ -17651,6 +17673,7 @@ var init_registry = __esm({
17651
17673
  "src/tools/registry.ts"() {
17652
17674
  "use strict";
17653
17675
  init_tools();
17676
+ init_instructions();
17654
17677
  privilegedModuleNames = new Set(MODULES.filter((m) => m.privileged).map((m) => m.name));
17655
17678
  }
17656
17679
  });
@@ -17771,7 +17794,8 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
17771
17794
  moduleNames,
17772
17795
  warnings,
17773
17796
  toolDefinitions: allTools,
17774
- handlers: toolHandlers
17797
+ handlers: toolHandlers,
17798
+ instructions
17775
17799
  } = buildToolset({
17776
17800
  tools: args.tools,
17777
17801
  preset: args.toolPreset,
@@ -17801,7 +17825,8 @@ async function run(parseArgsFn, importMetaUrl, allowPrivileged = false) {
17801
17825
  {
17802
17826
  capabilities: {
17803
17827
  tools: {}
17804
- }
17828
+ },
17829
+ instructions
17805
17830
  }
17806
17831
  );
17807
17832
  server.setRequestHandler(ListToolsRequestSchema, async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozilla/firefox-devtools-mcp",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "Model Context Protocol (MCP) server for Firefox DevTools automation",
5
5
  "author": "Mozilla",
6
6
  "license": "MIT OR Apache-2.0",